/* ══════════════════════════════════════════════════════════════
   MOTION SYSTEM
   Scroll-driven storytelling layered on top of the approved
   designs. Nothing in here changes a composition: every effect is
   transform / opacity / clip-path only, so no layout is read or
   written during scroll and nothing can shift the page.

   How it works
   · [data-reveal]        an element that animates in once, on entry
   · [data-stagger]       its direct children follow in sequence
   · [data-parallax]      driven by one shared rAF loop in motion.js
   · .is-in               added by the observer when the element enters

   Each section sets its own duration + distance through the
   --rv-dur / --rv-dist / --stagger custom properties, so the
   sections share one system but keep distinct personalities.
   ══════════════════════════════════════════════════════════════ */

:root{
  --ease-out: cubic-bezier(.22, .61, .36, 1);
  --ease-io:  cubic-bezier(.45, 0, .25, 1);
  --rv-dur:   .78s;
  --rv-dist:  34px;
  --stagger:  95ms;
}

/* ── Reveals ─────────────────────────────────────────────────── */
[data-reveal]{
  opacity:0;
  transition:opacity var(--rv-dur) var(--ease-out),
             transform var(--rv-dur) var(--ease-out),
             clip-path var(--rv-dur) var(--ease-out);
}
[data-reveal="up"]     {transform:translate3d(0, var(--rv-dist), 0)}
[data-reveal="down"]   {transform:translate3d(0, calc(var(--rv-dist) * -1), 0)}
[data-reveal="left"]   {transform:translate3d(calc(var(--rv-dist) * -1), 0, 0)}
[data-reveal="right"]  {transform:translate3d(var(--rv-dist), 0, 0)}
[data-reveal="scale"]  {transform:scale(1.045)}
[data-reveal="fade"]   {transform:none}
/* Masked image reveal — the photo wipes in rather than fading.
   The clip goes on the INNER wrapper, never on the observed element:
   an element clipped to zero area reports no intersection, so a mask
   on the observed box would stop its own reveal from ever firing. */
[data-reveal="mask"],
[data-reveal="mask-l"]{opacity:1}
[data-reveal="mask"]   > picture,
[data-reveal="mask"]   > img,
[data-reveal="mask-l"] > picture,
[data-reveal="mask-l"] > img{
  display:block;
  transition:clip-path var(--rv-dur) var(--ease-out),
             transform var(--rv-dur) var(--ease-out);
}
[data-reveal="mask"]   > picture,
[data-reveal="mask"]   > img{clip-path:inset(0 0 100% 0); transform:scale(1.05)}
[data-reveal="mask-l"] > picture,
[data-reveal="mask-l"] > img{clip-path:inset(0 100% 0 0)}
[data-reveal="mask"].is-in   > picture,
[data-reveal="mask"].is-in   > img,
[data-reveal="mask-l"].is-in > picture,
[data-reveal="mask-l"].is-in > img{clip-path:inset(0 0 0 0); transform:none}

[data-reveal].is-in{
  opacity:1;
  transform:none;
}

/* ── Stagger ─────────────────────────────────────────────────── */
/* nth-child rather than a JS-set index: no scripting cost, and the
   order is legible in the stylesheet.
   --stag0 / --seq0 are per-section BASE delays: they hold a group
   back until the section's establishing image has landed, so every
   section leads with its picture (the Service Areas standard).
   The resolved delay is published as --kid-delay / --seq-delay so
   follow-on effects (stars wipe, gold sweep, call pulse) can chain
   off the same clock instead of guessing. */
[data-stagger] > *{--kid-delay:calc(var(--stag0, 0ms) + var(--stagger) * 9)}
[data-stagger] > :nth-child(1){--kid-delay:calc(var(--stag0, 0ms) + var(--stagger) * 0)}
[data-stagger] > :nth-child(2){--kid-delay:calc(var(--stag0, 0ms) + var(--stagger) * 1)}
[data-stagger] > :nth-child(3){--kid-delay:calc(var(--stag0, 0ms) + var(--stagger) * 2)}
[data-stagger] > :nth-child(4){--kid-delay:calc(var(--stag0, 0ms) + var(--stagger) * 3)}
[data-stagger] > :nth-child(5){--kid-delay:calc(var(--stag0, 0ms) + var(--stagger) * 4)}
[data-stagger] > :nth-child(6){--kid-delay:calc(var(--stag0, 0ms) + var(--stagger) * 5)}
[data-stagger] > :nth-child(7){--kid-delay:calc(var(--stag0, 0ms) + var(--stagger) * 6)}
[data-stagger] > :nth-child(8){--kid-delay:calc(var(--stag0, 0ms) + var(--stagger) * 7)}
[data-stagger] > :nth-child(9){--kid-delay:calc(var(--stag0, 0ms) + var(--stagger) * 8)}
[data-stagger] > *{transition-delay:var(--kid-delay)}
/* an explicit running order, where source order isn't the reveal order */
[data-seq="1"]{--seq-delay:calc(var(--seq0, 0ms) + var(--stagger) * 1)}
[data-seq="2"]{--seq-delay:calc(var(--seq0, 0ms) + var(--stagger) * 2)}
[data-seq="3"]{--seq-delay:calc(var(--seq0, 0ms) + var(--stagger) * 3)}
[data-seq="4"]{--seq-delay:calc(var(--seq0, 0ms) + var(--stagger) * 4)}
[data-seq="5"]{--seq-delay:calc(var(--seq0, 0ms) + var(--stagger) * 5)}
[data-seq="6"]{--seq-delay:calc(var(--seq0, 0ms) + var(--stagger) * 6)}
[data-seq="7"]{--seq-delay:calc(var(--seq0, 0ms) + var(--stagger) * 7)}
[data-seq="8"]{--seq-delay:calc(var(--seq0, 0ms) + var(--stagger) * 8)}
[data-seq="9"]{--seq-delay:calc(var(--seq0, 0ms) + var(--stagger) * 9)}
[data-seq]{transition-delay:var(--seq-delay)}
/* Once an element has revealed and settled, its choreography delay is
   spent — .is-set (added by motion.js 3s after reveal) returns
   transition-delay to zero so hover feedback is never queued behind an
   entrance that already happened. */
.is-set{transition-delay:0ms !important}

/* ── Parallax ────────────────────────────────────────────────── */
/* motion.js writes --py; the element only ever translates. */
[data-parallax]{transform:translate3d(0, var(--py, 0px), 0); will-change:transform}


/* ══════════════════════════════════════════════════════════════
   HERO — load sequence
   One composition, not six fades. Everything shares a single
   upward drift and the timings overlap heavily, so the hero
   settles as one object; only the emphasis moves through it.
   The hidden state lives under .qsl-arm (set inline in <head>)
   so the browser paints it before the transition starts.
   ══════════════════════════════════════════════════════════════ */
