
/* ---------------------------------------------------------------- fonts */
/* Space Grotesk replaces Outfit as the display face. Outfit is a clean
   geometric sans with almost no personality -- it is the safe default, and
   "the font looks simple" was a fair reading of it. Space Grotesk is a
   grotesque with deliberately odd letterforms (the single-storey a, the
   angular g and j, the flat-sided o) which read as engineered rather than
   generic, and it has a true companion monospace so the display voice and
   the machine voice come from one family instead of borrowing the system
   stack. 6.8KB and 4.1KB -- together still less than Outfit alone. */
@font-face{font-family:"Instrument Sans";
  src:url(/assets/fonts/instrument-latin.woff2) format("woff2");
  font-weight:400 700;font-style:normal;font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+2000-206F,U+20AC,U+2122}
@font-face{font-family:"Plus Jakarta Sans";
  src:url(/assets/fonts/jakarta-latin.woff2) format("woff2");
  font-weight:400 700;font-style:normal;font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+2000-206F,U+20AC,U+2122}
@font-face{font-family:"Space Grotesk";
  src:url(/assets/fonts/spacegrotesk-latin.woff2) format("woff2");
  font-weight:500 700;font-style:normal;font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215}
@font-face{font-family:"Space Mono";
  src:url(/assets/fonts/spacemono-latin.woff2) format("woff2");
  font-weight:400 700;font-style:normal;font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215}
@font-face{font-family:Outfit;src:url(/assets/fonts/outfit-latin.woff2) format("woff2");
  font-weight:500 800;font-style:normal;font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215}
@font-face{font-family:"Source Sans 3";src:url(/assets/fonts/sourcesans3-latin.woff2) format("woff2");
  font-weight:400 700;font-style:normal;font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215}

/* --------------------------------------------------------------- tokens */
:root{
  /* Surfaces. A colder, deeper canvas than the old warm bone: neon only
     reads as neon against something close to black. */
  /* The page is light. --ink is no longer the canvas: it is the colour of
     the dark BANDS that punctuate it -- the stat strip, one mid-page anchor,
     the final CTA and the footer. Two tones 3% apart read as one flat page,
     which is what "too white" actually means, so the tint is deliberately
     visible and the navy anchors give the page a spine. */
  --ink:#163F7E;          /* dark band ground (was the canvas) */
  --ink-800:#0B2A57;      /* deeper navy, hover and gradient end */
  --ink-700:#1B2740;
  --steel-500:#5A6675;
  --steel-300:#A9BDD8;
  --bone:#EAF0F8;         /* the visible tint band */
  --bone-2:#EAF0F8;
  --white:#FFFFFF;

  /* Brand. Three requested colours, each used where it measures safely --
     the numbers are why, not preference:
       #163F7E  white text 10.26:1  -> dark bands, headings, ghost buttons
       #0273FF  white text  4.29 X  near-black 4.56 OK -> fills, dark-ground text
       #1A7BFF  white text  3.95 X  near-black 4.95 OK -> hover, glow, accents
     Both bright blues fail with white text, so no button ever pairs them
     with white. On the near-black canvas they clear AA comfortably, which is
     exactly where the neon belongs. */
  --navy:#163F7E;
  --signal:#163F7E;       /* primary CTA fill: white text 10.26:1 */
  --signal-accent:#0273FF;/* accents, plates, borders -- never a fill under white text */
  --signal-hi:#0B2A57;    /* CTA hover, 14.16:1 */
  --signal-dk:#0057C2;    /* 6.68:1 on white -- the only blue safe as body text */
  --signal-lift:#9CC4FF;  /* accent ON the navy bands: 6.6:1. #4D95FF was
                             tuned for the old near-black canvas and only
                             measures 3.45:1 against navy. */
  --signal-lt:#EEF4FF;    /* tinted plate for icons on a light ground */
  --star:#7A5400;         /* 6.78:1 on white -- stars state the rating */
  --blue:#0273FF;
  --trust:#12B981;        /* "open now", verified -- cooled to match */
  --alert:#FF4D5E;        /* emergency badge only */

  /* Neon. One glow recipe, reused, so the effect reads as a system rather
     than as decoration sprinkled per component. */
  --glow:0 0 0 1px rgba(26,123,255,.35),0 0 18px rgba(26,123,255,.28);
  --glow-strong:0 0 0 1px rgba(26,123,255,.5),0 0 28px rgba(26,123,255,.45);
  --hairline:rgba(26,123,255,.22);

  /* Text */
  --text:#3A4552;         /* 9.75:1 on white */
  --text-soft:#5A6675;    /* 5.85:1 -- the lightest text allowed anywhere */
  --text-invert:#FFFFFF;
  --text-invert-soft:#D6E2F2;
  --ink-text:#101828;     /* headings, 17.75:1 */

  /* Lines */
  --line:#DCE5F0;
  --line-2:#CBD5E1;
  --line-dark:#2E3A55;

  /* Shape */
  --r-sm:6px; --r-md:12px; --r-lg:20px; --r-pill:999px;
  --shadow-1:0 1px 2px rgba(25,32,51,.06),0 2px 8px rgba(25,32,51,.05);
  --shadow-2:0 4px 12px rgba(25,32,51,.09),0 12px 28px rgba(25,32,51,.08);
  --shadow-3:0 18px 44px rgba(25,32,51,.18);

  /* Layout */
  --wrap:1160px; --gut:20px;

  /* Type */
  /* Space Grotesk tops out at 700. Every 800 in this sheet was rewritten to
     700 rather than left to synthesise -- a faux-bold smears the stems and
     is exactly the "cheap" look the real face was chosen to fix. */
  --display:"Instrument Sans","Plus Jakarta Sans","Segoe UI",system-ui,sans-serif;
  --body:"Plus Jakarta Sans","Segoe UI",system-ui,-apple-system,sans-serif;
  /* Third role, and the one that stops the page reading as a template.
     Geometric-sans-over-humanist-sans is the default pairing on every
     generated site; adding a machine voice for labels, counts and step
     numbers is what makes it look engineered. Costs nothing to ship -- the
     system stack is already on every device, no font file, no request. */
  --mono:"Space Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;

  /* .btn-ghost is transparent, so it must be told what it sits on. Any new
     dark surface MUST set all four of these -- the alternative is ink text on
     an ink background at contrast ratio 1.00, which is invisible in a
     screenshot and only shows up in getComputedStyle. */
  --ghost-fg:var(--text);
  --ghost-bd:var(--line);
  --ghost-hv:var(--ink);
  --ghost-hv-bg:rgba(25,32,51,.05);
}

/* ----------------------------------------------------------------- base */
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
body{margin:0;font-family:var(--body);font-size:17px;line-height:1.65;
  color:var(--text);background:var(--white);-webkit-font-smoothing:antialiased}
img{max-width:100%;height:auto;display:block}

/* An inline <svg> carrying only a viewBox has no intrinsic size, so it grows
   to fill whatever box it lands in. components.py's icon() always writes
   explicit width/height for exactly this reason -- but the icons that come
   in with the old site's prose do not, and nothing here caught them. They
   rendered at 608x608 each: 41 of them made one service page 47,891px tall
   and 70 made a city page 74,523px, most of it stacked black chevrons.

   Scoped to SVGs with no dimensions of their own, so every icon we author
   is untouched. An unsized inline SVG is a bug wherever it appears. */
svg:not([width]):not([height]){width:1.15em;height:1.15em;
  vertical-align:-.15em;flex:none}

/* Some old-site SVGs do carry dimensions -- just absurd ones. A decorative
   mark on a 186x190 viewBox is exported as width="705.6" height="720", and
   five of them stack 3,600px of ornament into one section. Cap the box and
   let the viewBox scale the art down inside it. */
.prose svg{max-width:100%;max-height:2.6rem;height:auto}
.prose svg[width]:not([width="0"]){width:auto}
a{color:inherit}
/* Type scale. The old one ran 3.4rem / 2.4rem / 1.4rem -- three sizes close
   enough that nothing led and nothing supported, which is what "weak
   typography" looks like in practice. This opens the top end and pulls the
   middle down, so the ratio between levels is large enough to read as
   hierarchy from across the room. Tracking tightens as size grows, the way
   type wants to be set; at 6rem the default spacing looks slack. */
h1,h2,h3,h4{font-family:var(--display);margin:0 0 .5em;font-weight:700;
  text-wrap:balance;color:var(--ink-text)}
h1{font-size:clamp(2.4rem,5.4vw,4.6rem);font-weight:700;
  letter-spacing:-.035em;line-height:1.0}
h2{font-size:clamp(1.85rem,4vw,3rem);font-weight:700;
  letter-spacing:-.028em;line-height:1.06}
h3{font-size:clamp(1.1rem,1.6vw,1.32rem);font-weight:700;
  letter-spacing:-.012em;line-height:1.25}
h4{font-size:1rem;font-family:var(--body);font-weight:700;line-height:1.3}
p{margin:0 0 1.1em}
ul,ol{margin:0 0 1.1em;padding-left:1.3em}
li{margin:.35em 0}
:focus-visible{outline:3px solid var(--blue);outline-offset:2px;border-radius:2px}
.skip{position:absolute;left:-9999px}
.skip:focus{left:8px;top:8px;z-index:200;background:var(--white);
  padding:10px 16px;border-radius:var(--r-sm);box-shadow:var(--shadow-2)}

/* ------------------------------------------------------------- utility */
.wrap{max-width:var(--wrap);margin:0 auto;padding:0 var(--gut)}
.sec{padding:clamp(2.4rem,5vw,4rem) 0}
.sec-tight{padding:clamp(2.2rem,4vw,3.2rem) 0}
/* Consecutive prose bands are one argument split for tonal rhythm, not two
   separate ideas. Paying full section padding at both sides of every join
   spent 3,520px on spacing alone on a 23-section city page. Halve the seam
   where two prose bands meet; keep the full break either side of a component
   band, where the change of subject earns it. */
.sec + .sec{padding-top:clamp(1.6rem,3.5vw,2.6rem)}
.sec.cta,.sec.panelband{padding:clamp(3rem,6vw,4.6rem) 0}
.lede{font-size:1.12rem;color:var(--text-soft);max-width:62ch}
/* --signal on bone measures 4.12:1 -- under the 4.5 needed for small text.
   The darker step passes, and it honours the rule that the accent is a fill,
   never text on a light ground. */
/* Micro-labels in the machine voice, with a lit rule running into them.
   Wide tracking at small size is the counterweight to the very tight
   tracking on the display sizes -- the contrast between the two is doing
   most of the work of making the page feel set rather than defaulted. */
.eyebrow{font-family:var(--mono);font-size:.72rem;font-weight:600;
  letter-spacing:.2em;text-transform:uppercase;color:var(--signal-dk);
  margin:0 0 .9rem;display:flex;align-items:center;gap:.7rem}
.eyebrow::before{content:"";width:26px;height:1px;flex:none;
  background:linear-gradient(90deg,transparent,currentColor)}
/* Every dark band, not just .on-dark. --signal-dk is tuned for light
   grounds and measures 2.92:1 on the canvas -- the same oversight that
   left .btn-ghost at 1.55:1 on these two sections. Listing them is the
   fix; a new dark band must be added here or it inherits a light-ground
   colour and fails. */
/* No per-band eyebrow overrides. The base rule reads --signal-dk, and every
   dark surface redefines that token in one place, so a band added later
   inherits the right colour without anyone remembering to list it. The
   previous version WAS a list, .offerband was missing from it, and the
   eyebrow rendered navy-on-navy at exactly 1.0:1 on all 122 pages. */
.grid{display:grid;gap:1.25rem}
.g-2{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}
.g-3{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}
.g-4{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}
.center{text-align:center}
.mt0{margin-top:0}

/* Context classes flip a whole section rather than every child carrying its
   own dark variant. */
/* ---------------------------------------------- dark-surface contract ----
   This has been the same bug five times: .btn-ghost at 1.55:1, .eyebrow at
   2.92:1, .offer-chip at 4.21:1, the inline CTA on /blog/, and a social link
   at 2.92:1. Every one was a token tuned for a light ground inheriting onto a
   dark one, and every one was fixed by adding another selector to another
   list -- which is why it kept happening.

   Fixed as a contract instead. Any surface that paints itself dark joins this
   list ONCE, and gets every token remapped: the ghost button, and
   --signal-dk, which is the deep blue that only measures safely on white.
   On a dark ground it resolves to --signal-lift, which measures 6.06:1.
   Nothing downstream needs to know. */
/* Membership is decided by ONE question: does this element paint itself a
   dark background? .sociallinks was added on the assumption it did -- it does
   not, its cards are white -- and remapping the token there pushed nine
   social links to 2.98:1 on white. Worse than the bug it was meant to fix.
   Check the element's own background before adding it here. */
.on-dark,.statband,.heatstrip,.finalcta,.icta,.statusbar,.offerband,
.cta,.sociallink-lead,.on-navy{
  --ghost-fg:var(--text-invert);--ghost-bd:var(--hairline);
  --ghost-hv:var(--white);--ghost-hv-bg:rgba(26,123,255,.14);
  --signal-dk:var(--signal-lift)}
.on-dark{background:var(--ink);color:var(--text-invert)}
.on-dark h1,.on-dark h2,.on-dark h3,.on-dark h4{color:var(--text-invert)}
.on-dark .lede{color:var(--text-invert-soft)}
.on-dark .eyebrow{color:var(--signal-lt)}
.on-bone{background:var(--bone);border-block:1px solid var(--line)}

/* A dark band flips EVERY token through one class. Doing it per-property is
   how a ghost button ends up at 1.55:1 and an eyebrow at 2.92:1 -- both have
   happened on this project more than once. */
.on-navy{background:var(--ink);color:var(--text-invert-soft);
  --ink-text:#FFFFFF; --text:#D6E2F2; --text-soft:#A9BDD8;
  --line:rgba(255,255,255,.16); --line-2:rgba(255,255,255,.26);
  --signal-dk:#9CC4FF; --star:#FFC24D; --white:rgba(255,255,255,.06);
  --ghost-fg:#FFFFFF; --ghost-bd:rgba(255,255,255,.4);
  --ghost-hv:#FFFFFF; --ghost-hv-bg:rgba(255,255,255,.1);
  border-block:1px solid rgba(2,115,255,.45)}