#hero .hp-eyebrow,
#hero .hp-h1,
#hero .hp-line,
#hero .hp-services,
#hero .hp-btns,
#hero .hp-trust,
.hp-logo{
  transition:opacity 1.15s var(--ease-out), transform 1.15s var(--ease-out);
}
.qsl-arm #hero .hp-eyebrow,
.qsl-arm #hero .hp-h1,
.qsl-arm #hero .hp-line,
.qsl-arm #hero .hp-services,
.qsl-arm #hero .hp-btns,
.qsl-arm #hero .hp-trust{opacity:0; transform:translate3d(0, 26px, 0)}
.qsl-arm .hp-logo{opacity:0; transform:translate3d(0, -10px, 0)}
.qsl-arm #hero .hp-h1{transform:translate3d(0, 38px, 0)}
.qsl-arm #hero .hp-line{transform:scaleX(0); transform-origin:left center}

/* overlapping, not sequential — each starts before the last finishes */
.hp-logo              {transition-delay:.05s}
#hero .hp-eyebrow     {transition-delay:.14s}
#hero .hp-h1          {transition-delay:.22s}
#hero .hp-line        {transition-delay:.40s; transition-duration:.9s}
#hero .hp-services    {transition-delay:.46s}
#hero .hp-btns        {transition-delay:.56s}
#hero .hp-trust       {transition-delay:.66s}

/* The skyline settles with the copy rather than sitting there already
   finished — a very slight scale-off, no aggressive zoom.
   This rides the artwork LAYER (#hero::before), never #hero itself: #hero
   lays out the hero composition, and a geometry-altering transform on a
   layout container is exactly what crushed the Heritage timeline into
   itself. See feedback-never-transform-layout-containers. */
/* Reveal animations slide in from translateX(32px), which puts them past the
   right edge for the first half-second and flashes a horizontal scrollbar on
   load. shared.css guards `body`, but the html element still scrolls, so the
   guard never fired — and the homepage does not load shared.css at all.

   `clip` rather than `hidden`: hidden would make html a scroll container and
   break the one position:sticky element on the page. clip does not. */
html{overflow-x:clip}
body{overflow-x:clip}

/* Clip the hero, not the whole header block.
   On .hp-top this did two unintended things: it cut the mega panels off on
   inner pages, where .hp-top is only the bar and the dropdown is three times
   its height; and it clipped the header's own full-bleed background
   (.hp-top::before, width:100vw) back to the 1536 cap, leaving white gutters
   either side of the navy bar above 1536px. #hero is what actually needs
   containing — it is the element whose ::before artwork scales. */
#hero{overflow:hidden}
#hero::before{transition:transform 1.6s var(--ease-out)}
.qsl-arm #hero::before{transform:scale(1.035)}

/* ══════════════════════════════════════════════════════════════
   HEADER — compacts on scroll
   Height and logo width are container-query units, so both stay on
   the compositor and the bar never reflows the page below it.
   ══════════════════════════════════════════════════════════════ */
#site-header{transition:background-color .35s var(--ease-io), box-shadow .35s var(--ease-io)}
.hp-top.is-compact #site-header{
  background:#000B1B;
  box-shadow:0 6px 26px rgba(0,0,0,.42);
}
.hp-logo,
.hp-nav,
.hp-call{transition:transform .38s var(--ease-io), opacity .38s var(--ease-io)}
.hp-top.is-compact .hp-logo{transform:scale(.86); transform-origin:left center}
.hp-top.is-compact .hp-call{transform:scale(.94); transform-origin:right center}


/* ══════════════════════════════════════════════════════════════
   SECTION PERSONALITIES
   Same system, different pacing — the brief asks for each section
   to move differently while still reading as one site.
   ══════════════════════════════════════════════════════════════ */

/* HP-03 · four revenue categories — brisk, sequential */
.hp03{--rv-dur:.66s; --rv-dist:30px; --stagger:105ms}

/* HP-04 · Commercial — photo left, content right, services stagger */
.hp04{--rv-dur:.8s; --rv-dist:40px; --stagger:80ms}

/* HP-07 · Residential — calmer and warmer: longer, shorter travel */
.hp07{--rv-dur:.95s; --rv-dist:26px; --stagger:105ms}

/* HP-A · Automotive — more energy: quicker, further */
.hpA{--rv-dur:.62s; --rv-dist:44px; --stagger:85ms}

/* HP-B · Emergency — the strongest treatment on the page */
.hpB{--rv-dur:.58s; --rv-dist:40px; --stagger:75ms}

/* HP-09 · Heritage — deliberately slow, to change the page's pace */
.hp09{--rv-dur:1.05s; --rv-dist:24px; --stagger:150ms}

/* HP-10 · Reviews — measured */
.hp10{--rv-dur:.8s; --rv-dist:30px; --stagger:130ms}

/* HP-11 · Service areas */
.hp11{--rv-dur:.75s; --rv-dist:28px; --stagger:90ms}

/* HP-13 · Lead form — calm at the conversion point */
.hp13{--rv-dur:.9s; --rv-dist:22px; --stagger:110ms}

/* HP-14 · Footer — restrained */
.hp14{--rv-dur:.8s; --rv-dist:18px; --stagger:70ms}


/* ══════════════════════════════════════════════════════════════
   CHOREOGRAPHY — the Service Areas standard, applied per section.
   Every section: establishing image first, then headline, then
   support, then items, then feature, then CTA — one pass, then
   completely static. Desktop-only: on phones each element enters
   the viewport on its own, so base delays would just feel slow.
   ══════════════════════════════════════════════════════════════ */
@media (min-width:901px){
  /* Commercial · building → headline → copy → services → Access
     Control sweep → CTA */
  .hp04{--seq0:300ms; --stag0:850ms}
  .hp04__cta{--seq0:1030ms}

  /* Residential · house → headline → services → JUST MOVED? → CTA */
  .hp07{--seq0:300ms; --stag0:850ms}
  .hp07__moved{--seq0:1100ms}
  .hp07__foot{--seq0:1200ms}

  /* Automotive · key imagery → headline → services → the two
     routes (HELP NOW / NEED A KEY) → call bar → foot */
  .hpA{--seq0:280ms; --stag0:800ms}
  .hpA__routes{--stag0:1100ms}

  /* Emergency · scene → LOCKED OUT? → 24/7 → services → call
     block → CALL NOW (one pulse, after it lands) → assurance */
  .hpB{--seq0:260ms; --stag0:750ms}
  .hpB__call{--seq0:950ms}
  .hpB__btn.gold{--seq0:1050ms}

  /* Heritage · archive photos → title → 1977 → 80s → 90s →
     2000s → Today, the rule drawing down through the years */
  .hp09{--seq0:180ms}

  /* Categories / Reviews / Lead form · title leads, items follow */
  .hp03{--stag0:420ms}
  .hp10{--stag0:220ms}
  .hp13{--stag0:300ms}
  .hp13__form{--seq0:250ms}
  .hp13__foot{--seq0:300ms}
}