.on-navy h1,.on-navy h2,.on-navy h3,.on-navy h4{color:#fff}
.on-navy .btn-signal{background:#fff;color:var(--ink)}
.on-navy .btn-signal:hover{background:#EAF0F8}

/* Blueprint grid on the tinted bands. A garage door is a drawing before it
   is a door, so a measured grid is the one texture that belongs here rather
   than being applied because the page looked empty. */
.gridded{position:relative;isolation:isolate}
.gridded::before{content:"";position:absolute;inset:0;z-index:-1;
  pointer-events:none;opacity:.55;
  background-image:linear-gradient(rgba(22,63,126,.08) 1px,transparent 1px),
    linear-gradient(90deg,rgba(22,63,126,.08) 1px,transparent 1px);
  background-size:56px 56px;
  -webkit-mask-image:radial-gradient(ellipse 76% 66% at 50% 42%,#000,transparent);
  mask-image:radial-gradient(ellipse 76% 66% at 50% 42%,#000,transparent)}
.on-navy.gridded::before{
  background-image:linear-gradient(rgba(255,255,255,.09) 1px,transparent 1px),
    linear-gradient(90deg,rgba(255,255,255,.09) 1px,transparent 1px)}

/* The signature device. It was a 6px repeating bar echoing a sectional door
   panel; in amber it read as hazard tape, and once the accent went blue it
   read as a rendering glitch. Same idea, executed as light rather than
   paint: a 1px seam that ignites at the centre and falls off to nothing,
   with a soft bloom under it. One line, and it is the only place the neon
   touches a light ground. */
.panelband{position:relative}
.panelband::before{content:"";position:absolute;inset:0 0 auto;height:1px;
  background:linear-gradient(90deg,transparent,var(--signal-hi) 22%,
    var(--signal) 50%,var(--signal-hi) 78%,transparent);
  box-shadow:0 0 14px rgba(26,123,255,.55)}

/* ------------------------------------------------------------- buttons */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:.55rem;
  font-family:var(--display);font-weight:700;font-size:1rem;line-height:1;
  padding:.95rem 1.5rem;border-radius:var(--r-pill);border:2px solid transparent;
  text-decoration:none;cursor:pointer;transition:transform .12s ease,
  background-color .16s ease,border-color .16s ease,box-shadow .16s ease}
.btn:hover{transform:translateY(-1px)}
.btn:active{transform:translateY(0)}
@media (prefers-reduced-motion:reduce){.btn{transition:none}
  .btn:hover{transform:none}}
.btn-lg{padding:1.15rem 1.9rem;font-size:1.08rem}
.btn-block{width:100%}
/* Copper is a FILL with white text -- never copper text on a light ground. */
/* Amber is a fill with INK text -- 8.25:1, and exactly how the old site
   coloured its call-to-action. Amber text on a light ground would fail. */
/* Near-black text on the bright blue, never white: white measures 4.29:1 on
   #0273FF and fails AA, --ink measures 4.56:1 and passes. */
/* 44px floor on every button. 310 of them measured under 40px on mobile --
   mostly the compact inline-CTA variant -- which is under the tap target
   minimum for the controls we most want pressed. */
.btn{min-height:44px}
/* White on the navy fill: 10.26:1, past the 7:1 mark that measurably
   converts better. NOT var(--ink) -- since the palette flip that resolves to
   navy too, which rendered the button navy-on-navy at 1.0:1. */
.btn-signal{background:var(--signal);color:#fff;box-shadow:var(--shadow-2)}
.btn-signal:hover{background:var(--signal-hi);box-shadow:var(--shadow-3)}
.btn-ink{background:var(--ink);color:var(--text-invert)}
.btn-ink:hover{background:var(--ink-800)}
.btn-ghost{background:transparent;color:var(--ghost-fg);border-color:var(--ghost-bd)}
.btn-ghost:hover{color:var(--ghost-hv);background:var(--ghost-hv-bg)}

/* -------------------------------------------------------------- header */
.hdr{position:sticky;top:0;z-index:60;background:rgba(255,255,255,.94);
  backdrop-filter:saturate(140%) blur(8px);border-bottom:1px solid var(--line-dark)}
.hdr-in{display:flex;align-items:center;gap:1.25rem;
  min-height:72px;max-width:var(--wrap);margin:0 auto;padding:0 var(--gut)}
.hdr-logo{display:flex;align-items:center;gap:.6rem;text-decoration:none;
  font-family:var(--display);font-weight:700;font-size:1.12rem;
  color:var(--ink-text);letter-spacing:-.01em}
/* The badge carries the company name inside it, but at 44px that lettering
   is unreadable -- so the mark does recognition and the text beside it does
   reading. alt is empty because the adjacent text already names the company;
   a screen reader saying it twice is noise. */
.hdr-logo{display:inline-flex;align-items:center;gap:.6rem}
.hdr-logo img{width:44px;height:44px;flex:none;object-fit:contain}
@media (max-width:480px){.hdr-logo span{display:none}}
.hdr-nav{display:flex;gap:1.4rem;margin-left:auto}
.hdr-nav a{font-family:var(--display);font-weight:600;font-size:.97rem;
  color:var(--text);text-decoration:none;padding:.4rem 0;
  border-bottom:2px solid transparent}
.hdr-nav a:hover{color:var(--signal-dk)}
.hdr-nav a[aria-current=page]{color:var(--signal-dk);border-bottom-color:var(--signal)}  /* NOT --white: this header is light.
     White here made the current page's own nav link invisible on every
     page -- clicking Services landed you on a page whose "Services" tab
     had vanished. --signal-dk is 6.68:1 on white and still reads active. */
.hdr-act{display:flex;align-items:center;gap:.7rem}
.hdr-tel{display:flex;flex-direction:column;justify-content:center;
  line-height:1.15;text-decoration:none;min-height:44px}
.hdr-tel-lbl{font-size:.72rem;letter-spacing:.07em;text-transform:uppercase;
  color:var(--text-soft)}  /* --steel-300 is tuned for navy bands: 1.92:1 here */
.hdr-tel-num{font-family:var(--display);font-weight:700;font-size:1.16rem;
  color:var(--ink-text)}
/* 44px, not 42. It was two pixels under the tap minimum on every page
   in the site -- and it is the control that opens the whole nav. */
.hdr-burger{display:none;background:none;border:0;padding:.5rem;
  cursor:pointer;min-width:44px;min-height:44px}
.hdr-burger span{display:block;width:24px;height:2px;background:var(--ink-text);
  margin:5px 0;transition:transform .18s ease,opacity .18s ease}
.drawer{display:none;background:var(--white);border-top:1px solid var(--line);
  --ghost-fg:var(--ink-text);--ghost-bd:var(--line-2);
  --ghost-hv:var(--white);--ghost-hv-bg:rgba(249,250,250,.08)}
.drawer[data-open=true]{display:block}
.drawer a{display:block;padding:.85rem var(--gut);color:var(--ink-text);
  text-decoration:none;font-family:var(--display);font-weight:600;
  border-bottom:1px solid var(--line-dark)}

/* ---------------------------------------------------------------- hero */
/* ------------------------------------------------------ cinematic hero ---
   The photograph stops being a thumbnail beside the text and becomes the
   stage the text stands on. It bleeds the full width, the copy sits over it,
   and a scrim keeps the type legible without washing the image out.

   The scrim is measured, not eyeballed. Sampled against the homepage plate:
   the brightest photo pixel under the headline is 0.614 luminance, the two
   scrim layers composite to 0.866 alpha there, so the effective background
   lands at 0.086 and white type measures 7.73:1.

   It holds for any photograph, which is the point. Worst case is a pure
   white plate: 1.0 luminance behind 0.866 alpha still composites to 0.137
   and clears 5.6:1. No image the client uploads can break the hero. */
.hero{background:linear-gradient(180deg,#E7EFFA 0%,#F5F9FE 46%,#fff 100%);
  color:var(--text);position:relative;overflow:hidden;isolation:isolate}
/* The mascot is a cut-out with real alpha, so it stands ON the gradient
   rather than inside a card. A container would draw a box around thin air. */
.hero-media{position:relative;z-index:1;display:flex;justify-content:center;
  align-items:flex-end;overflow:visible;border-radius:0;box-shadow:none}
.hero-media img{width:auto;max-width:100%;max-height:520px;height:auto;
  aspect-ratio:auto;object-fit:contain;transform:none;
  filter:drop-shadow(0 18px 26px rgba(16,24,40,.16))}
@media (max-width:860px){.hero-media img{max-height:260px}}
/* Two scrims, not one: a vertical wash for the copy and a horizontal fade
   so the image survives on the side it is still visible. */
/* A soft blue bloom behind the figure, and nothing else. The two dark
   scrims that used to live here existed to keep white type legible over a
   full-bleed photograph; there is no photograph now, so they were laying a
   near-black wash across a light page for no reason. */
.hero::before{content:"";position:absolute;z-index:0;inset:0;
  pointer-events:none;
  background:radial-gradient(60% 70% at 72% 45%,rgba(2,115,255,.10),
    transparent 70%)}
/* Copy left, figure right. It was a single column from when the image was a
   full-bleed background, which left the mascot stacked underneath the copy
   and pushed the CTAs off the first screen. */
.hero-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,.95fr);
  gap:clamp(1.5rem,4vw,3rem);align-items:center;position:relative;z-index:1;
  padding:clamp(2.4rem,5vw,4rem) 0 clamp(2.4rem,5vw,4rem)}
.hero-grid-solo{grid-template-columns:1fr}
@media (max-width:860px){
  .hero-grid{grid-template-columns:1fr}
  /* On a phone the figure goes first and small -- it is decoration, and the
     headline plus the phone number should own the first screen. */
  .hero-media{order:-1}
}
/* The cap belongs on the headline, not the column: applied to .hero-copy it
   also squeezed the rating pill and the open-now badge into a 3-line stack. */
.hero-copy{max-width:min(100%,64ch)}
.hero h1{margin-bottom:.42em;max-width:16ch}
.hero-sub{font-size:clamp(1.05rem,1.5vw,1.22rem);color:var(--text);
  max-width:46ch;margin-bottom:2rem}
.hero-act{display:flex;flex-wrap:wrap;gap:.85rem;margin-bottom:1.7rem}
.hero-call{display:inline-flex;flex-direction:column;text-decoration:none;
  background:var(--white);border:1px solid var(--line-2);
  border-radius:var(--r-md);padding:.85rem 1.35rem;box-shadow:var(--shadow-1)}
.hero-call-top{font-family:var(--mono);font-size:.68rem;letter-spacing:.16em;
  text-transform:uppercase;color:var(--text-soft)}
.hero-call-num{font-family:var(--display);font-weight:700;font-size:1.5rem;
  color:var(--ink-text);letter-spacing:-.02em}
/* The copy column is capped at 20ch so the display size stays enormous.
   Below the two-column breakpoint that cap has to go or the words stack
   one per line. */
@media (max-width:820px){.hero-copy{max-width:none}
  .hero-sub{max-width:none}}

/* Open-now pill. Colour comes from JS deciding against the hours table. */
.hero-proof{display:flex;flex-wrap:wrap;align-items:center;gap:.8rem;
  margin-bottom:1.2rem}
.hero-proof .openpill{margin-bottom:0}
.openpill{display:inline-flex;align-items:center;gap:.5rem;
  font-family:var(--display);font-weight:700;font-size:.88rem;
  padding:.42rem .9rem;border-radius:var(--r-pill);
  background:#E4F7EF;color:#0A6B49;margin-bottom:1.1rem;
  border:1px solid #A7E8CB}
.openpill[data-open=false]{background:#FDECEA;color:#9B2C22;
  border-color:#F3C7C2}
.on-dark .openpill,.on-navy .openpill{background:rgba(14,124,86,.22);
  color:#7FE3B8;border-color:rgba(127,227,184,.3)}
.openpill i{width:8px;height:8px;border-radius:50%;background:currentColor;
  display:inline-block}


/* Every dark surface flips the same three tokens. The global heading colour
   (--ink-text) would otherwise paint near-black headings onto a navy band,
   and the eyebrow's --signal-dk is tuned for white grounds. Listing the
   surfaces once here is what stops this being rediscovered a fourth time --
   it has already cost a ghost button at 1.55:1, an eyebrow at 2.92:1 and a
   CTA heading at 1.73:1. ADD ANY NEW DARK BAND TO THIS SELECTOR. */
.on-dark,.on-navy,.cta,.statband,.offerband,.icta,.sociallink-lead,
.heatstrip,.finalcta,.hero-dark,.ocard-figure{
  --ink-text:#FFFFFF;
  --signal-dk:#9CC4FF;
  --text-soft:#A9BDD8;
}
.on-dark h1,.on-dark h2,.on-dark h3,.cta h2,.cta h3,.statband h2,
.offerband h2,.icta h2,.sociallink-lead h2,.heatstrip h2,.finalcta h2{
  color:#fff}

/* ------------------------------------------------- conversion sections -- */
.offerband{background:var(--ink);color:var(--text-invert);
  border-bottom:1px solid var(--line-dark);
  --ghost-fg:var(--text-invert);--ghost-bd:var(--line-dark);
  --ghost-hv:var(--white);--ghost-hv-bg:rgba(249,250,250,.08)}
.offerband-in{display:flex;flex-wrap:wrap;align-items:center;
  justify-content:space-between;gap:1.2rem;padding:1.1rem 0}
.offerband .eyebrow{color:var(--signal-dk);margin-bottom:.5rem}
.offer-chips{display:flex;flex-wrap:wrap;gap:.5rem .8rem}
.offer-chip{display:inline-flex;align-items:center;gap:.4rem;
  background:var(--ink-800);border:1px solid var(--line-dark);
  border-radius:var(--r-pill);padding:.45rem .9rem;font-size:.93rem}
/* --signal-lift, not --signal. The chip sits on the raised panel (#0E1628)
   rather than the canvas, and that ~7% lift drops #0273FF from 4.56:1 to
   4.21:1 -- under AA. Same hue, lifted to clear it at 6.06:1. */
.offer-chip strong{font-family:var(--display);font-weight:700;
  color:var(--signal-lift)}
.offerband-act{display:flex;flex-wrap:wrap;gap:.6rem}

/* How it works, as a connected stepper rather than three loose cards.
   The steps ARE a sequence -- call, window, quote -- so a line joining them
   is information, not decoration: it says these happen in order. The rule
   runs behind the nodes and stops at the last one, and it is drawn with a
   pseudo-element so it can never affect layout. */
.hiw-grid{list-style:none;margin:2.6rem 0 0;padding:0;display:grid;
  grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:1.6rem 1.2rem;
  position:relative}
.hiw{position:relative;padding:0;margin:0;background:none;border:0;
  box-shadow:none;overflow:visible}
.hiw-node{display:block;position:relative;height:46px;margin-bottom:1rem}
/* The connecting rule. Sits behind the node, drawn from the centre of this
   node to the centre of the next, and suppressed on the last item. */
.hiw-node::before{content:"";position:absolute;left:46px;right:-1.2rem;
  top:23px;height:2px;
  background:linear-gradient(90deg,rgba(2,115,255,.45),rgba(2,115,255,.12))}
.hiw:last-child .hiw-node::before{display:none}
@media (max-width:820px){.hiw-node::before{display:none}}
.hiw-dot{position:relative;display:grid;place-items:center;
  width:46px;height:46px;border-radius:14px;color:var(--white);
  background:linear-gradient(140deg,var(--signal),var(--signal-hi));
  box-shadow:0 6px 18px rgba(2,115,255,.34)}
.hiw-n{display:block;font-family:var(--mono);font-size:.7rem;
  letter-spacing:.18em;color:var(--signal-dk);margin-bottom:.4rem}
.hiw h3{margin-bottom:.35em}
.hiw p{color:var(--text-soft);margin:0}
@media (prefers-reduced-motion: no-preference){
  .hiw-dot{transition:transform .2s ease-out,box-shadow .2s ease-out}
  .hiw:hover .hiw-dot{transform:translateY(-2px);
    box-shadow:0 10px 26px rgba(2,115,255,.45)}
}
}

.price{background:var(--white);border:1px solid var(--line);
  border-top:4px solid var(--navy);border-radius:var(--r-md);padding:1.5rem;
  box-shadow:var(--shadow-1)}
.price-amt{font-family:var(--display);font-weight:700;
  font-size:clamp(1.7rem,3.5vw,2.2rem);color:var(--navy);line-height:1;
  margin-bottom:.5rem}
.price h3{margin-bottom:.35em}
.price p{color:var(--text-soft);margin:0}

@media (max-width:640px){
  .offerband-in{flex-direction:column;align-items:flex-start}
  .offerband-act .btn{flex:1}
}

/* --------------------------------------------------- proof / reviews --- */
.gicon{flex:none;display:block}
.stars{display:inline-flex;gap:1px;color:#F5A623;vertical-align:middle}
.stars svg{display:block}

.gbadge{display:inline-flex;align-items:center;gap:.7rem;text-decoration:none;
  background:var(--white);border:1px solid var(--line);
  border-radius:var(--r-pill);padding:.6rem 1.1rem;box-shadow:var(--shadow-1);
  color:var(--text);min-height:48px}
.gbadge:hover{border-color:var(--blue);box-shadow:var(--shadow-2)}
.gbadge-txt{display:flex;flex-direction:column;line-height:1.25}
.gbadge-txt strong{font-family:var(--display);font-weight:700;font-size:.98rem}
.gbadge-txt span{font-size:.82rem;color:var(--text-soft);
  display:inline-flex;align-items:center;gap:.35rem}
.on-dark .gbadge,.hero .gbadge{background:var(--ink-800);
  border-color:var(--line-dark);color:var(--text-invert)}
.on-dark .gbadge-txt span,.hero .gbadge-txt span{color:var(--text-invert-soft)}

/* The proof row. Google first: for a stranger deciding whether to call, the
   rating is the strongest signal on the page. */
.proof{background:var(--navy);color:var(--white)}
.proof-in{display:flex;flex-wrap:wrap;gap:.9rem 2rem;justify-content:center;
  padding:1rem 0;font-family:var(--display);font-weight:600;font-size:.95rem}
.proof span{display:inline-flex;align-items:center;gap:.5rem}
.proof strong{font-weight:700}
.proof .gicon{background:var(--white);border-radius:50%;padding:2px}

.reviews{margin-top:2rem}
.review{margin:0;background:var(--white);border:1px solid var(--line);
  border-radius:var(--r-md);padding:1.4rem;box-shadow:var(--shadow-1);
  display:flex;flex-direction:column;gap:.8rem}
.review-top{display:flex;align-items:center;gap:.6rem}
.review blockquote{margin:0;font-size:.99rem;line-height:1.6;flex:1}
.review figcaption{font-family:var(--display);font-weight:700;font-size:.94rem}
.review figcaption span{font-weight:400;color:var(--text-soft)}

/* --------------------------------------------------------- trust strip */
/* Navy band with white text, as the old site had it. */
.trust{background:var(--navy);color:var(--white)}
.trust-in{display:flex;flex-wrap:wrap;gap:1.2rem 2.4rem;justify-content:center;
  padding:1rem 0;font-family:var(--display);font-weight:600;font-size:.98rem}
.trust span{display:inline-flex;align-items:center;gap:.5rem}

/* --------------------------------------------------------------- cards */
.card{background:var(--white);border:1px solid var(--line);
  border-radius:var(--r-md);padding:1.5rem;box-shadow:var(--shadow-1);
  transition:transform .16s ease,box-shadow .16s ease}
.card:hover{transform:translateY(-3px);box-shadow:var(--shadow-2)}
@media (prefers-reduced-motion:reduce){.card{transition:none}
  .card:hover{transform:none}}
.card h3{margin-bottom:.45em}
.card p{color:var(--text-soft);margin-bottom:0}
.card-link{text-decoration:none;display:block;color:inherit}
.on-dark .card{background:var(--ink-800);border-color:var(--line-dark)}
.on-dark .card p{color:var(--text-invert-soft)}
.card-ico{width:44px;height:44px;border-radius:var(--r-sm);
  background:var(--signal-lt);color:var(--signal-dk);display:grid;
  place-items:center;margin-bottom:1rem}
.on-dark .card-ico{background:rgba(255,164,0,.18);color:var(--signal-lt)}

/* --------------------------------------------------------------- prose */
/* The nbsp normalisation in content.py fixes the cause; this catches any
   genuinely unbreakable token (a long URL, a part number) that would
   otherwise push the page sideways on a phone. */
.prose{max-width:72ch;overflow-wrap:anywhere}
/* A body image is an illustration, not a billboard. Uncapped, a tall portrait
   upload fills the viewport and pushes the next paragraph off-screen, which
   is how a page stops being readable. 420px keeps the image and the text that
   explains it in view together. object-fit means a tall crop loses its edges
   rather than its subject. */
.prose img{border-radius:var(--r-md);margin:1.5rem 0;
  max-height:420px;width:100%;object-fit:cover}
.prose figure{margin:1.5rem 0}
.prose li img{max-height:none;width:auto;display:inline-block;margin:0}
.prose h2{margin-top:2.2rem}
.prose h3{margin-top:1.6rem}
.prose a{color:var(--signal-dk);text-decoration:underline;
  text-underline-offset:2px}
.prose a:hover{color:var(--signal)}
.prose ul{padding-left:1.2em}
.prose table{width:100%;border-collapse:collapse;margin:1.5rem 0;
  display:block;overflow-x:auto}
.prose td,.prose th{border:1px solid var(--line);padding:.6rem .8rem;
  text-align:left}

/* ----------------------------------------------------------------- faq */
.faq{border-top:1px solid var(--line)}
.faq details{border-bottom:1px solid var(--line);padding:.2rem 0}
.faq summary{font-family:var(--display);font-weight:700;font-size:1.06rem;
  padding:1.05rem 2.2rem 1.05rem 0;cursor:pointer;position:relative;
  list-style:none}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{content:"";position:absolute;right:.4rem;top:1.4rem;
  width:9px;height:9px;border-right:2px solid var(--signal);
  border-bottom:2px solid var(--signal);transform:rotate(45deg);
  transition:transform .18s ease}
.faq details[open] summary::after{transform:rotate(-135deg)}
.faq-a{padding:0 2.2rem 1.1rem 0;color:var(--text-soft)}

/* ------------------------------------------------------------ CTA band */
.cta{background:var(--ink);color:var(--text-invert);text-align:center;
  --ghost-fg:var(--text-invert);--ghost-bd:var(--line-dark);
  --ghost-hv:var(--white);--ghost-hv-bg:rgba(249,250,250,.08)}
.cta h2{margin-bottom:.4em}
/* --text-soft on the ink ground measures 2.71:1. */
.cta .lede{margin:0 auto 1.6rem;color:var(--text-invert-soft)}
.cta-act{display:flex;flex-wrap:wrap;gap:.85rem;justify-content:center}

/* --------------------------------------------- progressive disclosure --- */
/* A folded section still holds every word in the DOM: <details> is indexed,
   revealed by browser find-in-page, and opened by a link to its anchor. The
   page gets shorter; the crawler sees what it always saw. */
/* Deferred rendering for folded bands. These are always below the fold by
   construction -- the first substantial section never folds -- so the
   browser can skip layout and paint until one scrolls near. contain-
   intrinsic-size supplies a placeholder height so the scrollbar does not
   jump as they resolve. NOT applied to open bands: on above-the-fold
   content it costs more than it saves. */
.sec-fold{padding:0;content-visibility:auto;contain-intrinsic-size:auto 96px}
.sec-fold + .sec-fold{padding-top:0}
.disclose{border-top:1px solid var(--line);margin:0}
.sec-fold:last-of-type .disclose{border-bottom:1px solid var(--line)}
.disclose-sum{display:flex;align-items:center;gap:1rem;cursor:pointer;
  padding:1.35rem 0;list-style:none;justify-content:space-between}
.disclose-sum::-webkit-details-marker{display:none}
.disclose-sum h2{margin:0;font-size:clamp(1.12rem,2.2vw,1.4rem);
  letter-spacing:-.01em;flex:1 1 auto;min-width:0}
.disclose-sum:hover h2{color:var(--signal-dk)}
.disclose-hint{flex:none;display:inline-flex;align-items:center;gap:.45rem;
  font-family:var(--body);font-size:.82rem;font-weight:600;
  letter-spacing:.02em;color:var(--signal-dk);white-space:nowrap}
/* The chevron is drawn, not an icon file -- one less request and it can
   inherit currentColor as the disclosure opens. */
.disclose-hint::after{content:"";width:8px;height:8px;flex:none;
  border-right:2px solid currentColor;border-bottom:2px solid currentColor;
  transform:rotate(45deg) translate(-1px,-1px);
  transition:transform .18s ease}
.disclose[open] .disclose-hint::after{transform:rotate(-135deg) translate(-2px,-2px)}
.disclose[open] .disclose-sum h2{color:var(--navy)}
.disclose-body{padding:0 0 1.9rem}
.disclose-sum:focus-visible{outline:2px solid var(--signal);outline-offset:3px}
@media (prefers-reduced-motion:reduce){.disclose-hint::after{transition:none}}

/* Inline CTA: punctuation between long runs of copy, not another band.
   Sits in the text column so it reads as part of the argument. */
.icta{display:flex;align-items:center;justify-content:space-between;
  gap:1.2rem;flex-wrap:wrap;margin:2.2rem 0;padding:1.05rem 1.3rem;
  background:var(--ink);color:var(--text-invert);border-radius:var(--r-md);
  border-left:4px solid var(--signal)}
.icta-txt{display:flex;flex-direction:column;line-height:1.3;min-width:0}
.icta-txt strong{font-family:var(--display);font-size:1.04rem;font-weight:700}
.icta-txt span{font-size:.9rem;color:var(--text-invert-soft)}
.icta-act{display:flex;gap:.6rem;flex-wrap:wrap}
/* The strip sits inside .prose, so `.prose a{color:signal-dk;underline}` wins
   on specificity and painted blue underlined text across an amber button.
   Restated here rather than fought with !important. */
/* Scoped to .icta, NOT .prose .icta. The first version only flipped the
   tokens when the strip sat inside prose, so the copy on /blog/ -- which is
   not in prose -- rendered .btn-ghost at 1.55:1 on the dark strip. Fourth
   instance of the same bug; the rule is that the DARK SURFACE owns the flip,
   never the context it happens to sit in. */
.icta{--ghost-fg:var(--text-invert);--ghost-bd:var(--hairline);
  --ghost-hv:var(--white);--ghost-hv-bg:rgba(26,123,255,.14)}
.icta .btn,.icta a{text-decoration:none}
.icta .btn-signal{color:#fff}
.icta .btn-ghost{color:var(--text-invert)}
.icta .btn{padding:.6rem 1rem;font-size:.95rem}
@media (max-width:600px){
  .icta{flex-direction:column;align-items:stretch}
  .icta-act{width:100%}
  .icta .btn{flex:1 1 auto;justify-content:center}
}

/* ------------------------------------------------------------- offices */
.office{background:var(--white);border:1px solid var(--line);
  border-radius:var(--r-md);padding:1.5rem}
.office h3{margin-bottom:.3em}
.office address{font-style:normal;color:var(--text-soft);margin-bottom:1rem}
.office-links{display:flex;gap:1rem;flex-wrap:wrap;font-weight:600;
  font-family:var(--display);font-size:.95rem}
.office-links a{color:var(--signal-dk)}

/* ---------------------------------------------------------------- toc -- */
.toc{background:var(--bone-2);border-bottom:1px solid var(--line)}
.toc .wrap{padding-top:1.3rem;padding-bottom:1.3rem}
.toc-lbl{font-family:var(--display);font-weight:700;font-size:.82rem;
  letter-spacing:.09em;text-transform:uppercase;color:var(--text-soft);
  margin:0 0 .7rem}
.toc ol{list-style:none;display:flex;flex-wrap:wrap;gap:.5rem .7rem;
  margin:0;padding:0;counter-reset:toc}
.toc li{counter-increment:toc}
.toc a{display:inline-flex;align-items:center;gap:.45rem;
  background:var(--white);border:1px solid var(--line);
  border-radius:var(--r-pill);padding:.5rem .95rem;font-size:.92rem;
  text-decoration:none;color:var(--text);min-height:40px}
.toc a::before{content:counter(toc,decimal-leading-zero);
  font-family:var(--display);font-weight:700;color:var(--signal-dk);
  font-size:.8rem}
.toc a:hover{border-color:var(--blue);color:var(--signal-dk)}

/* Anchored headings must not hide under the sticky header. */
.sec[id]{scroll-margin-top:86px}

/* ------------------------------------------------------ media in prose -- */
/* An uncapped image at prose width is most of a screen. Capping the height
   and gridding runs of them is what took a city page from 31,000px to
   something a person will actually scroll. */
.prose-img{width:100%;max-height:440px;object-fit:cover;
  border-radius:var(--r-md);margin:1.5rem 0}
.photos{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
  gap:1rem;margin:1.5rem 0}
.photos img{width:100%;aspect-ratio:4/3;object-fit:cover;
  border-radius:var(--r-md)}
.thumbs{display:flex;flex-wrap:wrap;gap:1.2rem;margin:1.4rem 0;
  align-items:flex-start}
.thumbs img{width:auto;height:auto;max-width:180px;max-height:180px;
  object-fit:contain;border-radius:var(--r-sm)}
.badges{display:flex;flex-wrap:wrap;align-items:center;gap:1.4rem 2rem;
  margin:1.4rem 0;padding:1.1rem 1.3rem;background:var(--white);
  border:1px solid var(--line);border-radius:var(--r-md)}
/* margin:0 is load-bearing. .prose img sets a 1.5rem block margin for body
   photographs; inherited here it added 96px of dead space inside a row of
   34px logos and left the card looking half-empty. */
.badges{justify-content:center}
.badges img{height:34px;width:auto;max-width:140px;object-fit:contain;
  opacity:.85;margin:0;max-height:34px}
.on-dark .badges{background:var(--ink-800);border-color:var(--line-dark)}

/* ------------------------------------------------ structured sections --- */
/* Centre the measure. Left-aligned in a 1160px wrap, a 629px column left
   531px of blank page down the right of every band -- the layout read as
   unfinished rather than as a deliberate narrow measure. */
.prose-wrap{max-width:var(--wrap);margin:0 auto;padding:0 var(--gut)}
.prose-wrap>.prose{max-width:74ch;margin-left:auto;margin-right:auto}
.sec+.sec>.prose-wrap>.prose h2:first-child,
.sec+.sec>.wrap>.prose h2:first-child{margin-top:0}

/* Short list items become tiles rather than a bullet run: a list of eight
   two-word items is a grid, and rendering it as prose doubles page height. */
.tiles{margin:1.4rem 0 0}
.tile{display:flex;align-items:flex-start;gap:.6rem;background:var(--white);
  border:1px solid var(--line);border-radius:var(--r-sm);padding:.85rem 1rem;
  font-size:.97rem}
.tile svg{color:var(--trust);flex:none;margin-top:.15rem}
.on-dark .tile{background:var(--ink-800);border-color:var(--line-dark)}
.on-bone .tile{background:var(--white)}

.steps{display:grid;gap:1rem;margin-top:1.6rem}
.step{display:flex;gap:1.1rem;align-items:flex-start;background:var(--white);
  border:1px solid var(--line);border-left:4px solid var(--blue);
  border-radius:var(--r-sm);padding:1.2rem 1.35rem}
.step-n{font-family:var(--display);font-weight:700;font-size:1.5rem;
  color:var(--signal-dk);line-height:1;flex:none;min-width:2.2rem}
.step h3{margin-bottom:.3em}
.step p{margin:0;color:var(--text-soft)}

/* ------------------------------------------------- gallery / offers ---- */
.shots{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));
  gap:1.25rem;margin-top:2rem}
.shot{margin:0;background:var(--white);border:1px solid var(--line);
  border-radius:var(--r-md);overflow:hidden;box-shadow:var(--shadow-1)}
.shot img{width:100%;aspect-ratio:4/3;object-fit:cover;display:block}
.shot figcaption{padding:.85rem 1rem;font-size:.94rem;color:var(--text-soft)}

.offer{background:var(--white);border:1px solid var(--line);
  border-radius:var(--r-md);padding:1.6rem;box-shadow:var(--shadow-1);
  border-top:4px solid var(--signal)}
.offer-amt{font-family:var(--display);font-weight:700;
  font-size:clamp(1.9rem,4vw,2.5rem);color:var(--signal-dk);line-height:1;
  margin-bottom:.5rem}
.offer h3{margin-bottom:.35em}
.offer p{color:var(--text-soft);margin:0}
.offer-fine{font-size:.9rem;color:var(--text-soft);margin-top:1.5rem}

.sociallinks{display:flex;flex-direction:column;gap:.65rem;margin-top:2rem}
.sociallink{display:flex;align-items:center;justify-content:space-between;
  gap:1rem;padding:1rem 1.25rem;background:var(--white);
  border:1px solid var(--line);border-radius:var(--r-md);
  text-decoration:none;font-family:var(--display);font-weight:600;
  min-height:56px;transition:border-color .15s ease,transform .15s ease}
.sociallink:hover{border-color:var(--signal);transform:translateX(2px)}
@media (prefers-reduced-motion:reduce){
  .sociallink{transition:none}.sociallink:hover{transform:none}}
.sociallink svg{color:var(--signal-dk);flex:none}
/* This one IS dark -- it is the call-us card, on --ink, sitting inside a
   light .sociallinks grid. So it joins the dark-surface contract
   individually rather than its container joining on its behalf: the
   nested span was inheriting --signal-dk and measuring 2.92:1. */
.sociallink-lead{background:var(--ink);color:var(--text-invert);
  --signal-dk:var(--signal-lift);
  --ghost-fg:var(--text-invert);--ghost-bd:var(--hairline);
  border-color:var(--ink)}
.sociallink-lead svg{color:var(--signal-lt)}

/* -------------------------------------------------------------- footer */
.ftr{background:var(--bone);color:var(--text);border-top:1px solid var(--line);
  padding:clamp(2.5rem,5vw,3.5rem) 0 0}
.ftr-grid{display:grid;grid-template-columns:1.4fr 1fr 1fr 1.2fr;gap:2rem}
/* Footer column labels. They were <h4> following the CTA band's <h2>, which
   skipped a level on all 123 pages -- the only structural heading fault on
   the site. Now <h3>; the visual size is set here so nothing changed to look
   at. In the machine voice, matching every other label on the page. */
.ftr h3{color:var(--ink-text);font-family:var(--mono);
  font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;
  font-weight:600;margin-bottom:1rem}
.ftr a{color:var(--text);text-decoration:none}
.ftr a:hover{color:var(--white);text-decoration:underline}
.ftr ul{list-style:none;padding:0;margin:0}
/* 44px is the accessible minimum for a tap target; these were 24px. */
.ftr li{margin:.15em 0}
.ftr li a{display:inline-block;padding:.55rem 0;min-height:44px;
  line-height:1.5}
.ftr-tel{display:inline-flex;align-items:center;min-height:44px;font-family:var(--display);font-weight:700;font-size:1.35rem;
  color:var(--white);text-decoration:none;display:inline-block;
  margin-bottom:.5rem}
.ftr-hours td{padding:.15rem .8rem .15rem 0;font-size:.94rem}
.ftr-hours td:last-child{color:var(--ink-text)}
.ftr-social{display:flex;gap:.7rem;margin-top:1rem;flex-wrap:wrap}
/* 44px, not 36. These are standalone icon links with no text label,
   so the target-size exemption for inline prose links does not apply. */
.ftr-social a{width:44px;height:44px;border-radius:50%;
  background:var(--ink-800);display:grid;place-items:center;
  border:1px solid var(--line-dark)}
.ftr-social a:hover{background:var(--signal);border-color:var(--signal)}
/* Used on a light section AND in the dark footer. --steel-300 is a
   dark-ground token: on --bone-2 it measures 2.16:1. */
.ftr-cities{font-size:.92rem;line-height:1.9;color:var(--text-soft)}
.ftr .ftr-cities,.on-dark .ftr-cities{color:var(--steel-300)}
/* The legal row is .ftr-btm, not .ftr-bot -- an earlier fix targeted a
   class that does not exist, so these stayed at 18px. */
.ftr-btm a{display:inline-flex;align-items:center;min-height:44px}
.ftr-btm{border-top:1px solid var(--line-dark);margin-top:2.5rem;
  padding:1.3rem 0;font-size:.88rem;display:flex;flex-wrap:wrap;gap:1rem;
  justify-content:space-between}

/* --------------------------------------------------- sticky mobile bar */
/* 820px is load-bearing: it is what the "sticky mobile call/book bar"
   conversion requirement means concretely, and it is independent of the
   980px nav breakpoint. */
/* A dark surface, so it MUST set all four ghost variables -- without them
   .btn-ghost inherits the light-surface defaults and renders #333 on #192033
   at 1.28:1. That is an invisible "Book" button on every phone, and it is
   invisible in a screenshot too: only getComputedStyle shows it. */
.mobile-bar{display:none;position:fixed;left:0;right:0;bottom:0;z-index:80;
  background:var(--ink);border-top:1px solid var(--line-dark);
  padding:.6rem;gap:.6rem;box-shadow:0 -6px 20px rgba(25,32,51,.28);
  --ghost-fg:var(--text-invert);--ghost-bd:var(--line-dark);
  --ghost-hv:var(--white);--ghost-hv-bg:rgba(249,250,250,.08)}
.mobile-bar .btn{flex:1;padding:.9rem .6rem}

/* --------------------------------------------------------- breadcrumbs */
.crumbs{font-size:.9rem;color:var(--text-soft);padding:1rem 0 0}
.crumbs ol{list-style:none;display:flex;flex-wrap:wrap;gap:.45rem;
  padding:0;margin:0}
.crumbs li::after{content:"/";margin-left:.45rem;color:var(--line)}
.crumbs li:last-child::after{content:""}
.crumbs a{color:var(--text-soft)}

/* --------------------------------------------------------- scroll reveal */
/* Transform only. This previously started at opacity:0 and waited for an
   IntersectionObserver to set data-reveal=in -- which meant that on any given
   page load, every card, office, review and city link below the fold computed
   to zero opacity. Measured on the Mesa page: 41 text-bearing elements
   invisible at once.

   Two things wrong with that. Googlebot renders with Chrome and does not
   scroll the full document, so content sitting at opacity 0 is content
   offered to the crawler as invisible. And if the JS ever fails to run --
   blocked, errored, slow -- the page keeps its content permanently hidden
   from everyone.

   Sliding 14px gives the same reveal. Nothing is ever invisible to a crawler,
   a screen reader, or a visitor who lands mid-page on a fragment link. */
[data-reveal]{transform:translateY(14px);transition:transform .5s ease}
[data-reveal=in]{transform:none}
@media (prefers-reduced-motion:reduce){
  [data-reveal]{transform:none;transition:none}}

/* -------------------------------------------------------- breakpoints */
@media (min-width:821px){.mobile-bar{display:none!important}}
@media (max-width:980px){
  .hdr-nav{display:none}
  .hdr-burger{display:block;margin-left:auto}
  .hdr-act .hdr-tel{display:flex}
}
@media (max-width:900px){
  .hero-grid{grid-template-columns:1fr;gap:1.8rem}
  .hero-media{order:-1}
  .ftr-grid{grid-template-columns:1fr 1fr}
}
@media (max-width:820px){
  .mobile-bar{display:flex}
  body{padding-bottom:74px}
}
@media (max-width:640px){
  .hdr-act .btn{display:none}
  .hero-act{flex-direction:column;align-items:stretch}
  .hero-act .btn{width:100%}
}
@media (max-width:520px){
  .ftr-grid{grid-template-columns:1fr}
  .hdr-tel-lbl{display:none}
}

/* =========================================================== landing v2 ===
   Conversion template. Every rule here supports one of the levers named in
   components.py -- immediacy, self-selection, loss aversion, proof, anchoring
   -- rather than decorating the page. */

/* Status bar. Thin, dark, above the hero: the three facts a person with a
   broken door wants before they read anything. */
.statusbar{background:var(--ink);color:var(--text-invert);
  border-bottom:1px solid var(--hairline);font-size:.86rem}
.statusbar-in{display:flex;align-items:center;gap:.85rem;flex-wrap:wrap;
  padding:.6rem var(--gut);min-height:44px}
.st-item{display:inline-flex;align-items:center;gap:.45rem;
  font-family:var(--mono);font-size:.76rem;letter-spacing:.04em;
  color:#C7D2E4;white-space:nowrap}
.st-item strong{color:var(--white);font-weight:600}
.st-dot{width:7px;height:7px;border-radius:50%;background:var(--steel-500);
  flex:none}
.st-open[data-state=open] .st-dot{background:var(--trust);
  box-shadow:0 0 8px var(--trust)}
.st-open[data-state=closed] .st-dot{background:var(--alert)}
.st-sep{width:1px;height:14px;background:var(--ink-700);flex:none}
.st-call{margin-left:auto;display:inline-flex;align-items:center;
  min-height:44px;font-family:var(--display);font-weight:700;
  font-size:1rem;color:var(--white);text-decoration:none;white-space:nowrap;
  padding:.3rem .2rem}
.st-call:hover{color:var(--signal-lift)}
@media (max-width:700px){
  .st-eta,.st-sep{display:none}
  .st-call{margin-left:auto}
}

/* Symptom picker -- the first small yes. Chips are big touch targets because
   this is the one thing we most want a phone user to press. */
.symp-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));
  gap:.85rem;margin-top:1.9rem}
.symp{display:flex;flex-direction:column;gap:.2rem;text-decoration:none;
  background:var(--white);border:1px solid var(--line);
  border-radius:var(--r-md);padding:1.05rem 1.15rem;min-height:44px;
  transition:border-color .16s ease,transform .16s ease,box-shadow .16s ease}
.symp strong{font-family:var(--display);font-size:1.02rem;font-weight:700;
  color:var(--navy)}
.symp span{font-size:.86rem;color:var(--text-soft)}
.symp:hover{border-color:var(--signal);transform:translateY(-2px);
  box-shadow:0 0 0 1px var(--signal),0 10px 24px rgba(2,115,255,.14)}
.symp:focus-visible{outline:3px solid var(--signal);outline-offset:2px}
@media (prefers-reduced-motion:reduce){.symp{transition:none}
  .symp:hover{transform:none}}

/* Heat strip. The live reading is the hook; the paragraph is the reason. */
/* Any band on the dark canvas must also flip the ghost-button tokens. Without
   them .btn-ghost keeps its light-ground colours and renders --text on --ink:
   1.55:1, a button you can only find by hovering. Every other dark surface on
   the site does this; these two were new and did not. */
.heatstrip,.finalcta{--ghost-fg:var(--text-invert);--ghost-bd:var(--hairline);
  --ghost-hv:var(--white);--ghost-hv-bg:rgba(26,123,255,.14)}
.heatstrip{background:var(--ink);color:var(--text-invert);position:relative;
  overflow:hidden}
.heatstrip::before{content:"";position:absolute;width:38rem;height:38rem;
  right:-12rem;top:-16rem;border-radius:50%;pointer-events:none;
  background:radial-gradient(circle,rgba(255,138,0,.16),transparent 62%)}
.heat-in{display:grid;grid-template-columns:minmax(0,15rem) minmax(0,1fr);
  gap:clamp(1.5rem,4vw,3.5rem);align-items:start;position:relative}
.heat-read{border:1px solid var(--hairline);border-radius:var(--r-lg);
  padding:1.4rem 1.5rem;background:rgba(14,22,40,.6);
  display:flex;flex-direction:column;gap:.15rem}
.heat-label{font-family:var(--mono);font-size:.7rem;letter-spacing:.18em;
  text-transform:uppercase;color:var(--steel-300)}
.heat-temp{font-family:var(--display);font-weight:700;
  font-size:clamp(3rem,7vw,4.4rem);line-height:1;letter-spacing:-.04em;
  color:var(--white);font-variant-numeric:tabular-nums}
.heat-sub{font-size:.84rem;color:var(--steel-300)}
.heat-copy h2{color:var(--white)}
.heat-copy p{color:#CBD6E6}
.heat-act{display:flex;flex-wrap:wrap;gap:.7rem;margin-top:1.4rem}
@media (max-width:820px){.heat-in{grid-template-columns:1fr}}

/* Proof. Rating and licence sit level with the heading, so credibility is
   read at the same moment as the claim rather than two screens later. */
.pf-head{display:flex;flex-wrap:wrap;gap:1.5rem;align-items:flex-end;
  justify-content:space-between}
.pf-badges{display:flex;flex-wrap:wrap;gap:.6rem}
.pf-chip{display:flex;flex-direction:column;gap:.1rem;background:var(--white);
  border:1px solid var(--line);border-radius:var(--r-md);
  padding:.6rem .95rem;font-size:.78rem;color:var(--text-soft)}
.pf-chip strong{font-family:var(--display);font-size:.98rem;
  color:var(--navy);font-weight:700}
.pf-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));
  gap:1rem;margin-top:2rem}
.pf-card{margin:0;background:var(--white);border:1px solid var(--line);
  border-radius:var(--r-md);padding:1.3rem 1.35rem;box-shadow:var(--shadow-1)}
.pf-card blockquote{margin:.7rem 0 1rem;font-size:.98rem;color:var(--text)}
.pf-card figcaption{font-family:var(--display);font-weight:700;
  font-size:.92rem;color:var(--navy);display:flex;flex-direction:column}
.pf-card figcaption span{font-family:var(--body);font-weight:400;
  font-size:.78rem;color:var(--text-soft)}
.pf-stars{color:#F5A623}

/* Price frame. Tabular figures so the numbers align down the column -- a
   ragged money column reads as guesswork. */
.pr-table{margin-top:1.9rem;border-top:1px solid var(--line)}
.pr-row{display:grid;grid-template-columns:minmax(0,1fr) auto;
  gap:.4rem 1.5rem;padding:1.05rem 0;border-bottom:1px solid var(--line);
  align-items:baseline}
.pr-what{font-family:var(--display);font-weight:700;font-size:1.05rem;
  color:var(--navy)}
.pr-num{font-family:var(--display);font-weight:700;font-size:1.15rem;
  color:var(--signal-dk);font-variant-numeric:tabular-nums;text-align:right;
  white-space:nowrap}
.pr-note{grid-column:1/-1;font-size:.9rem;color:var(--text-soft);margin:0}

/* Local block */
.local-in{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(0,1fr);
  gap:clamp(1.5rem,4vw,3rem);align-items:start}
.local-card{background:var(--bone-2);border:1px solid var(--line);
  border-radius:var(--r-lg);padding:1.5rem}
.local-card h3{color:var(--navy)}
.local-card p{font-size:.92rem;color:var(--text-soft)}
.area-chips{display:flex;flex-wrap:wrap;gap:.45rem;margin-top:1.3rem}
.area-chip{font-family:var(--mono);font-size:.74rem;letter-spacing:.04em;
  background:var(--signal-lt);color:var(--navy);border-radius:var(--r-pill);
  padding:.35rem .75rem}
@media (max-width:820px){.local-in{grid-template-columns:1fr}}

/* Final CTA */
.finalcta{background:var(--ink);color:var(--text-invert)}
.finalcta-in{display:flex;flex-wrap:wrap;gap:1.8rem;align-items:center;
  justify-content:space-between}
.finalcta h2{color:var(--white);margin-bottom:.3em}
.finalcta .lede{color:#CBD6E6;max-width:46ch}
.finalcta-act{display:flex;flex-wrap:wrap;gap:.7rem}

/* Archive fold keeps the original copy in the document without it setting
   the length of the page. */
.archive .disclose{border-top:1px solid var(--line)}
.archive .disclose-sum h2{font-size:clamp(1.05rem,2vw,1.25rem)}

/* ================================================================ motion ===
   Every rule below is transform/opacity only -- the two properties the
   compositor can animate without touching layout or paint. Nothing here can
   cause a layout shift, which matters because CLS is a ranking factor and a
   reveal that pushes the page down as it arrives fails it.

   prefers-reduced-motion is honoured by construction: the whole block is
   inside a no-preference query, so reduced-motion users get the finished
   state immediately rather than a disabled animation. */

/* Free-estimate cards */
.est-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
  gap:1rem;margin-top:2.2rem}
.est{position:relative;background:var(--white);border:1px solid var(--line);
  border-radius:var(--r-md);padding:2.4rem 1.4rem 1.4rem;overflow:hidden;
  box-shadow:var(--shadow-1);isolation:isolate}
.est-n{position:absolute;top:-.9rem;right:.5rem;z-index:-1;
  font-family:var(--display);font-weight:700;font-size:5.5rem;line-height:1;
  letter-spacing:-.05em;color:transparent;
  -webkit-text-stroke:1.5px rgba(2,115,255,.34)}
@supports not (-webkit-text-stroke:1px red){.est-n{color:rgba(2,115,255,.12)}}
.est-tag{display:inline-block;font-family:var(--mono);font-size:.7rem;
  letter-spacing:.14em;text-transform:uppercase;color:var(--signal-dk);
  background:var(--signal-lt);border-radius:var(--r-pill);
  padding:.25rem .6rem;margin-bottom:.7rem}
.est h3{margin-bottom:.3em}
.est p{color:var(--text-soft);margin:0;font-size:.95rem}

/* ---- scroll-driven reveal, no JavaScript --------------------------------
   animation-timeline: view() lets the browser drive this off the element's
   own position in the scrollport. No IntersectionObserver, no scroll
   listener, nothing on the main thread. It is wrapped in @supports so
   browsers without it simply render the finished state -- the content is
   never hidden by a feature that did not load, which is the failure mode
   that takes a page's text away from a crawler. */
@media (prefers-reduced-motion: no-preference){
  @supports (animation-timeline: view()){
    /* Transform only -- NOT opacity. The first version faded from opacity:0,
       which left all 27 animated elements on this page invisible until they
       were scrolled to. Googlebot renders with Chrome and snapshots without
       scrolling the whole document, so that is content handed to the crawler
       at zero opacity: an SEO regression introduced in the name of a fade.
       Sliding 18px reads as the same reveal and is never invisible to
       anything -- crawler, screen reader, or a user who lands mid-page from
       a fragment link. */
    @keyframes rise{
      from{transform:translateY(18px)}
      to{transform:none}
    }
    .est,.symp,.pf-card,.hiw,.area-chip,.pr-row{
      animation:rise auto both;
      animation-timeline:view();
      animation-range:entry 5% cover 26%;
    }
  }
}

/* ---- micro-interactions -------------------------------------------------
   Short, ease-out, and on transform only. The 1px press is what makes a
   button feel like a button rather than a coloured rectangle. */
.btn{transition:background-color .16s ease-out,color .16s ease-out,
  box-shadow .16s ease-out,transform .12s ease-out}
.btn:active{transform:translateY(1px)}
.est,.pf-card,.hiw{transition:transform .18s ease-out,box-shadow .18s ease-out,
  border-color .18s ease-out}
.est:hover,.pf-card:hover,.hiw:hover{transform:translateY(-3px);
  border-color:var(--signal);box-shadow:0 12px 30px rgba(2,115,255,.13)}

/* The live temperature counts up to its reading rather than snapping. Only
   the digits change -- tabular figures hold the width, so a 3-digit reading
   arriving after a 2-digit one cannot reflow the card. */
.heat-temp{transition:opacity .3s ease-out}
.heat-temp[data-counting]{opacity:.55}

/* Status bar dot: a slow pulse while open, nothing at all while closed. A
   pulsing dot on a closed shop is a lie told in motion. */
@media (prefers-reduced-motion: no-preference){
  @keyframes livepulse{
    0%,100%{box-shadow:0 0 0 0 rgba(18,185,129,.55)}
    70%{box-shadow:0 0 0 7px rgba(18,185,129,0)}
  }
  .st-open[data-state=open] .st-dot{animation:livepulse 2.4s ease-out infinite}
}

/* The hero photo drifts a little as the page scrolls. Small on purpose --
   large parallax is the single most template-looking effect on the web. */
@media (prefers-reduced-motion: no-preference){
  @supports (animation-timeline: view()){
    @keyframes heroDrift{to{transform:scale(1.04) translateY(4%)}}
    .hero-media img{animation:heroDrift auto linear;
      animation-timeline:view();animation-range:cover;}
  }
}

@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{animation-duration:.01ms !important;
    animation-iteration-count:1 !important;transition-duration:.01ms !important}
}

/* ========================================================== background ====
   "Patches" on the dark bands: a faint engineering grid, a blue bloom, and a
   grain layer to kill the banding a large gradient shows on cheap panels.
   All three are painted, not loaded -- no image request, and each is a
   pseudo-element so nothing here can affect layout. */
.patterned{position:relative;isolation:isolate;overflow:hidden}
.patterned::before{content:"";position:absolute;inset:0;z-index:-2;
  pointer-events:none;opacity:.5;
  background-image:
    linear-gradient(rgba(26,123,255,.10) 1px,transparent 1px),
    linear-gradient(90deg,rgba(26,123,255,.10) 1px,transparent 1px);
  background-size:64px 64px;
  /* fade the grid out toward the edges so it reads as texture, not a table */
  -webkit-mask-image:radial-gradient(ellipse 80% 70% at 50% 40%,#000,transparent);
  mask-image:radial-gradient(ellipse 80% 70% at 50% 40%,#000,transparent)}
.patterned::after{content:"";position:absolute;inset:0;z-index:-1;
  pointer-events:none;opacity:.5;mix-blend-mode:overlay;
  background-image:radial-gradient(circle at 18% 22%,
    rgba(2,115,255,.30),transparent 45%),
    radial-gradient(circle at 82% 12%,rgba(26,123,255,.22),transparent 42%)}

/* ============================================================= counters ===
   The reference page has this block and every figure on it reads "0 +" --
   the animation never fires, so the proof it is meant to carry is missing.
   These count for real, and every number is one that survives a check. */
.statband{background:var(--ink);color:var(--text-invert)}
.stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));
  gap:1rem;margin-top:1.8rem}
.stat{display:flex;flex-direction:column;align-items:flex-start;gap:.3rem;
  padding:1.35rem 1.3rem;border:1px solid var(--hairline);
  border-radius:var(--r-md);background:rgba(14,22,40,.55);
  backdrop-filter:blur(6px)}
.stat-ico{color:var(--signal-lift);display:inline-flex}
.stat-n{font-family:var(--display);font-weight:700;
  font-size:clamp(2.1rem,4.4vw,2.9rem);line-height:1;letter-spacing:-.035em;
  color:var(--white);
  /* tabular figures hold the width while the number counts, so a digit
     changing cannot nudge the label underneath it */
  font-variant-numeric:tabular-nums}
.stat-lbl{font-family:var(--mono);font-size:.72rem;letter-spacing:.13em;
  text-transform:uppercase;color:var(--steel-300)}

/* ============================================================== marquee ===
   Looping motion, which normally has to justify itself. Here it does: the row
   is wider than the screen and this is how you show that without a scrollbar.
   Duplicated in the markup so the loop is seamless; paused on hover and
   removed entirely under reduced-motion, where it becomes a plain wrapped
   row rather than a frozen strip with half its logos off-screen. */
.marquee-sec{background:var(--bone-2);border-block:1px solid var(--line)}
.marquee-sub{color:var(--text-soft);max-width:52ch;margin:.2rem 0 0}
.marquee{overflow:hidden;margin-top:1.4rem;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent)}
.marquee-track{display:flex;width:max-content}
.marquee-row{display:flex;align-items:stretch;gap:.9rem;padding-right:.9rem}
/* Each brand in an identical card. The logos are wildly inconsistent -- some
   SVGs carry a solid white plate, some are wordmarks three times wider than
   the square badges beside them. A uniform card absorbs the plate and gives
   every brand the same optical weight, which is what makes the row read as a
   set rather than as a folder of files. */
.pcard{display:grid;place-items:center;width:158px;height:78px;flex:none;
  background:var(--white);border:1px solid var(--line);
  border-radius:var(--r-md);box-shadow:var(--shadow-1);
  transition:border-color .18s ease-out,box-shadow .18s ease-out}
.pcard img{height:30px;width:auto;max-width:112px;object-fit:contain;margin:0;
  opacity:.72;filter:grayscale(1);
  transition:opacity .18s ease-out,filter .18s ease-out}
/* Wordmark, used when a brand has no logo file. Set in the display face at a
   fixed optical size so every brand carries identical weight -- which is the
   thing seven mismatched SVGs could never do. */
.pmark{font-family:var(--display);font-weight:700;font-size:1.02rem;
  letter-spacing:-.015em;color:var(--navy);text-align:center;line-height:1.15;
  padding:0 .6rem;transition:color .18s ease-out}
.pcard:hover{border-color:var(--signal);box-shadow:0 8px 22px rgba(2,115,255,.14)}
.pcard:hover img{opacity:1;filter:none}
.pcard:hover .pmark{color:var(--signal-dk)}
@media (prefers-reduced-motion: no-preference){
  @keyframes marq{to{transform:translateX(-50%)}}
  .marquee-track{animation:marq 46s linear infinite}
  .marquee:hover .marquee-track{animation-play-state:paused}
}
@media (prefers-reduced-motion: reduce){
  .marquee{overflow:visible;-webkit-mask-image:none;mask-image:none}
  .marquee-track{width:auto;flex-wrap:wrap}
  .marquee-row[aria-hidden=true]{display:none}
}

/* ======================================================== spot icons ======
   Warmth without emoji. Each chip gets a tinted plate behind a brand-coloured
   glyph -- the visual lift emoji would give, but themeable, identical on
   every platform, and able to take --signal. */
.symp{position:relative;padding-left:3.6rem}
.symp::before{content:"";position:absolute;left:1.1rem;top:1.15rem;
  width:26px;height:26px;border-radius:8px;
  background:linear-gradient(135deg,var(--signal),var(--signal-hi));
  box-shadow:0 4px 12px rgba(2,115,255,.32)}
.symp::after{content:"";position:absolute;left:1.1rem;top:1.15rem;
  width:26px;height:26px;
  background:currentColor;color:var(--white);
  -webkit-mask:center/13px no-repeat
    url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21 4a5.5 5.5 0 01-7.3 7.3L5 20a2 2 0 01-3-3l8.7-8.7A5.5 5.5 0 0118 1z" fill="none" stroke="white" stroke-width="2.4" stroke-linejoin="round"/></svg>');
  mask:center/13px no-repeat
    url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21 4a5.5 5.5 0 01-7.3 7.3L5 20a2 2 0 01-3-3l8.7-8.7A5.5 5.5 0 0118 1z" fill="none" stroke="white" stroke-width="2.4" stroke-linejoin="round"/></svg>')}
/* ===================================================== motion, v2 ========
   Ideas borrowed from the reference snippet, not its colours: a live dot
   that actually pulses, cards that answer the cursor, and an accordion
   marker that rotates rather than swapping glyphs. All transform/opacity,
   so none of it can shift layout. */
@media (prefers-reduced-motion:no-preference){
  @keyframes tgd-pulse{0%,100%{opacity:1;transform:scale(1)}
    50%{opacity:.35;transform:scale(.7)}}
  .st-open[data-state=open] .st-dot,.openpill i{
    animation:tgd-pulse 1.6s ease-in-out infinite}

  /* cards answer the cursor: a lift plus the accent arriving on the border */
  .card,.hiw,.est,.pf-card,.symp,.pcard,.gal figure{
    transition:transform .2s cubic-bezier(.16,1,.3,1),
      border-color .2s ease-out,box-shadow .2s ease-out}
  .card:hover,.hiw:hover,.est:hover,.pf-card:hover,.symp:hover,.gal figure:hover{
    transform:translateY(-4px);border-color:var(--signal-accent);
    box-shadow:0 14px 32px -14px rgba(22,63,126,.4)}

  /* the accordion marker turns instead of being replaced */
  .faq summary::after,.disclose-hint::after{transition:transform .22s ease-out}

  /* buttons take a real press */
  .btn{transition:background-color .16s ease-out,color .16s ease-out,
    box-shadow .18s ease-out,transform .12s ease-out}
  .btn:hover{transform:translateY(-1px)}
  .btn:active{transform:translateY(1px)}

  /* the hero figure settles in on load -- once, not on every scroll */
  @keyframes tgd-rise{from{opacity:0;transform:translateY(14px)}
    to{opacity:1;transform:none}}
  .hero-media img{animation:tgd-rise .5s cubic-bezier(.16,1,.3,1) both}
}

/* NAP lines recovered from the archive copy. Were <a role="button"> with no
   href -- a control nothing could activate. Now plain text, styled to match
   the links they sit beside so the row still reads as a set. */
.arch-nap{display:inline-block;font-family:var(--display);font-weight:600;
  font-size:.94rem;color:var(--text);padding:.4rem .1rem;margin-right:1rem}
.arch-nap small{font-family:var(--body);font-weight:400;color:var(--text-soft)}
.arch-icon{display:inline-flex;align-items:center;justify-content:center;
  width:2.2rem;height:2.2rem;color:var(--text-soft)}

/* ============================================ offer card + why cards ====
   Structures taken from the reference the owner supplied; colour, type and
   weights are ours. Space Grotesk stops at 700, so the reference's 900s are
   700 here -- asking for 900 gets a synthesised faux-bold. */
.ocard{padding:clamp(3rem,7vw,4.6rem) 0}
.ocard-head{text-align:center;margin-bottom:2.2rem}
.ocard-badge,.why-badge{display:inline-flex;align-items:center;gap:.5rem;
  border:1px solid var(--signal-accent);border-radius:40px;padding:.38rem .9rem;
  font-family:var(--mono);font-size:.7rem;font-weight:700;text-transform:uppercase;
  letter-spacing:.08em;color:var(--signal-dk);margin-bottom:1rem}
.ocard-badge i,.why-badge i{width:6px;height:6px;border-radius:50%;
  background:var(--signal-accent);flex:none}
.ocard-title,.why-title{font-family:var(--display);font-weight:700;
  font-size:clamp(1.7rem,3.6vw,2.4rem);line-height:1.14;letter-spacing:-.02em;
  color:var(--ink-text);margin:0 0 .6rem}
.ocard-sub,.why-sub{font-size:1.02rem;line-height:1.6;max-width:38rem;
  margin:0 auto;color:var(--text-soft)}
.ocard-card{display:grid;grid-template-columns:minmax(0,240px) minmax(0,1fr);
  border:1px solid var(--line);border-radius:var(--r-lg);overflow:hidden;
  background:var(--white);box-shadow:var(--shadow-1)}
.ocard-figure{position:relative;background:var(--ink);color:var(--white);
  padding:2.4rem 1.4rem;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:.35rem;text-align:center}
.ocard-figure::after{content:"";position:absolute;width:150px;height:150px;
  border-radius:50%;background:radial-gradient(circle,rgba(2,115,255,.34),transparent 70%);
  pointer-events:none}
.ocard-figure-lbl{position:relative;font-family:var(--mono);font-size:.72rem;
  text-transform:uppercase;letter-spacing:.16em;color:var(--signal-lift)}
.ocard-figure-big{position:relative;font-family:var(--display);font-weight:700;
  font-size:clamp(2.8rem,6vw,3.7rem);line-height:1;letter-spacing:-.04em;
  color:var(--white)}
.ocard-figure-note{position:relative;font-size:.82rem;line-height:1.45;
  color:var(--steel-300)}
.ocard-body{padding:clamp(1.4rem,3vw,2.2rem)}
.ocard-body-title{font-family:var(--display);font-weight:700;font-size:1.22rem;
  color:var(--ink-text);margin:0 0 1.2rem}
.ocard-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
  gap:.8rem;margin-bottom:1.3rem}
.ocard-item{display:flex;gap:.75rem;align-items:flex-start;
  border:1px solid var(--line);border-radius:var(--r-sm);padding:.85rem .8rem;
  background:var(--bone)}
.ocard-num{flex:none;width:28px;height:28px;border-radius:50%;
  display:inline-flex;align-items:center;justify-content:center;
  border:1px solid var(--signal-accent);font-family:var(--mono);
  font-size:.72rem;font-weight:700;color:var(--signal-dk);background:var(--white)}
.ocard-item h4{font-family:var(--display);font-weight:700;font-size:.9rem;
  color:var(--ink-text);margin:0 0 .18rem}
.ocard-item p{font-size:.83rem;line-height:1.5;color:var(--text-soft);margin:0}
.ocard-note{border-left:3px solid var(--signal-accent);border-radius:0 8px 8px 0;
  background:var(--bone);padding:.8rem .95rem;font-size:.88rem;line-height:1.65;
  color:var(--text);margin:0 0 1.3rem}
.ocard-note strong{color:var(--ink-text);font-weight:700}
.ocard-btns{display:flex;gap:.7rem;flex-wrap:wrap}
.why{padding:clamp(3rem,7vw,4.6rem) 0}
.why-head{text-align:center;margin-bottom:2.4rem}
.why-cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:1rem;margin-bottom:3rem}
.why-card{border:1px solid var(--line);border-radius:var(--r-md);
  padding:1.5rem 1.2rem;text-align:center;background:var(--white)}
.why-card-ico,.why-ben-ico{display:inline-flex;align-items:center;
  justify-content:center;width:2.3rem;height:2.3rem;border-radius:50%;
  background:var(--bone);color:var(--signal-dk);flex:none}
.why-card-ico{margin-bottom:.75rem}
.why-card h3{font-family:var(--display);font-weight:700;font-size:1rem;
  line-height:1.3;color:var(--ink-text);margin:0 0 .4rem}
.why-card p{font-size:.86rem;line-height:1.6;color:var(--text-soft);margin:0}
.why-ben-title{font-family:var(--display);font-weight:700;
  font-size:clamp(1.35rem,2.6vw,1.7rem);text-align:center;color:var(--ink-text);
  margin:0 0 1.3rem}
.why-bens{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.85rem}
.why-ben{display:flex;gap:.85rem;align-items:flex-start;
  border:1px solid var(--line);border-left:3px solid var(--signal-accent);
  border-radius:0 10px 10px 0;padding:1rem 1.1rem;background:var(--white)}
.why-ben h4{font-family:var(--display);font-weight:700;font-size:.94rem;
  color:var(--ink-text);margin:0 0 .2rem}
.why-ben p{font-size:.85rem;line-height:1.55;color:var(--text-soft);margin:0}
@media (max-width:860px){
  .ocard-card{grid-template-columns:minmax(0,1fr)}
  .ocard-grid,.why-cards,.why-bens{grid-template-columns:minmax(0,1fr)}
  .ocard-btns{flex-direction:column}
  .ocard-btns .btn{width:100%;justify-content:center}
}
@media (prefers-reduced-motion:no-preference){
  .ocard-badge i,.why-badge i{animation:tgd-pulse 1.5s ease-in-out infinite}
  .why-card,.why-ben,.ocard-item{transition:transform .2s cubic-bezier(.16,1,.3,1),
    border-color .2s ease-out,box-shadow .2s ease-out}
  .why-card:hover,.why-ben:hover,.ocard-item:hover{transform:translateY(-3px);
    border-color:var(--signal-accent);box-shadow:0 12px 26px -14px rgba(22,63,126,.38)}
}

/* Four classes that shipped in markup with no rule at all -- found by
   check_classes.sh the first time it was ever run. `.st-weather` is the live
   temperature chip: it has been rendering unstyled on every city page. */
.st-weather{display:inline-flex;align-items:center;gap:.35rem;
  font-family:var(--mono);font-size:.78rem;letter-spacing:.04em}
.st-weather[hidden]{display:none}
.st-sep-w{opacity:.45}
.st-sep-w[hidden]{display:none}
.offerband-txt{flex:1 1 22rem;min-width:0}
.p1{margin:0 0 .8rem}

/* Related-services block. One editorial link set per page, chosen from what
   the page actually mentions -- see seo_links.py for why it is a block and
   not 707 inline links. */
.rel{padding:clamp(2.2rem,5vw,3.2rem) 0}
.rel-h{font-family:var(--display);font-weight:700;
  font-size:clamp(1.3rem,2.6vw,1.7rem);color:var(--ink-text);margin:0 0 1rem}
.rel-list{list-style:none;margin:0;padding:0;display:grid;
  grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:.7rem}
.rel-list a{display:block;border:1px solid var(--line);
  border-left:3px solid var(--signal-accent);border-radius:0 10px 10px 0;
  padding:.85rem 1rem;background:var(--white);text-decoration:none}
.rel-t{display:block;font-family:var(--display);font-weight:700;
  font-size:.95rem;color:var(--signal-dk)}
.rel-d{display:block;font-size:.84rem;line-height:1.5;color:var(--text-soft);
  margin-top:.15rem}
@media (prefers-reduced-motion:no-preference){
  .rel-list a{transition:transform .18s cubic-bezier(.16,1,.3,1),
    border-color .18s ease-out,box-shadow .18s ease-out}
  .rel-list a:hover{transform:translateY(-3px);border-color:var(--signal-accent);
    box-shadow:0 12px 24px -14px rgba(22,63,126,.36)}
}

/* The tint the panels sit on. Without it they are white cards on a white
   page, which is invisible -- the whole point of the panel treatment is the
   contrast between card and ground. This rule was written, verified on
   staging, and then lost resolving a stash conflict, so the panels shipped
   without it. That is why production did not look like the staging build the
   owner approved. */
body{background:#F1F5FA}

/* ------------------------------------------------------------ booking */
.form-grid{display:grid;grid-template-columns:1fr 1.05fr;gap:2.5rem;
  align-items:start}
.form-intro .lede{margin-bottom:1.2rem}
.form-callout{font-family:var(--display);font-weight:600}
.form-callout a{color:var(--signal-dk)}
.form-points{list-style:none;padding:0;margin:1.4rem 0 0}
.form-points li{position:relative;padding-left:1.7rem;margin:.6em 0;
  color:var(--text-soft)}
.form-points li::before{content:"";position:absolute;left:0;top:.55em;
  width:9px;height:5px;border-left:2px solid var(--trust);
  border-bottom:2px solid var(--trust);transform:rotate(-45deg)}
.form-card{position:relative;background:var(--white);
  border-radius:var(--r-lg);padding:1.9rem;
  box-shadow:0 1px 2px rgba(7,12,24,.05),0 18px 46px -18px rgba(7,12,24,.22)}
/* A lit hairline rather than a flat 1px grey border. The card is the thing
   we most want to feel considered, and a border that catches the accent at
   the top is the cheapest way to say "this is the important panel" without
   adding weight anywhere else. */
.form-card::before{content:"";position:absolute;inset:0;border-radius:inherit;
  padding:1px;pointer-events:none;
  background:linear-gradient(160deg,rgba(2,115,255,.55),var(--line) 38%,
    var(--line));
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;mask-composite:exclude}
.f-row{margin-bottom:1.15rem}
.f-row label,.f-urgency legend,.f-lbl{display:block;font-family:var(--display);
  font-weight:600;font-size:.9rem;margin-bottom:.45rem;color:var(--navy)}
.f-opt{font-weight:400;color:var(--text-soft)}
/* Inputs sit on a tinted ground rather than white-on-white, so the field is
   legible as a field before it is focused. 48px min-height clears the touch
   target minimum with room, and 16px font stops iOS zooming the page on
   focus -- the single most common reason a mobile form feels broken. */
.f-row input,.f-row select,.f-row textarea{width:100%;font:inherit;
  font-size:16px;min-height:48px;padding:.85rem 1rem;
  border:1.5px solid var(--line);border-radius:10px;
  background:#FBFCFE;color:var(--text);
  transition:border-color .16s ease-out,box-shadow .16s ease-out,
    background-color .16s ease-out}
.f-row textarea{min-height:96px;resize:vertical}
.f-row input::placeholder,.f-row textarea::placeholder{color:#9AA6BC}
.f-row input:hover,.f-row select:hover,.f-row textarea:hover{
  border-color:#B9C6DA}
.f-row input:focus,.f-row select:focus,.f-row textarea:focus{
  border-color:var(--signal);outline:none;background:var(--white);
  box-shadow:0 0 0 4px rgba(2,115,255,.16)}
/* Valid-once-filled, but only after the field is left -- validating on every
   keystroke tells someone their half-typed phone number is wrong. */
.f-row input:not(:placeholder-shown):user-valid{border-color:rgba(18,185,129,.5)}
.f-row [aria-invalid=true]{border-color:var(--alert);
  box-shadow:0 0 0 4px rgba(255,77,94,.14)}
.f-err{color:var(--alert);font-size:.88rem;margin:.35rem 0 0}
.f-urgency{border:0;padding:0;margin:0 0 1.15rem;display:grid;gap:.5rem}
/* Urgency options are full-width rows, not pills. Three pills wrapping
   unevenly across two lines read as an afterthought; stacked rows read as a
   choice, and each one gets a 44px+ target on a phone. */
.f-chip{display:flex;align-items:center;gap:.75rem;margin:0;
  padding:.8rem 1rem;min-height:52px;
  border:1.5px solid var(--line);border-radius:12px;background:#FBFCFE;
  cursor:pointer;font-family:var(--display);font-weight:600;font-size:.92rem;
  transition:border-color .16s ease-out,background-color .16s ease-out,
    box-shadow .16s ease-out}
.f-chip:hover{border-color:#B9C6DA}
/* The native radio dot is hidden, not removed. It stays in the DOM, in the
   tab order, and operable by arrow keys -- display:none would take all three
   away and break the fieldset for anyone not using a mouse. Selection is
   shown by the card itself plus a tick, so it is never carried by colour
   alone. */
.f-chip input{position:absolute;width:1px;height:1px;min-height:0;margin:0;
  opacity:0;pointer-events:none}
.f-chip{position:relative;padding-right:2.9rem}
/* The tick. Drawn from two borders rather than an icon file so it inherits
   currentColor and costs nothing to ship. */
.f-chip::after{content:"";position:absolute;right:1.15rem;top:50%;
  width:15px;height:9px;margin-top:-7px;
  border-left:2.5px solid var(--signal-dk);
  border-bottom:2.5px solid var(--signal-dk);
  transform:rotate(-45deg) scale(.6);transform-origin:center;
  opacity:0;transition:opacity .16s ease-out,transform .16s ease-out}
.f-chip:has(input:checked)::after{opacity:1;transform:rotate(-45deg) scale(1)}
@media (prefers-reduced-motion:reduce){.f-chip::after{transition:none}}
.f-chip:has(input:checked){border-color:var(--signal);
  background:var(--signal-lt);color:var(--signal-dk);
  box-shadow:0 0 0 3px rgba(2,115,255,.14)}
.f-chip:has(input:focus-visible){outline:3px solid var(--signal);
  outline-offset:2px}
.f-hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.f-status{margin:.9rem 0 0;font-weight:600;font-family:var(--display)}
.f-status[data-kind=ok]{color:var(--trust)}
.f-status[data-kind=err]{color:var(--alert)}
.f-fine{font-size:.86rem;color:var(--text-soft);margin:.7rem 0 0}
button[data-submit][disabled]{opacity:.65;cursor:progress}
@media (max-width:900px){.form-grid{grid-template-columns:1fr;gap:1.8rem}}

/* ---- booking widget: chips, pairing, success ------------------------- */
.bk-body{display:block}
/* Short fields paired so the card does not become a column of eight
   identical boxes -- which is what makes a form look long before anyone has
   read a word of it. Collapses on narrow screens. */
.f-pair{display:grid;grid-template-columns:1fr 1fr;gap:0 .9rem}
@media (max-width:520px){.f-pair{grid-template-columns:1fr}}
.f-rule{border:0;border-top:1px solid var(--line);margin:1.4rem 0}

/* Arrival windows as chips. A dropdown hides every option behind a tap and
   gives no sense of how much choice there is -- the windows are the
   friendliest part of the offer, so they are shown. */
.win-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));
  gap:.5rem}
.win{font:inherit;font-family:var(--mono);font-size:.8rem;letter-spacing:.02em;
  padding:.7rem .5rem;min-height:44px;cursor:pointer;text-align:center;
  background:var(--white);color:var(--text);border:1px solid var(--line);
  border-radius:var(--r-sm);
  transition:border-color .16s ease-out,background-color .16s ease-out,
    color .16s ease-out,transform .12s ease-out}
.win:hover{border-color:var(--signal)}
.win:active{transform:translateY(1px)}
.win[aria-checked=true]{background:var(--signal);color:#fff;
  border-color:var(--signal);font-weight:700;
  box-shadow:0 0 0 3px rgba(2,115,255,.18)}
.win:focus-visible{outline:3px solid var(--signal);outline-offset:2px}
.win-none{font-size:.9rem;color:var(--text-soft);margin:0}

/* Urgency chips get a two-line label so the option explains itself. */
.f-urgency .f-chip span{display:flex;flex-direction:column;line-height:1.25}
.f-urgency .f-chip strong{font-family:var(--display);font-size:.95rem}
.f-urgency .f-chip span strong + *{font-size:.78rem}

.f-select{position:relative}
.f-select::after{content:"";position:absolute;right:.95rem;top:50%;
  width:7px;height:7px;margin-top:-5px;pointer-events:none;
  border-right:2px solid var(--text-soft);border-bottom:2px solid var(--text-soft);
  transform:rotate(45deg)}
.f-select select{appearance:none;padding-right:2.4rem}

.bk-actions{display:flex;gap:.6rem;align-items:center}
.bk-actions .btn-lg{flex:1 1 auto;justify-content:center}

/* Success replaces the card instead of appending a line under a form that
   still looks like it wants filling in. */
.bk-done{text-align:center;padding:1.4rem .4rem .6rem}
.bk-tick{display:inline-grid;place-items:center;width:56px;height:56px;
  border-radius:50%;background:rgba(18,185,129,.14);color:var(--trust);
  margin-bottom:.9rem}
.bk-done h3{margin-bottom:.3em}
.bk-done p{color:var(--text-soft);margin-bottom:1.2rem}
@media (prefers-reduced-motion: no-preference){
  @keyframes tickIn{from{transform:scale(.7)}to{transform:none}}
  .bk-tick{animation:tickIn .32s cubic-bezier(.16,1,.3,1)}
}