/* ══════════════════════════════════════════════════════════════
   SECTION-SPECIFIC MOMENTS
   ══════════════════════════════════════════════════════════════ */

/* Emergency · the call CTA gets ONE pulse as it arrives, never a loop */
@keyframes qsl-pulse-once{
  0%   {box-shadow:0 0 0 0 rgba(229,169,59,.55)}
  70%  {box-shadow:0 0 0 16px rgba(229,169,59,0)}
  100% {box-shadow:0 0 0 0 rgba(229,169,59,0)}
}
.hpB__btn.gold.is-in{animation:qsl-pulse-once 1.5s var(--ease-out) calc(var(--seq-delay, 0ms) + .5s) 1 both}

/* Heritage · the timeline rule draws down as the steps arrive.
   STRUCTURAL RULE — the draw lives on the step connectors
   (.hp09-step::before, the actual thin rules), NEVER on .hp09__line:
   that class is the CONTAINER holding all four steps' content, and a
   scaleY on it vertically squashes every line of timeline copy into
   the same space (the client caught it as overlapping text). A
   transform may only ever be applied to decoration, not to a box
   that lays out content. */
.hp09-step::before{transform-origin:top center; transform:scaleY(0); transition:transform .9s var(--ease-io)}
.hp09-step.is-in::before{transform:scaleY(1)}

/* Reviews · stars wipe in behind the card reveal, chained off the
   card's own stagger slot so they always follow their card */
.is-in
/* Service areas · the map arrives as one cinematic beat, then rests.
   1 · the art settles from a slow push-in (scale on the picture — the
       observed element must never be clipped or hidden by its own effect)
   2 · a gold light-bar scans across it once
   3 · radar rings fire ONCE in a wave radiating out from Memphis and
       fade to nothing — ~3.7s total, then the section is fully static.
       .is-in is never removed and every animation runs a single
       iteration, so the sequence cannot replay on re-scroll.
       cq units keep every ring proportional to the map at any viewport. */
.hp11__map > picture{transform:scale(1.1); transition:transform 2.4s var(--ease-io)}
/* the pins layer settles WITH the art, so the rings stay registered on
   the baked-in pins throughout the push-in (both boxes share a centre) */
.hp11__pins{transform:translate(-50%,-50%) scale(1.1); transition:transform 2.4s var(--ease-io)}
/* the HOME BASE layer has to ride the same push-in as the art and the pin
   layer — without this it sat still while both of those scaled 1.1 -> 1,
   so the marker drifted off its coordinate for the 2.4s of the reveal */
.hp11__home-layer{transform:translate(-50%,-50%) scale(1.1); transition:transform 2.4s var(--ease-io)}
.hp11__map.is-in > picture{transform:none}
.hp11__map.is-in .hp11__pins{transform:translate(-50%,-50%)}
.hp11__map.is-in .hp11__home-layer{transform:translate(-50%,-50%)}

.hp11__scan{position:absolute;inset:0;overflow:hidden;pointer-events:none}
.hp11__scan::before{
  content:'';position:absolute;top:0;bottom:0;left:0;width:9cqw;
  transform:translateX(-10cqw);opacity:0;
  background:linear-gradient(100deg,
    rgba(231,176,73,0) 0%, rgba(231,176,73,.16) 38%,
    rgba(255,224,150,.5) 50%,
    rgba(231,176,73,.16) 62%, rgba(231,176,73,0) 100%);
}
.hp11__map.is-in .hp11__scan::before{animation:qsl-map-scan 2s var(--ease-io) .3s 1 both}
@keyframes qsl-map-scan{
  0%   {transform:translateX(-10cqw); opacity:0}
  12%  {opacity:1}
  80%  {opacity:1}
  100% {transform:translateX(112cqw); opacity:0}
}

.hp11-pin{
  position:absolute;left:var(--x);top:var(--y);
  width:var(--ring,7cqw);height:var(--ring,7cqw);
  margin:calc(var(--ring,7cqw) / -2) 0 0 calc(var(--ring,7cqw) / -2);
}
.hp11-pin::before,
.hp11-pin::after{
  content:'';position:absolute;inset:0;border-radius:50%;
  border:calc(var(--px,1px) * 2.5) solid rgba(231,176,73,.85);
  box-shadow:0 0 calc(var(--px,1px) * 14) rgba(231,176,73,.35),
             inset 0 0 calc(var(--px,1px) * 10) rgba(231,176,73,.18);
  opacity:0;transform:scale(.18);
}
.hp11-pin::after{display:none}
.hp11-pin--hub{--ring:16cqw}
.hp11-pin--hub::after{display:block}
.hp11__map.is-in .hp11-pin::before{
  animation:qsl-ping 1.15s cubic-bezier(.16,.6,.4,.98) var(--d,0s) 1 both;
}
.hp11__map.is-in .hp11-pin--hub::before{animation-duration:1.3s}
.hp11__map.is-in .hp11-pin--hub::after{
  animation:qsl-ping 1.3s cubic-bezier(.16,.6,.4,.98) calc(var(--d,0s) + .45s) 1 both;
}
@keyframes qsl-ping{
  0%   {opacity:0; transform:scale(.18)}
  14%  {opacity:.95}
  70%  {opacity:0; transform:scale(1)}
  100% {opacity:0; transform:scale(1)}
}

/* ── HOME BASE "you are here" ring — MOBILE ONLY ──────────────
   Desktop keeps the full logo badge with no ring, per the brief.
   Both rings are pseudo-elements on the 26px disc and are absolutely
   positioned, so they contribute NOTHING to the marker's box: the
   collision geometry verified at 390/430/768/1280/1536/1920 is
   unchanged. Centred by negative margins (the same technique
   .hp11-pin uses) rather than translate(-50%,-50%), which keeps the
   `transform` property free for the scale — so only opacity and
   transform ever animate, never width/height. The disc and its brand
   mark never move. Monotonic scale + monotonic fade: no bounce, no
   flash, nothing that reads as an alert beacon. */
@media (max-width:980px){
  .hp11-home__badge{position:relative}
  .hp11-home__badge::before,
  .hp11-home__badge::after{
    content:'';position:absolute;left:50%;top:50%;
    width:33px;height:33px;margin:-16.5px 0 0 -16.5px;
    border-radius:50%;
    pointer-events:none;
  }
  /* the static ring — always present, including under reduced motion */
  .hp11-home__badge::before{border:1px solid rgba(231,176,73,.5)}
  /* the pulse — 33px out to ~45px by scale alone */
  .hp11-home__badge::after{
    border:1.5px solid rgba(231,176,73,.9);
    opacity:0;
    animation:qsl-home-pulse 2.7s ease-out infinite;
  }
  @keyframes qsl-home-pulse{
    0%   {transform:scale(1);    opacity:.45}
    100% {transform:scale(1.36); opacity:0}
  }
}


/* ══════════════════════════════════════════════════════════════
   MICROINTERACTIONS
   Refined, not playful: no bounce, no spin, no persistent glow.
   ══════════════════════════════════════════════════════════════ */
.hp03-card,
.hp13-choice,
.hpA-route,
.svc-card,
.icard{transition:transform .34s var(--ease-out), box-shadow .34s var(--ease-out), border-color .34s var(--ease-out)}
/* Two-phase transitions for interactive cards: these carry their own
   hover `transition` shorthands (here and in home.css — a.hp04__cell)
   which would silently cancel the reveal transition AND its stagger
   delay. Until .is-set lands (3s after reveal), the entrance owns the
   transition; afterwards the rule stops matching and the hover
   transitions above resume untouched. */
.hp03-card:not(.is-set), .hp13-choice:not(.is-set), .hpA-route:not(.is-set),
.svc-card:not(.is-set), .icard:not(.is-set), a.hp04__cell:not(.is-set){
  transition:opacity var(--rv-dur) var(--ease-out), transform var(--rv-dur) var(--ease-out);
  transition-delay:var(--kid-delay, 0ms);
}

@media (hover:hover) and (pointer:fine){
  .hp03-card:hover{transform:translate3d(0,-6px,0)}
  .hp03-card:hover .hp03-card__photo{transform:scale(1.06)}
  .hp03-card:hover .hp03-card__link svg{transform:translate3d(6px,0,0)}

  .hp04__cell:hover svg,
  .hp07__pills a:hover svg,
  .hpA__pills div:hover svg,
  .hpB__pills div:hover svg{transform:translate3d(0,-3px,0)}

  /* Access Control links to a growth service, so it answers a little harder */
  a.hp04__cell[href*="access-control"]:hover{background:rgba(212,162,57,.16)}
  a.hp04__cell[href*="access-control"]:hover svg{transform:translate3d(0,-4px,0) scale(1.06)}

  .hpA__cta:hover svg,
  .hpB__cta:hover svg,
  .hp04__cta:hover svg,
  .hp07__cta:hover svg,
  .hp13__submit:hover svg{transform:translate3d(5px,0,0)}

  .hp14-col a:hover{transform:translate3d(3px,0,0)}
  .hp14__social a:hover{transform:translate3d(0,-3px,0)}
}

.hp03-card__photo,
.hp03-card__link svg,
.hp04__cell svg,
.hp07__pills a svg,
.hpA__pills div svg,
.hpB__pills div svg,
.hpA__cta svg,
.hpB__cta svg,
.hp04__cta svg,
.hp07__cta svg,
.hp13__submit svg,
.hp14-col a,
.hp14__social a{transition:transform .34s var(--ease-out)}

/* pressed state — immediate, so a tap never feels laggy */
.hp-btn:active,
.hpA__callbtn:active,
.hpB__btn:active,
.hp13__submit:active,
.qcta__btn:active,
.btn:active{transform:translate3d(0,1px,0)}

/* the selected lead-form card responds at once, with no reveal delay */
.hp13-choice{transition:transform .34s var(--ease-out), box-shadow .34s var(--ease-out), border-color .18s linear, background-color .18s linear}


/* ══════════════════════════════════════════════════════════════
   DEPTH — a few memorable moments, quiet in between
   Three devices only, used sparingly:
     · display type leads the supporting copy
     · photography settles in and grades dark-to-light
     · one section gets a distinct signature
   ══════════════════════════════════════════════════════════════ */

/* ── Oversized type moves differently from body copy ──────────
   The display lines travel further and take longer than the lede
   beneath them, so the headline reads as the heavier object. */
/* NOTE: .qsl-display > span already carries scaleX(--sx) — the display face
   is drawn narrower in the mockups than Anton actually is. Any transform we
   add here MUST keep that scale, or the headline renders 20% too wide and
   runs off its panel. Compose, never replace. */
[data-reveal].qsl-display > span{
  transition:opacity calc(var(--rv-dur) * 1.35) var(--ease-out),
             transform calc(var(--rv-dur) * 1.35) var(--ease-out);
}
[data-reveal].qsl-display:not(.is-in) > span{
  opacity:0;
  transform:translate3d(0, calc(var(--rv-dist) * 1.5), 0) scaleX(var(--sx));
}
[data-reveal].qsl-display.is-in > span{
  opacity:1;
  transform:scaleX(var(--sx));
}
[data-reveal].qsl-display.is-in > span:nth-child(2){transition-delay:.12s}
[data-reveal].qsl-display.is-in > span:nth-child(3){transition-delay:.22s}
/* the headline box itself must not also fade, or the lines double up */
[data-reveal].qsl-display{opacity:1}

/* ── Photography settles and grades in ───────────────────────
   The image starts slightly oversized, dark and desaturated, then
   resolves. Selective: strongest where the brief wants weight. */
[data-grade] > picture,
[data-grade] > img{
  transition:filter var(--grade-dur, 1.1s) var(--ease-out),
             transform var(--grade-dur, 1.1s) var(--ease-out),
             clip-path var(--rv-dur) var(--ease-out);
}
[data-grade]:not(.is-in) > picture,
[data-grade]:not(.is-in) > img{
  filter:brightness(var(--grade-b, .62)) saturate(var(--grade-s, .8));
  transform:scale(var(--grade-scale, 1.07));
}
[data-grade].is-in > picture,
[data-grade].is-in > img{filter:none; transform:none}

/* Commercial — the most confident movement on the page */
.hp04 [data-grade]{--grade-dur:1.5s; --grade-b:.5; --grade-s:.72; --grade-scale:1.10}
.hp04 .hp04__media{--rv-dur:1.25s}
/* Automotive — quicker, with energy */
.hpA  [data-grade]{--grade-dur:1s;   --grade-b:.6; --grade-s:.82; --grade-scale:1.07}
/* Residential — calm and warm: barely graded, barely scaled */
.hp07 [data-grade]{--grade-dur:1.5s; --grade-b:.84; --grade-s:.95; --grade-scale:1.035}
/* Emergency — already a night shot, so only a light lift */
.hpB  [data-grade]{--grade-dur:1.2s; --grade-b:.66; --grade-s:.9;  --grade-scale:1.05}
/* Heritage — slow and heavy, the archive photograph earning its place */
.hp09 [data-grade]{--grade-dur:1.9s; --grade-b:.5; --grade-s:.62; --grade-scale:1.08}

/* ── Access Control · a little technological energy ──────────
   One gold sweep across the tile as it arrives, then still. */
@keyframes qsl-sweep{
  from{transform:translate3d(-120%,0,0)}
  to  {transform:translate3d(220%,0,0)}
}
a.hp04__cell[href*="access-control"]{position:relative; overflow:hidden}
a.hp04__cell[href*="access-control"]::after{
  content:''; position:absolute; top:0; bottom:0; left:0; width:38%;
  background:linear-gradient(100deg, transparent, rgba(229,169,59,.22), transparent);
  transform:translate3d(-120%,0,0);
  pointer-events:none;
}
a.hp04__cell[href*="access-control"].is-in::after{
  /* chained off the cell's stagger slot — the sweep is the section's
     closing beat, after the grid has finished arriving */
  animation:qsl-sweep 1.15s var(--ease-out) calc(var(--kid-delay, 0ms) + .6s) 1 both;
}

/* ── Heritage · the timeline should be noticed, not waited on ─
   Steps arrive briskly; only the rule drawing down is slow. */
.hp09{--stagger:110ms}
.hp09-step{--rv-dur:.7s}


/* ── Interior · Safes and Recent Work ────────────────────────
   The Safes/showroom and Projects sections left the homepage in
   the revenue-category refactor, so their treatments live on the
   interior pages. Safes gets the heaviest, slowest movement on
   the site — the product imagery should feel substantial.
   Projects get depth: a scale-settle as each card enters. */
.tcard[data-grade],
.hero-photo[data-grade]{--grade-dur:1.8s; --grade-b:.52; --grade-s:.7; --grade-scale:1.09}
.tcard[data-grade]{--rv-dur:1.05s; --rv-dist:30px}
.tcards[data-stagger], .tcards.stagger-children{--stagger:150ms}

.work-card[data-grade]{--grade-dur:1.15s; --grade-b:.6; --grade-s:.8; --grade-scale:1.08; --rv-dur:.85s}
.work-grid{--stagger:110ms}

/* the grade needs something to grade — these cards hold a bare <img> */
.tcard[data-grade] > img,
.work-card[data-grade] > img,
.hero-photo[data-grade] > img{transition:filter var(--grade-dur,1.1s) var(--ease-out), transform var(--grade-dur,1.1s) var(--ease-out)}
.tcard[data-grade]{overflow:hidden}


/* ══════════════════════════════════════════════════════════════
   MOBILE — a simpler system, not a scaled-down desktop one
   Shorter travel, shorter durations, tighter stagger, no parallax.
   ══════════════════════════════════════════════════════════════ */
@media (max-width:900px){
  :root{--rv-dur:.55s; --rv-dist:20px; --stagger:70ms}
  .hp03,.hp04,.hp07,.hpA,.hpB,.hp09,.hp10,.hp11,.hp13,.hp14{
    --rv-dur:.55s; --rv-dist:20px; --stagger:70ms;
  }
  /* everything arrives vertically on a phone */
  [data-reveal="left"],
  [data-reveal="right"]{transform:translate3d(0, var(--rv-dist), 0)}
  [data-reveal="mask"]   > picture, [data-reveal="mask"]   > img,
  [data-reveal="mask-l"] > picture, [data-reveal="mask-l"] > img{clip-path:none; transform:none}
  [data-reveal="scale"]{transform:translate3d(0, var(--rv-dist), 0)}
  /* parallax is disabled in script too; this is the belt-and-braces half */
  [data-reveal="mask"] > picture, [data-reveal="mask"] > img,
  [data-reveal="mask-l"] > picture, [data-reveal="mask-l"] > img{clip-path:none !important; transform:none !important; transition:none !important}
  [data-parallax]{transform:none !important}
  .hp04__panel{transform:none !important}
  [data-grade] > picture, [data-grade] > img{filter:none !important; transform:none !important; transition:none !important}
  [data-reveal].qsl-display > span{opacity:1 !important; transform:scaleX(var(--sx)) !important; transition:none !important}
  a.hp04__cell[href*="access-control"]::after{display:none !important}
  [data-parallax-frame] img{position:static !important; top:auto !important; height:100% !important}
  .hp09-step::before{transition-duration:.6s}
  /* A phone visitor must be able to hit Call immediately: the buttons and
     trust bar arrive almost at once and the whole sequence is shorter. */
  #hero .hp-eyebrow, #hero .hp-h1, #hero .hp-line,
  #hero .hp-services, #hero .hp-btns, #hero .hp-trust,
  .hp-logo{transition-duration:.6s}
  #hero .hp-eyebrow{transition-delay:.06s}
  #hero .hp-h1     {transition-delay:.12s}
  #hero .hp-line   {transition-delay:.20s}
  #hero .hp-services{transition-delay:.22s}
  #hero .hp-btns   {transition-delay:.16s}
  #hero .hp-trust  {transition-delay:.26s}
  .qsl-arm #hero::before{transform:none}
  .qsl-arm #hero .hp-btns{transform:translate3d(0,12px,0)}
}


/* ══════════════════════════════════════════════════════════════
   PARALLAX SAFETY
   A parallaxed photo is given headroom and its frame is clipped,
   so the offset can never expose an edge inside the composition.
   The frames are rectangles that nothing currently overflows, so
   clipping them changes no layout.
   ══════════════════════════════════════════════════════════════ */
.hp04__media,
.hp07__media,
.hpA__media,
.hpB__media,
.hp09__past,
.hp09__now{overflow:hidden}

/* Continuous parallax was retired from the homepage (sections must
   settle completely once their entrance finishes), but the frame
   markup stays: it gives these media boxes a definite height for the
   cover image to resolve against — a bare percentage height would
   silently collapse to intrinsic ratio and leave a gap. The height
   must be stated because an absolutely positioned REPLACED element
   ignores `bottom` when its height is auto. */
[data-parallax-frame]{position:absolute;inset:0;display:block}
[data-parallax-frame] img{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  object-fit:cover;
}
.hp04__media{height:100%}
@media (max-width:900px){
  [data-parallax-frame]{position:relative;inset:auto}
  [data-parallax-frame] img{position:static; height:100%}
}


/* ══════════════════════════════════════════════════════════════
   REDUCED MOTION
   A clean static site — everything visible, nothing animating.
   ══════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce){
  [data-reveal],
  [data-reveal].is-in,
  .qsl-arm #hero, .qsl-arm #hero .hp-eyebrow, .qsl-arm #hero .hp-h1,
  .qsl-arm #hero .hp-line, .qsl-arm #hero .hp-services,
  .qsl-arm #hero .hp-btns, .qsl-arm #hero .hp-trust, .qsl-arm .hp-logo,
  #hero, #hero .hp-eyebrow, #hero .hp-h1, #hero .hp-line,
  #hero .hp-services, #hero .hp-btns, #hero .hp-trust,
  .hp-logo{
    opacity:1 !important;
    transform:none !important;
    clip-path:none !important;
    transition:none !important;
    animation:none !important;
    transition-delay:0ms !important;
  }
  /* pseudo-elements are never matched by the selector list above */
  #hero::before, .qsl-arm #hero::before{transform:none !important; transition:none !important}
  [data-parallax]{transform:none !important}
  .hp09-step::before{transform:none !important; transition:none !important}
  a.hp04__cell[href*="access-control"]::after{animation:none !important; display:none !important}
  .hp11-pin, .hp11__scan{display:none !important}
  .hp11__map > picture{transform:none !important; transition:none !important}
  .hp11__pins{transform:translate(-50%,-50%) !important; transition:none !important}
  .hp11__home-layer{transform:translate(-50%,-50%) !important; transition:none !important}
  /* the HOME BASE pulse goes; the static ring stays, so the marker still
     reads as "you are here" rather than as another city pin */
  .hp11-home__badge::after{display:none !important}
  .hpB__btn.gold.is-in{animation:none !important}
  .hp03-card,.hp13-choice,.hpA-route,.svc-card,.icard,
  .hp03-card__photo,.hp14-col a,.hp14__social a{transition:none !important}
  html{scroll-behavior:auto !important}
}

/* No JS: nothing may stay hidden. */
.no-js [data-reveal]{opacity:1 !important; transform:none !important}
.no-js .hp09-step::before{transform:none !important}
.no-js [data-reveal] > picture, .no-js [data-reveal] > img{clip-path:none !important; transform:none !important}
.no-js [data-grade] > picture, .no-js [data-grade] > img{filter:none !important; transform:none !important}
.no-js .qsl-display > span{opacity:1 !important; transform:scaleX(var(--sx)) !important}
.no-js #hero .hp-eyebrow, .no-js #hero .hp-h1, .no-js #hero .hp-line,
.no-js #hero .hp-services, .no-js #hero .hp-btns, .no-js #hero .hp-trust,
.no-js .hp-logo, .no-js #hero{opacity:1 !important; transform:none !important}
/* a JS failure must never leave the hero armed and invisible — the artwork
   layer needs saying separately, since `*` never matches a pseudo-element */
.no-js.qsl-arm #hero *{opacity:1 !important; transform:none !important}
.no-js #hero::before, .no-js.qsl-arm #hero::before{transform:none !important}


/* ══════════════════════════════════════════════════════════════
   DESKTOP STICKY NAV + SCROLL-TO-TOP (2026-08-24, client-requested)
   Both are chrome ADDED alongside the frozen design/motion system,
   not part of it — #site-header itself is never touched, resized,
   or repositioned. Desktop only (>=901px, matching the choreography
   breakpoint elsewhere in this file); both start fully hidden via
   plain CSS, so a no-JS visitor never sees either — no .no-js
   override needed, unlike the reveal system.
   ══════════════════════════════════════════════════════════════ */
.qsl-sticky,.qsl-totop{display:none}

@media (min-width:901px){
  .qsl-sticky{
    display:block;position:fixed;top:0;left:0;right:0;z-index:500;
    background:#001029;
    box-shadow:0 6px 22px rgba(0,0,0,.38);
    border-bottom:1px solid rgba(224,169,60,.3);
    opacity:0;visibility:hidden;pointer-events:none;
    transform:translateY(-8px);
    transition:opacity .32s var(--ease-io), transform .32s var(--ease-io),
               visibility 0s linear .32s;
  }
  .qsl-sticky.is-visible{
    opacity:1;visibility:visible;pointer-events:auto;transform:none;
    transition:opacity .32s var(--ease-io), transform .32s var(--ease-io);
  }
  .qsl-sticky__in{
    max-width:1536px;margin-inline:auto;
    display:flex;align-items:center;gap:32px;
    height:60px;padding:0 32px;
  }
  .qsl-sticky__logo{flex:none;display:block;line-height:0}
  .qsl-sticky__logo img{height:26px;width:auto;display:block}
  .qsl-sticky__links{
    display:flex;align-items:center;gap:22px;flex:1 1 auto;
    list-style:none;margin:0;padding:0;
  }
  .qsl-sticky__links a{
    font-family:'Roboto Condensed','Arial Narrow',sans-serif;
    font-weight:700;font-size:13px;letter-spacing:.05em;text-transform:uppercase;
    color:#fff;text-decoration:none;white-space:nowrap;
    transition:color .2s ease;
  }
  .qsl-sticky__links a:hover{color:#E9A415}
  .qsl-sticky__call{
    flex:none;display:flex;align-items:center;gap:9px;
    color:#fff;text-decoration:none;
  }
  .qsl-sticky__call svg{width:16px;height:16px;color:#E9A415;flex:none}
  .qsl-sticky__call span{
    display:flex;flex-direction:column;line-height:1.1;
    font-family:'Roboto Condensed','Arial Narrow',sans-serif;
    font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
    color:rgba(255,255,255,.7);
  }
  .qsl-sticky__call b{font-size:15px;font-weight:700;color:#E9A415;letter-spacing:0}
  .qsl-sticky__call:hover b{color:#fff}

  /* Anchor targets must clear the sticky bar once it's showing — the base
     104px offset (css/home.css) covers the full header; this adds the
     sticky bar's own 60px + a few px breathing room while `html.qsl-stuck`
     is set (toggled by js/motion.js in lockstep with .is-visible). */
  html.qsl-stuck{scroll-padding-top:172px}

  .qsl-totop{
    display:grid;place-items:center;position:fixed;right:28px;bottom:28px;z-index:480;
    width:46px;height:46px;border-radius:6px;
    background:#001029;border:1.5px solid rgba(224,169,60,.55);
    color:#E9A415;cursor:pointer;
    opacity:0;visibility:hidden;pointer-events:none;
    transform:translateY(10px);
    transition:opacity .28s var(--ease-io), transform .28s var(--ease-io),
               background .2s ease, border-color .2s ease,
               visibility 0s linear .28s;
  }
  .qsl-totop.is-visible{
    opacity:1;visibility:visible;pointer-events:auto;transform:none;
    transition:opacity .28s var(--ease-io), transform .28s var(--ease-io),
               background .2s ease, border-color .2s ease;
  }
  .qsl-totop svg{width:19px;height:19px;transition:transform .2s ease}
  .qsl-totop:hover{background:#0A1E3D;border-color:#E9A415}
  .qsl-totop:hover svg{transform:translateY(-2px)}
  .qsl-totop:focus-visible{outline:2px solid #E9A415;outline-offset:2px}
}

@media (prefers-reduced-motion: reduce){
  .qsl-sticky,.qsl-totop{transition:none !important}
}


/* ══════════════════════════════════════════════════════════════
   MOBILE SERVICE NAV + FLOATING CALL + SCROLL-TO-TOP
   (2026-08-24, client-requested — "Navigation + Floating CTA Upgrade")
   Homepage only, mobile-only (<=900px).

   REPLACES the previous full-width fixed CALL NOW / REQUEST SERVICE bar,
   which was removed at the client's request: it permanently consumed
   54px + safe-area of every viewport. Neither action was removed from
   the site — Call became the always-available floating button below, and
   Request Service moved into the page's own CTAs + the menu drawer.
   The nav's 4th slot changed from EMERGENCY to MENU for the same reason
   (the floating Call button is now the persistent emergency action).

     z-index stack (all comfortably under .mobile-menu's 2000):
       700  .qsl-mnav    (top)
       690  .qsl-mcall   (bottom-right, floating)
       680  .qsl-mtotop  (bottom-right, ABOVE .qsl-mcall — see its `bottom`)
     heights (real px; this is chrome, not part of the 1536 composition):
       .qsl-mnav    48px normal / 42px "compact" (scroll-down response)
       .qsl-mcall   58px circle
       .qsl-mtotop  46px
   ══════════════════════════════════════════════════════════════ */
.qsl-mnav,.qsl-mcall,.qsl-mtotop{display:none}

@media (max-width:900px){
  .qsl-mnav{
    display:flex;position:fixed;top:0;left:0;right:0;z-index:700;
    align-items:stretch;justify-content:space-between;
    height:48px;padding:0 8px;
    background:#000C1E;
    border-bottom:1px solid rgba(224,169,60,.45);
    box-shadow:0 6px 18px rgba(0,0,0,.42);
    opacity:0;visibility:hidden;pointer-events:none;
    transform:translateY(-6px);
    transition:opacity .26s var(--ease-io), transform .26s var(--ease-io),
               height .18s var(--ease-io), visibility 0s linear .26s;
  }
  .qsl-mnav.is-visible{
    opacity:1;visibility:visible;pointer-events:auto;transform:none;
    transition:opacity .26s var(--ease-io), transform .26s var(--ease-io),
               height .18s var(--ease-io);
  }
  /* §7: compact state may not go below 44px — it was 42px, which also
     dropped each item under the 44px interactive minimum. */
  .qsl-mnav.is-compact{height:44px}
  .qsl-mnav__item{
    position:relative;flex:1 1 auto;min-height:44px;
    display:flex;align-items:center;justify-content:center;gap:5px;
    min-width:0;padding:0 2px;
    background:none;border:0;
    font-family:'Roboto Condensed','Arial Narrow',sans-serif;
    font-weight:700;font-size:12px;letter-spacing:.05em;text-transform:uppercase;
    color:rgba(255,255,255,.92);text-decoration:none;
    transition:color .2s ease;
  }
  .qsl-mnav__item span{white-space:nowrap}
  .qsl-mnav__ic{
    width:15px;height:15px;flex:none;
    color:rgba(224,169,60,.72);
    transition:color .2s ease;
  }
  .qsl-mnav__item.is-active{color:#E9A415}
  .qsl-mnav__item.is-active .qsl-mnav__ic{color:#E9A415}
  .qsl-mnav__item:active{color:#E9A415}
  .qsl-mnav__item:focus-visible{outline:2px solid #E9A415;outline-offset:-3px;border-radius:3px}
  /* MENU is an ACTION, not a destination: it never takes the active state
     and never moves the indicator. A hairline separator sets it apart from
     the three service shortcuts without making it look like a 4th one. */
  .qsl-mnav__menu{
    flex:0 0 auto;padding-left:11px;margin-left:5px;cursor:pointer;
  }
  .qsl-mnav__menu::before{
    content:'';position:absolute;left:0;top:50%;transform:translateY(-50%);
    width:1px;height:20px;background:rgba(255,255,255,.22);
  }
  .qsl-mnav__menu .qsl-mnav__ic{width:17px;height:17px;color:#E9A415}
  /* geometry is set from the ACTIVE item's measured offsetLeft/Width in JS,
     never a fraction-of-N guess — the labels are not equal width. */
  .qsl-mnav__ind{
    position:absolute;left:0;bottom:0;height:3px;width:0;
    background:#E9A415;
    transform:translateX(0);
    transition:transform .3s var(--ease-io), width .3s var(--ease-io);
  }

  /* Anchor targets must clear the mobile nav once it's stuck — shared
     html.qsl-stuck class with the desktop sticky nav (mutually exclusive
     by viewport width), value redeclared per breakpoint. 48px bar + 16px
     breathing room, so a section heading can never land underneath it. */
  html.qsl-stuck{scroll-padding-top:64px}

  /* ── floating Call: the one-tap conversion, always reachable ──
     A real <a href="tel:"> so it dials directly — never opens a menu
     first. The intro pill (.is-intro) widens it ONCE per page visit to
     teach what the icon does, then collapses back to the circle; the
     width transition runs on max-width so the circle stays a true circle
     at rest. */
  .qsl-mcall{
    display:flex;align-items:center;justify-content:flex-start;
    position:fixed;right:18px;z-index:690;
    bottom:calc(18px + env(safe-area-inset-bottom));
    width:64px;height:64px;padding:0;
    border-radius:32px;overflow:hidden;
    background:#E9A415;color:#001029;text-decoration:none;
    /* Four layers, each doing a specific job, because this button has to
       stay legible over BOTH the white service sections and the navy
       ones: two dark drop shadows lift it off white, the hairline white
       ring separates it from navy, and the wide gold glow keeps its edge
       from dissolving into the gold accents in photography. */
    box-shadow:0 8px 22px rgba(0,0,0,.38),
               0 3px 8px rgba(0,0,0,.26),
               0 0 0 1px rgba(255,255,255,.16),
               0 0 22px rgba(233,164,21,.34);
    opacity:0;visibility:hidden;pointer-events:none;
    transform:translateY(10px) scale(.94);
    transition:opacity .28s var(--ease-io), transform .28s var(--ease-io),
               width .42s var(--ease-io), visibility 0s linear .28s;
  }
  .qsl-mcall.is-visible{
    opacity:1;visibility:visible;pointer-events:auto;
    transform:none;
    transition:opacity .28s var(--ease-io), transform .28s var(--ease-io),
               width .42s var(--ease-io);
  }

  /* ── the periodic reminder ──
     A ring that expands out of the button and fades, plus a barely-there
     swell, roughly every 6.5s. Deliberately NOT a bounce, shake or flash.

     Built entirely from box-shadow rather than a ::after ring because the
     button sets overflow:hidden (it has to, to clip the label while the
     pill collapses) — a pseudo-element ring would be cut off at the
     button's edge, whereas an element's own box-shadow is painted
     outside its box and is never clipped by its own overflow.

     Only runs once the intro pill has collapsed, so the two animations
     can never fight over `transform` or `box-shadow`. The ring spends
     ~0.9s of each cycle visible and the remaining ~5.6s at zero spread
     AND zero alpha — the 15% stop exists purely so it snaps back to its
     start size while invisible instead of visibly shrinking back. */
  @keyframes qsl-mcall-ping{
    0%{
      transform:scale(1);
      box-shadow:0 8px 22px rgba(0,0,0,.38),0 3px 8px rgba(0,0,0,.26),
                 0 0 0 1px rgba(255,255,255,.16),0 0 22px rgba(233,164,21,.34),
                 0 0 0 0 rgba(233,164,21,.5);
    }
    4%{transform:scale(1.05)}
    14%{
      transform:scale(1);
      box-shadow:0 8px 22px rgba(0,0,0,.38),0 3px 8px rgba(0,0,0,.26),
                 0 0 0 1px rgba(255,255,255,.16),0 0 22px rgba(233,164,21,.34),
                 0 0 0 17px rgba(233,164,21,0);
    }
    15%,100%{
      transform:scale(1);
      box-shadow:0 8px 22px rgba(0,0,0,.38),0 3px 8px rgba(0,0,0,.26),
                 0 0 0 1px rgba(255,255,255,.16),0 0 22px rgba(233,164,21,.34),
                 0 0 0 0 rgba(233,164,21,0);
    }
  }
  .qsl-mcall.is-visible:not(.is-intro){
    animation:qsl-mcall-ping 6.5s var(--ease-io) 1.1s infinite;
  }
  /* §7 — the ring is an invitation; once the visitor has engaged the
     button it has served its purpose and must not keep pulsing. */
  .qsl-mcall.is-engaged{animation:none !important}

  .qsl-mcall svg{
    width:27px;height:27px;flex:none;
    margin-left:18.5px;margin-right:18.5px; /* centres the icon in the 64px circle */
  }
  .qsl-mcall__label{
    flex:none;
    max-width:0;opacity:0;
    padding-right:0;
    font-family:'Roboto Condensed','Arial Narrow',sans-serif;
    font-weight:700;font-size:14px;letter-spacing:.05em;text-transform:uppercase;
    white-space:nowrap;overflow:hidden;
    transition:max-width .42s var(--ease-io), opacity .3s var(--ease-io),
               padding-right .42s var(--ease-io);
  }
  /* 18.5 + 27 + 18.5 (the resting circle) + 114 (label box) = 178 */
  .qsl-mcall.is-intro{width:178px}
  .qsl-mcall.is-intro .qsl-mcall__label{max-width:114px;opacity:1;padding-right:20px}
  .qsl-mcall:active{background:#F4B733}
  .qsl-mcall:focus-visible{outline:2px solid #fff;outline-offset:3px}

  /* ── scroll-to-top: sits directly ABOVE the Call button ──
     Held at 40px while Call grew 58 -> 64px, which widens the hierarchy
     gap deliberately: the gold Call circle is the primary action, this
     outlined arrow is utility, and the brief asks for Call to carry the
     greater visual priority.
     Geometry, so the two can never collide: Call is 64px tall sitting
     18px off the bottom, so its top edge is at 82px; +12px breathing
     room puts this button's bottom edge at 94px. Same safe-area inset,
     and right:27px centres this 40px button on the 64px one's axis
     (18 + 64/2 = 50 from the right; 27 + 40/2 = 47 — close enough that
     they read as one stack, and matching the previous optical offset).
     Right edges are intentionally NOT flush; the axis alignment is what
     the eye follows. */
  .qsl-mtotop{
    display:grid;place-items:center;position:fixed;right:27px;z-index:680;
    bottom:calc(94px + env(safe-area-inset-bottom));
    width:40px;height:40px;border-radius:20px;
    background:#001029;border:1.5px solid rgba(224,169,60,.5);
    color:rgba(233,164,21,.9);
    box-shadow:0 4px 12px rgba(0,0,0,.3);
    opacity:0;visibility:hidden;pointer-events:none;
    transform:translateY(8px);
    transition:opacity .26s var(--ease-io), transform .26s var(--ease-io),
               visibility 0s linear .26s;
  }
  .qsl-mtotop.is-visible{
    opacity:1;visibility:visible;pointer-events:auto;transform:none;
    transition:opacity .26s var(--ease-io), transform .26s var(--ease-io);
  }
  /* §7 — yields when it would sit on interactive content (form fields,
     service-area links, accordion summaries, the reviews CTA, footer
     controls). Visual only: it keeps its slot and returns the moment the
     overlap clears, so nothing jumps. */
  .qsl-mtotop.is-yielded{opacity:0;visibility:hidden;pointer-events:none}
  .qsl-mtotop svg{width:17px;height:17px}
  .qsl-mtotop:active{background:#0A1E3D}
  .qsl-mtotop:focus-visible{outline:2px solid #E9A415;outline-offset:2px}

  /* The old full-width-bar clearance is gone (that was the point), but the
     page's LAST row still needs to clear the floating stack: at the very
     bottom of the document the controls come to rest permanently, and the
     footer's social icons were sitting underneath the Call button.
     Everywhere else an overlap is transient — the content scrolls past —
     but here it never resolves, so this one band is reserved.
     Call button top edge = 18 + 64 = 82px above the viewport bottom;
     +10px breathing room. This footer bar is shared by all 27 pages, so
     the reservation lands sitewide the moment the controls do. */
  .hp14__bar{padding-bottom:calc(92px + env(safe-area-inset-bottom)) !important}
}

/* the three service shortcuts get tighter on the narrowest phones so the
   4-slot bar never wraps, clips or cramps its touch targets at 320px */
@media (max-width:359px){
  .qsl-mnav{padding:0 5px}
  .qsl-mnav__item{font-size:11px;letter-spacing:.03em;gap:4px}
  .qsl-mnav__ic{width:14px;height:14px}
  .qsl-mnav__menu{padding-left:8px;margin-left:3px}
  /* Call steps 64 -> 58px on the narrowest phones (still well over the
     48px minimum tap target) and the pill narrows with it so it can
     never reach the left edge of a 320px screen. */
  .qsl-mcall{width:58px;height:58px;border-radius:29px}
  .qsl-mcall svg{width:24px;height:24px;margin-left:17px;margin-right:17px}
  .qsl-mcall.is-intro{width:166px}
  .qsl-mcall.is-intro .qsl-mcall__label{max-width:108px}
  /* 18 + 58 = 76px top edge, +12px gap */
  .qsl-mtotop{bottom:calc(88px + env(safe-area-inset-bottom))}
  .hp14__bar{padding-bottom:calc(86px + env(safe-area-inset-bottom)) !important}
}

@media (prefers-reduced-motion: reduce){
  .qsl-mnav,.qsl-mcall,.qsl-mtotop,.qsl-mcall__label{transition:none !important}
  /* the intro pill is a teaching animation, not information — with
     reduced motion the button simply appears as its resting circle */
  .qsl-mcall.is-intro{width:64px}
  .qsl-mcall.is-intro .qsl-mcall__label{max-width:0;opacity:0;padding-right:0}
  /* and the periodic ring never runs at all — the button keeps its
     static four-layer shadow, which is what carries its visibility */
  .qsl-mcall.is-visible:not(.is-intro){animation:none !important}
}

@media (prefers-reduced-motion: reduce) and (max-width:359px){
  .qsl-mcall.is-intro{width:58px}
}
