/*
 * Fixes layered over the mirrored WordPress markup.
 *
 * mirror/ stays a byte-faithful clone -- these are injected by build/serve.mjs
 * instead of edited in, so the clone keeps its value as a record of what the
 * live site actually serves. Everything here is plain CSS and is meant to be
 * pasted into WordPress (Elementor -> Site Settings -> Custom CSS, or the
 * theme's Additional CSS) when the same fix is made live.
 */

/* ---------------------------------------------------------------------------
 * 1. Service grid: one icon size for all six cards.
 *
 * The six service cards use the same Elementor image-box markup, but the source
 * artwork is not the same size: four icons are 60x60 and two -- roof replacement
 * (ui-image-5320) and roof inspections (ui-image-5322) -- are 330x330. Nothing
 * constrained them, so those two rendered at 330px, swallowing the photograph
 * behind them and making their cards five times taller than the rest.
 *
 * Constraining the icon rather than patching the two files keeps every card the
 * same height whatever artwork is dropped in later.
 * ------------------------------------------------------------------------- */
/* Scoped to the two offending images by their WordPress attachment ids, NOT to
 * .elementor-image-box-wrapper generally -- the Service Area town cards use the
 * same widget, and a blanket 60px rule shrinks all thirteen town photographs to
 * thumbnails. Learned the hard way; do not widen this selector.
 *
 *   ui-image-5320  roof-replacement-icon.webp  (330x330 source)
 *   ui-image-5322  roof-inspection-icon.webp   (330x330 source)
 *
 * The other four service icons are already 60x60 and are left untouched, so all
 * six cards end up the same height.
 */
.elementor-image-box-img img.ui-image-5320,
.elementor-image-box-img img.ui-image-5322 {
  width: 60px !important;
  height: 60px !important;
  max-width: 60px !important;
  object-fit: contain;
  display: block;
  margin-inline: auto;
}

/* ---------------------------------------------------------------------------
 * 2. Past Work strip: a genuinely endless, full-bleed crawl.
 *
 * This no longer runs on Swiper. Elementor's carousel is loop-mode Swiper,
 * which fakes an infinite loop by cloning slides and then silently teleporting
 * the wrapper back to the start (loopFix). That teleport is the jump, and no
 * combination of speed, freeMode or slidesPerGroup removes it -- it is how the
 * mode works.
 *
 * So overrides.js destroys the Swiper instance and rebuilds the strip as a
 * plain track holding N whole copies of the photographs, animated from 0 to
 * -50% of its own width. Half the track is exactly the copies in the first
 * half, so the frame at 100% is pixel-identical to the frame at 0% and the
 * loop has no seam at all. It is also cheaper: one compositor-driven
 * animation, no per-frame JavaScript.
 *
 * Pausing is then just animation-play-state, which stops on the exact frame
 * under the pointer rather than having to interrupt a transition by hand.
 * ------------------------------------------------------------------------- */
.itlr-marquee {
  --itlr-img-w: 299px;   /* 35% down from the 460px Elementor rendered */
  --itlr-img-h: 307px;
  --itlr-gap: 26px;

  width: 100vw !important;
  max-width: 100vw !important;
  margin-left: calc(50% - 50vw) !important;
  margin-right: calc(50% - 50vw) !important;
  overflow: hidden;
  /* Room for a hovered photograph to grow to 1.25 and to sit at an angle
     without being clipped by the container it is scrolling inside. */
  padding-block: 46px;
  /* Fade both ends rather than cutting them. */
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 6%, #000 94%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 6%, #000 94%, transparent 100%);
}

.itlr-marquee__track {
  display: flex;
  align-items: center;
  width: max-content;
  /* No `gap`. Spacing goes on the items as margin-right so that every item
     contributes exactly the same pitch -- a gap is dropped between the two
     halves as well, which makes -50% land a few pixels off one whole copy and
     puts the seam straight back. Duration is set from JS: distance / speed. */
  animation-name: itlr-crawl;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  will-change: transform;
}

@keyframes itlr-crawl {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

/* Hovering anywhere on the strip stops it dead. Nothing dims: the ask was to
   stop the carousel, not to style the other photographs. */
.itlr-marquee:hover .itlr-marquee__track,
.itlr-marquee:focus-within .itlr-marquee__track {
  animation-play-state: paused;
}

.itlr-marquee__item {
  position: relative;
  flex: 0 0 auto;
  width: var(--itlr-img-w);
  height: var(--itlr-img-h);
  margin-right: var(--itlr-gap);
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 10px 26px rgb(0 0 0 / .38);
  outline: 1px solid rgb(255 255 255 / .10);
  outline-offset: -1px;
  transition: transform .45s cubic-bezier(.22, 1, .36, 1),
              box-shadow .45s cubic-bezier(.22, 1, .36, 1);
}

.itlr-marquee__item img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}

/* Alternating tilt. The class is assigned in JS from the index within one copy
   of the set, NOT with :nth-child -- there are seven photographs, so an odd
   count means :nth-child(odd) would land on different photographs in each copy
   and the tilt pattern would visibly change at the loop point. */
.itlr-marquee__item--cw  { transform: rotate(2.5deg); }
.itlr-marquee__item--ccw { transform: rotate(-2.5deg); }

/* Hovered: straighten up and grow. z-index lifts it over its neighbours, which
   otherwise clip the corners of the enlarged photograph. */
.itlr-marquee__item:hover {
  transform: rotate(0deg) scale(1.25);
  z-index: 3;
  box-shadow: 0 22px 50px rgb(0 0 0 / .55);
  outline-color: rgb(255 255 255 / .24);
}

@media (max-width: 1024px) {
  .itlr-marquee { --itlr-img-w: 250px; --itlr-img-h: 257px; --itlr-gap: 20px; padding-block: 38px; }
}

@media (max-width: 767px) {
  .itlr-marquee { --itlr-img-w: 200px; --itlr-img-h: 206px; --itlr-gap: 16px; padding-block: 30px; }
}

/* Someone who has asked for less motion should not be handed an endless one.
   The strip stays, and stays browsable -- it simply does not move. */
@media (prefers-reduced-motion: reduce) {
  .itlr-marquee__track { animation: none !important; }
  .itlr-marquee__item:hover { transform: rotate(0deg); }
}

/* ---------------------------------------------------------------------------
 * 2b. The estimate prompt under the Past Work strip.
 *
 * Injected by overrides.js. It sits directly beneath the photographs on purpose:
 * the strip is the proof, and the ask belongs immediately after the proof rather
 * than at the bottom of the page. Styled against the section's own dark
 * background using the Elementor globals, so it re-themes with the site.
 * ------------------------------------------------------------------------- */
.itlr-cta {
  position: relative;
  z-index: 2;
  max-width: 760px;
  margin: 52px auto 8px;
  padding: 40px 32px 44px;
  text-align: center;
  /* A self-contained panel, not bare text on the section.
     The section below this one carries a bright cyan band that overlaps
     upwards, so the prompt lands half on near-black and half on cyan. Bare
     white text and a cyan button disappear across that seam. Giving it its own
     dark surface makes it readable wherever the seam happens to fall, and at
     any viewport -- the overlap moves with the layout. */
  background: var(--e-global-color-secondary, #14161E);
  border: 1px solid rgb(255 255 255 / .09);
  border-radius: 24px;
  box-shadow: 0 24px 60px rgb(0 0 0 / .38);
}

.itlr-cta__title {
  margin: 0 0 14px;
  font-size: clamp(1.5rem, 1.1rem + 1.6vw, 2.25rem);
  line-height: 1.2;
  font-weight: 700;
  color: #fff;
}

.itlr-cta__body {
  margin: 0 auto 28px;
  max-width: 56ch;
  font-size: 1.0625rem;
  line-height: 1.65;
  color: rgb(255 255 255 / .82);
}

.itlr-cta__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  justify-content: center;
}

.itlr-cta__btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 15px 30px;
  border-radius: 999px;
  font-weight: 700;
  font-size: 1rem;
  line-height: 1;
  text-decoration: none;
  transition: transform .2s ease, background-color .2s ease, box-shadow .2s ease;
}

.itlr-cta__btn--primary {
  background: var(--e-global-color-primary, #00B6F1);
  color: #fff;
  box-shadow: 0 8px 24px rgb(0 182 241 / .32);
}

.itlr-cta__btn--secondary {
  border: 2px solid rgb(255 255 255 / .35);
  color: #fff;
}

.itlr-cta__btn:hover {
  transform: translateY(-2px);
}

.itlr-cta__btn--primary:hover {
  background: #00a2d8;
  color: #fff;
}

.itlr-cta__btn--secondary:hover {
  border-color: #fff;
  background: rgb(255 255 255 / .08);
}

.itlr-cta__btn:focus-visible {
  outline: 3px solid #fff;
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .itlr-cta__btn:hover {
    transform: none;
  }
}

/* ---------------------------------------------------------------------------
 * 3. Former employee: suppress his slides before the browser fetches them.
 *
 * The team carousel gives each crew member a slide with their portrait as a CSS
 * background and a second portrait on hover. Three Elementor elements carry his
 * pair -- 6d5023c on the homepage, 56194a8 and 8ac57c0 on /about-us/ -- and each
 * was checked against the stylesheets to be sure it holds no other crew
 * member's photograph.
 *
 * overrides.js removes these slides from the DOM so Swiper recounts, but that
 * runs a few hundred milliseconds after load, by which time the browser has
 * already fetched the background and shown his face. Nulling the image here
 * means the request is never made: a computed background-image of none fetches
 * nothing. Verified by watching the network for SEMI7879 and SEMI7885.
 *
 * :has() hides the whole slide immediately where supported; the JS removal is
 * the fallback and the thing that keeps the carousel's slide count honest.
 *
 * 13fd32f (homepage) and 6ac0876 (/about-us/) are a second crew member, taken
 * off at the client's request on 27 September 2026 (SEMI8001, hover SEMI7982).
 * ------------------------------------------------------------------------- */
.elementor-element-56194a8,
.elementor-element-6d5023c,
.elementor-element-8ac57c0,
.elementor-element-13fd32f,
.elementor-element-6ac0876,
.elementor-element-56194a8:hover,
.elementor-element-6d5023c:hover,
.elementor-element-8ac57c0:hover,
.elementor-element-13fd32f:hover,
.elementor-element-6ac0876:hover,
.elementor-element-56194a8::before,
.elementor-element-6d5023c::before,
.elementor-element-8ac57c0::before,
.elementor-element-13fd32f::before,
.elementor-element-6ac0876::before,
.elementor-element-56194a8:hover::before,
.elementor-element-6d5023c:hover::before,
.elementor-element-8ac57c0:hover::before,
.elementor-element-13fd32f:hover::before,
.elementor-element-6ac0876:hover::before {
  background-image: none !important;
}

.swiper-slide:has(.elementor-element-56194a8),
.swiper-slide:has(.elementor-element-6d5023c),
.swiper-slide:has(.elementor-element-8ac57c0),
.swiper-slide:has(.elementor-element-13fd32f),
.swiper-slide:has(.elementor-element-6ac0876) {
  display: none !important;
}

/* ---------------------------------------------------------------------------
 * 4. Certification strip: one band instead of five white rectangles.
 *
 * Elementor sets `background: #fff` on each .swiper-slide of the badge
 * carousel, so on a dark section the five logos arrive as five ragged white
 * rectangles of differing widths -- they read as screenshots someone pasted in
 * rather than as a trust bar.
 *
 * The white cannot simply be removed. The marks are transparent PNGs and most
 * of them do read on dark, but BBB's "Start With Trust" strapline and Google's
 * "Reviews / 5.0" line are dark grey and disappear entirely against the navy.
 * Recolouring is not an option either: these are third-party brand marks, and
 * Google's in particular may not be restyled.
 *
 * So the white stays and is made deliberate -- one rounded band behind the
 * whole strip, with the logos scaled to a common height so the row reads as
 * one object. Applies to all five pages carrying .image-slider.
 * ------------------------------------------------------------------------- */
.image-slider .elementor-image-carousel-wrapper {
  background: #fff;
  border-radius: 18px;
  padding: 16px 12px;
  box-shadow: 0 12px 34px rgb(0 0 0 / .22);
}

/* The per-slide rectangles are what made the row look ragged. */
.image-slider .swiper-slide {
  background: transparent !important;
  padding: 0 !important;
  display: flex !important;
  align-items: center;
  justify-content: center;
}

/* A common optical height. The source files run from 44px to 65px tall, which
   is why some marks looked twice the weight of their neighbours. */
.image-slider .swiper-slide-inner,
.image-slider figure.swiper-slide-inner {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
}

.image-slider .swiper-slide-image {
  max-height: 46px;
  width: auto !important;
  max-width: 82%;
  height: auto;
  object-fit: contain;
  margin-inline: auto;
}

/* The arrows sat over the band once it had a defined edge. */
.image-slider .elementor-swiper-button,
.image-slider .swiper-button-next,
.image-slider .swiper-button-prev {
  display: none !important;
}

@media (max-width: 767px) {
  .image-slider .elementor-image-carousel-wrapper { border-radius: 14px; padding: 12px 8px; }
  .image-slider .swiper-slide-image { max-height: 38px; }
}

/* ---------------------------------------------------------------------------
 * 5. /home/ hero: the lightning still, animated.
 *
 * The lightning is an <img> inside .banner-bg-img, not a CSS background -- the
 * stylesheet rule that looks like it paints it is invalid (the url() carries a
 * stray `fetchpriority(high)` inside the value, so the whole declaration is
 * dropped). overrides.js wraps that img and lays a looping video over it.
 *
 * The img stays in place underneath rather than being removed: it holds the
 * layout box the video is sized against, and it is what remains visible if
 * autoplay is refused, which mobile browsers do on Low Power Mode.
 * ------------------------------------------------------------------------- */
.itlr-hero-videowrap {
  position: relative;
  display: block;
  line-height: 0;
}

.itlr-hero-videowrap > img {
  display: block;
  width: 100%;
  height: auto;
}

.itlr-hero-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
  /* Fades up once the first frame is decoded, so a slow connection shows the
     still rather than a black rectangle. */
  opacity: 0;
  transition: opacity .6s ease;
}

.itlr-hero-video[data-ready="1"] { opacity: 1; }

/* Someone who asked for less motion keeps the still image; overrides.js never
   builds the video at all, so this is only belt and braces. */
@media (prefers-reduced-motion: reduce) {
  .itlr-hero-video { display: none; }
}

/* ---------------------------------------------------------------------------
 * 6. Service-area town cards: lift and clear on hover.
 *
 * Thirteen cards in one .service-areas section, on six pages (/, /home/,
 * /services/, /service-area/, /roof-types/ and the duplicate home design), all
 * carrying the hand-written .city-box class -- so one rule covers every copy.
 *
 * The "tint" over each photograph is not an overlay. There is no ::before, no
 * background and no filter anywhere in the card; the images themselves are
 * dark, with channel means around 19/23/28 and a blue lean. So it cannot be
 * removed by deleting a layer -- hovering brightens the photograph back up
 * instead, which is the same result and touches no markup.
 *
 * The whole wrapper scales, not just the image, so the town name grows with the
 * photograph rather than staying pinned at its original size.
 * ------------------------------------------------------------------------- */
.service-areas .city-box {
  /* Already position:relative from Elementor; the z-index needs a stacking
     context to raise against, and the transition has to live on the resting
     state or the card snaps back instantly when the pointer leaves. */
  transition: z-index 0s linear .01s;
}

.service-areas .city-box .elementor-image-box-wrapper {
  transition: transform .45s cubic-bezier(.22, 1, .36, 1),
              filter .45s ease,
              box-shadow .45s ease;
  will-change: transform;
}

.service-areas .city-box img {
  transition: filter .45s ease;
}

/* Gated on a real pointer: on a touch screen :hover latches after a tap and
   leaves one card stuck at 1.5 until you tap elsewhere. */
@media (hover: hover) {
  .service-areas .city-box:hover,
  .service-areas .city-box:focus-within {
    z-index: 20;
  }

  .service-areas .city-box:hover .elementor-image-box-wrapper,
  .service-areas .city-box:focus-within .elementor-image-box-wrapper {
    transform: scale(1.5);
    box-shadow: 0 26px 60px rgb(0 0 0 / .55);
  }

  /* Lifting the baked-in darkness. Brightness alone goes grey and washed out,
     so a little saturation and contrast come with it. */
  .service-areas .city-box:hover img,
  .service-areas .city-box:focus-within img {
    filter: brightness(1.45) saturate(1.12) contrast(1.04);
  }
}

/* Keyboard users get the same reveal without the section jumping around. */
.service-areas .city-box a:focus-visible {
  outline: 3px solid var(--e-global-color-primary, #00B6F1);
  outline-offset: 3px;
}

/* Reduced motion keeps the reveal -- it is information, not decoration -- and
   drops the movement, which is the part that was objected to. */
@media (prefers-reduced-motion: reduce) {
  .service-areas .city-box:hover .elementor-image-box-wrapper,
  .service-areas .city-box:focus-within .elementor-image-box-wrapper {
    transform: none;
  }
  .service-areas .city-box .elementor-image-box-wrapper,
  .service-areas .city-box img { transition: filter .2s ease; }
}

/* ---------------------------------------------------------------------------
 * 7. The lightning video on the CSS-background banner (every page but /home/).
 *
 * On /home/ the artwork is an <img>, so the video sits inside a wrapper and
 * inherits its box. Everywhere else it is a background-image on the banner
 * section itself, so the video is inserted as that section's first child and
 * has to be layered by hand: above the section's own background, below the
 * decorative overlay and below the content.
 *
 * The z-index bumps are scoped to sections that actually received a video, so
 * no other Elementor section's stacking is touched.
 * ------------------------------------------------------------------------- */
[data-itlr-herovid="1"] > .itlr-hero-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
  z-index: 0;
  opacity: 0;
  transition: opacity .6s ease;
}

[data-itlr-herovid="1"] > .itlr-hero-video[data-ready="1"] { opacity: 1; }

/* Elementor's decorative overlay and the content both have to stay on top. */
[data-itlr-herovid="1"] > .elementor-background-overlay { z-index: 1; }

[data-itlr-herovid="1"] > .elementor-container,
[data-itlr-herovid="1"] > .e-con-inner,
[data-itlr-herovid="1"] > .elementor-column-gap-default {
  position: relative;
  z-index: 2;
}

@media (prefers-reduced-motion: reduce) {
  [data-itlr-herovid="1"] > .itlr-hero-video { display: none; }
}

/* ---------------------------------------------------------------------------
 * 8. /past-work/ gallery: a placeholder while an item is still loading.
 *
 * Elementor's own lazyload leaves items with no background at all, which is a
 * hole in the grid rather than a loading state. overrides.js loads them
 * properly; this just means the half-second before it does looks deliberate.
 * ------------------------------------------------------------------------- */
.e-gallery-image[data-thumbnail] {
  background-color: rgb(255 255 255 / .06);
  background-size: cover;
  background-position: center;
}

/* ---------------------------------------------------------------------------
 * 9. The map: footer column, and the contact page.
 *
 * The footer row is a four-column flex container capped at Elementor's 1170px,
 * and the four columns already fill it exactly (351+234+234+351). So the map
 * cannot simply be dropped in -- the row is widened to take the space that was
 * sitting empty either side of the footer, and the columns give up a little
 * width to make room. They are flex: 0 1 auto, so they shrink rather than
 * overflow.
 *
 * Below 1200px there is no spare width to take, so the map stops being a column
 * and becomes a full-width band under the four.
 * ------------------------------------------------------------------------- */
.itlr-footer-map {
  display: block;
  min-width: 0;
}

.itlr-footer-map .itlr-map-frame {
  display: block;
  width: 100%;
  height: 100%;
  min-height: 240px;
  border: 0;
  border-radius: 14px;
  /* The embed is a bright daylight map on a near-white footer; a hairline stops
     it floating. */
  box-shadow: 0 6px 20px rgb(0 0 0 / .12);
}

@media (min-width: 1200px) {
  /* Take back the page margin the footer was leaving empty, without letting the
     row outgrow the viewport.
     Both the row and every boxed ancestor up to the footer section: a flex row
     cannot grow past its parent, and Elementor caps that parent at 1170px, so
     releasing only the row squeezed the four columns instead of widening
     anything. !important because Elementor's own cap is a three-class selector
     and this is one attribute. */
  [data-itlr-map="1"],
  [data-itlr-mapwide="1"] {
    max-width: min(1560px, calc(100vw - 60px)) !important;
    width: auto !important;
  }

  .itlr-footer-map {
    flex: 0 0 clamp(260px, 22%, 380px);
    margin-left: clamp(20px, 2.4vw, 46px);
    /* Same height as the columns beside it, whatever they come to. */
    align-self: stretch;
  }
}

@media (max-width: 1199px) {
  /* Below this there is no spare page margin to take, so the map stops being a
     column. The row has to be allowed to wrap or a 100% basis just gets shrunk
     back into the line by flex. */
  [data-itlr-map="1"] { flex-wrap: wrap; }

  .itlr-footer-map {
    flex: 1 0 100%;
    width: 100%;
    margin-top: 34px;
  }
  .itlr-footer-map .itlr-map-frame { height: 260px; min-height: 0; }
}

/* The contact page's own map, repointed at the pinned embed by overrides.js.
   Only the corners are touched -- its size is Elementor's. */
.itlr-map-frame--page {
  border: 0;
  border-radius: 14px;
}

/* ---------------------------------------------------------------------------
 * 10. The YouTube embed: 16:9 at every width.
 *
 * /services/, /service-area/ and /roof-types/ carry the same embed, written by
 * hand into an Elementor HTML widget with width="560" height="500" on the tag.
 * The width attribute never binds, because the theme already ships
 * `.elementor iframe { max-width: 100%; width: 100% }` and the frame therefore
 * fits its column at every width. The height attribute does bind: nothing in
 * the theme gives these frames a height, so the box stays 500px tall however
 * wide the column gets. The rendered shape swings from 0.68:1 on a 375px phone
 * to 2.34:1 on a 1440px desktop, and YouTube letterboxes its 16:9 picture
 * inside whichever of those it is handed -- pillarboxed on the phone, thick
 * bands top and bottom on the desktop. Neither ever shows the video full size.
 *
 * aspect-ratio settles both ends at once, but only once the height attribute
 * stops applying, which is what height:auto is for. A presentational attribute
 * maps to its property at the lowest priority there is, so one plain
 * declaration releases it.
 *
 * Scoped to the YouTube frames rather than to iframes generally, because the
 * footer and service-area Google Maps embeds are iframes too and are not 16:9,
 * and the Tag Manager noscript frame is a 0x0 pixel that has to stay 0x0. One
 * class plus an attribute selector outscores `.elementor iframe`, which is one
 * class and one type, so this wins on specificity alone and needs no
 * !important and no particular position in the cascade.
 * ------------------------------------------------------------------------- */
.elementor-widget-html iframe[src*="youtube.com/embed"] {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
}

/* ---------------------------------------------------------------------------
 * 11. The two focus rings the theme deletes.
 *
 * Most of the outline:none in the mirrored CSS is harmless: it is either
 * guarded by :not(:focus-visible), or it sits on something a keyboard cannot
 * reach anyway (the Essential Addons load-more button is inside a wrapper the
 * theme hides with display:none !important, and the accordion tab titles carry
 * role="button" but no tabindex, so nothing can focus them). Two rules do land
 * on something reachable, on all 424 pages:
 *
 *   .hfe-has-submenu-container a.hfe-menu-item:focus { outline: 0 }
 *       the five footer navigation links that own a submenu: Services, Roof
 *       Types, About Us, Careers and Service Area. The links either side of
 *       them are untouched and keep the browser's own ring, so tabbing along
 *       the footer menu the highlight disappears for five stops and comes back.
 *
 *   body .get-form-banner.bnr-form form .forminator-row.forminator-row-last
 *   button.forminator-button.forminator-button-submit { outline: none }
 *       the Submit button on the banner form. This one is not scoped to :focus
 *       at all, so there is no state in which a ring can appear on it.
 *
 * Restored on :focus-visible, not on :focus, so a mouse click still leaves no
 * ring behind. That is almost certainly what the theme's authors were trying
 * to achieve with outline:none back when :focus-visible was not available.
 *
 * The colours are the site's own Elementor globals with the literals as
 * fallbacks, for when this is pasted somewhere the variables are not defined.
 * Primary cyan is the ring already used on the town cards in section 6, on the
 * team cards and on the careers CTA. The Submit button is filled with that
 * same cyan (#00B6F1, set !important by the rule that blanks its outline), so
 * a cyan ring on it would be invisible against its own background; it takes
 * the dark secondary instead, the way the gate button below does.
 *
 * Both selectors deliberately restate enough of the original to outrank it.
 * The nav rule scores three classes and one type and the Submit rule six
 * classes and three types, which is too much for a short selector to beat, and
 * winning on specificity is steadier here than winning on source order.
 * ------------------------------------------------------------------------- */
.hfe-nav-menu .hfe-has-submenu-container a.hfe-menu-item:focus-visible {
  outline: 3px solid var(--e-global-color-primary, #00B6F1);
  outline-offset: 3px;
}

body .get-form-banner.bnr-form form .forminator-row.forminator-row-last button.forminator-button.forminator-button-submit:focus-visible {
  outline: 3px solid var(--e-global-color-secondary, #14161E);
  outline-offset: 3px;
}

/* ---------------------------------------------------------------------------
 * The "Start the build" gate -- homepage only.
 *
 * Modelled on the Eye Candy Customs warehouse gate, with one deliberate
 * difference: nothing here darkens the footage. ECC paints a near-black ground
 * and a top-to-bottom black gradient over its still. This does not. The ground
 * is white, there is no scrim, and the button earns its contrast from a solid
 * brand-cyan chip rather than from dimming the picture behind it.
 *
 * Full-bleed cover at every width, including portrait phones. The clip is 16:9
 * against a viewport of roughly 0.46:1, so a phone sees about the middle
 * quarter of the frame -- but the footage was shot for that: the prompt held
 * the house centred with all the action in the central third. Checked against
 * the closing frame, a phone still gets the new roof, the stone, the siding
 * and the front door. Letterboxing onto white was the alternative and it read
 * as a small strip marooned in a lot of nothing.
 * ------------------------------------------------------------------------ */
#itlr-build-gate {
  position: fixed;
  inset: 0;
  z-index: 99999;
  background: #ffffff;
  opacity: 1;
  transition: opacity .7s ease;
}
#itlr-build-gate[data-state="done"] { opacity: 0; pointer-events: none; }
/* The dialog takes focus on open (see overrides.js); it is a full-bleed panel,
   so a ring around it would just outline the viewport. */
#itlr-build-gate:focus { outline: none; }

#itlr-build-gate .itlr-gate-media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
}
#itlr-build-gate .itlr-gate-video { opacity: 0; transition: opacity .35s ease; }
#itlr-build-gate[data-state="playing"] .itlr-gate-video { opacity: 1; }
#itlr-build-gate[data-state="playing"] .itlr-gate-poster {
  opacity: 0;
  transition: opacity .35s ease;
}

#itlr-build-gate .itlr-gate-ui {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 22px;
  padding: 24px;
  text-align: center;
  transition: opacity .3s ease;
}
#itlr-build-gate[data-state="playing"] .itlr-gate-ui {
  opacity: 0;
  pointer-events: none;
}

/* Gate CTA -- refined capsule. Replaces every previous rule for this button:
   the flat slab, the 6px white ring, the hard outline and the white pulse are
   all gone, so appearance is restated in full below. */

#itlr-build-gate .itlr-gate-btn {
  -webkit-appearance: none;
  appearance: none;
  margin: 0;
  border: 0;
  text-decoration: none;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  animation: none; /* clears the old white-ring pulse if it is still cascading */

  /* Flex, so the chevron is a sibling of the (anonymous) text item and stays
     optically centred whether the label sets on one line or two. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .62em;

  /* The premium move is spacing, not size: 15px caps tracked out at .15em read
     as considered; 34px caps read as a shout. */
  font-family: "Exo 2", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-weight: 600;
  font-size: clamp(13px, 1.05vw, 15px);
  text-transform: uppercase;
  letter-spacing: .15em;
  line-height: 1.45;
  text-align: center;
  text-wrap: balance;
  white-space: normal;

  /* 43 tracked characters run ~600px wide with the chrome. The cap keeps it on
     one line from roughly 760px up, and lets it break to a balanced two lines
     below that -- never three. */
  max-width: min(86vw, 640px);
  min-height: 48px; /* touch target */
  padding: clamp(14px, 1.35vw, 18px) clamp(24px, 2.5vw, 36px);
  border-radius: 999px;

  /* Ink on brand cyan. The white wash only lightens the fill, so the measured
     ratio is a floor, never a ceiling. */
  color: var(--e-global-color-secondary, #14161E);
  background-color: var(--e-global-color-primary, #00B6F1);
  background-image: linear-gradient(180deg, rgba(255, 255, 255, .16), rgba(255, 255, 255, 0) 58%);

  /* Separation without a scrim: a 1px ink rim to hold the edge against pale
     plywood, a catch-light along the top, and three ambient shadow layers that
     fall off the way a real object's would -- tight and dark near the edge,
     wide and soft underneath. */
  box-shadow:
    0 0 0 1px rgba(20, 22, 30, .18),
    inset 0 1px 0 rgba(255, 255, 255, .38),
    0 2px 4px rgba(20, 22, 30, .18),
    0 10px 22px -8px rgba(20, 22, 30, .42),
    0 26px 48px -22px rgba(20, 22, 30, .5);
}

/* Chevron drawn from two borders -- no glyph, no icon font, no request. */
#itlr-build-gate .itlr-gate-btn::after {
  content: "";
  flex: 0 0 auto;
  width: .42em;
  height: .42em;
  border-top: 1.5px solid currentColor;
  border-right: 1.5px solid currentColor;
  transform: rotate(45deg);
  /* letter-spacing leaves a trailing space after the last glyph; this takes it
     back so the chevron sits where the eye expects it. */
  margin-left: -.14em;
  opacity: .85;
}

#itlr-build-gate .itlr-gate-btn:hover {
  /* One step lighter than brand cyan, so ink contrast rises rather than falls. */
  background-color: #1AC3F6;
  background-color: color-mix(in srgb, var(--e-global-color-primary, #00B6F1) 88%, #ffffff);
  box-shadow:
    0 0 0 1px rgba(20, 22, 30, .2),
    inset 0 1px 0 rgba(255, 255, 255, .45),
    0 3px 6px rgba(20, 22, 30, .2),
    0 16px 30px -10px rgba(20, 22, 30, .44),
    0 34px 60px -24px rgba(20, 22, 30, .52);
}

#itlr-build-gate .itlr-gate-btn:active {
  background-color: var(--e-global-color-primary, #00B6F1);
  box-shadow:
    0 0 0 1px rgba(20, 22, 30, .22),
    inset 0 1px 0 rgba(255, 255, 255, .3),
    0 1px 2px rgba(20, 22, 30, .2),
    0 6px 14px -6px rgba(20, 22, 30, .38);
}

/* Keyboard ring: a white hairline hugging the fill, then the ink outline
   outside it, so the focus state survives both pale decking and dark shingle. */
#itlr-build-gate .itlr-gate-btn:focus-visible {
  outline: 3px solid var(--e-global-color-secondary, #14161E);
  outline-offset: 4px;
  box-shadow:
    0 0 0 2px rgba(255, 255, 255, .95),
    0 2px 4px rgba(20, 22, 30, .18),
    0 10px 22px -8px rgba(20, 22, 30, .42),
    0 26px 48px -22px rgba(20, 22, 30, .5);
}

/* Phones: tighter tracking so 43 characters still land on two lines, not three. */
@media (max-width: 480px) {
  #itlr-build-gate .itlr-gate-btn {
    letter-spacing: .1em;
    gap: .55em;
    padding: 15px 22px;
    max-width: min(92vw, 360px);
  }
}

@media (prefers-reduced-motion: no-preference) {
  #itlr-build-gate .itlr-gate-btn {
    transition:
      transform .35s cubic-bezier(.22, .61, .36, 1),
      background-color .25s ease,
      box-shadow .35s cubic-bezier(.22, .61, .36, 1);
  }
  #itlr-build-gate .itlr-gate-btn::after {
    transition: transform .35s cubic-bezier(.22, .61, .36, 1);
    animation: itlrGateNudge 4.2s ease-in-out infinite; /* quiet beckon, arrow only */
  }
  #itlr-build-gate .itlr-gate-btn:hover { transform: translateY(-2px); }
  #itlr-build-gate .itlr-gate-btn:hover::after {
    animation: none;
    transform: rotate(45deg) translate(2px, -2px);
  }
  #itlr-build-gate .itlr-gate-btn:active { transform: translateY(0); }
}

@keyframes itlrGateNudge {
  0%, 74%, 100% { transform: rotate(45deg) translate(0, 0); }
  86%           { transform: rotate(45deg) translate(2.5px, -2.5px); }
}


#itlr-build-gate .itlr-gate-skip {
  position: absolute;
  right: 20px;
  bottom: 20px;
  z-index: 2;
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
  border: 1px solid rgba(20, 22, 30, .18);
  background: rgba(255, 255, 255, .94);
  color: var(--e-global-color-secondary, #14161E);
  font-family: "Exo 2", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  padding: 10px 18px;
  border-radius: 4px;
}
#itlr-build-gate .itlr-gate-skip:hover { background: #ffffff; border-color: rgba(20, 22, 30, .4); }

/* The chat widget is `position: fixed` at `z-index: 2147483647` -- the largest
   a 32-bit signed z-index can be, so the gate cannot out-stack it, and it
   loaded over the top of an `aria-modal` dialog and sat exactly on the Skip
   button in the bottom-right corner. `elementsFromPoint` at the middle of Skip
   returned the widget, not the button: a mouse user could not skip the video.

   Hidden from CSS rather than from `buildGate()` on purpose. The widget script
   is deferred (see build/seo/perf.mjs), so it is usually injected *after* the
   gate is built -- a one-shot sweep at build time would miss it, whereas a rule
   keyed off a class on <html> applies whenever the widget turns up. The class
   is removed in finish(), which restores the widget on every exit path. */
html.itlr-gate-open [id^="chat-widget"],
html.itlr-gate-open iframe.iframe-hidden {
  display: none !important;
}

/* The scroll lock. buildGate() sets `document.body.style.overflow = 'hidden'`,
   which on this site locks nothing: the theme puts `overflow-y: auto` on the
   root element, so <html> is the scrolling box and hiding body's overflow just
   hides overflow *inside* body. The page went on scrolling behind the gate.
   Lock the root as well, and the one that actually scrolls is pinned.

   overscroll-behavior stops the scroll chaining through to the page on a
   trackpad or a phone, and touch-action stops a touch drag panning the document
   before any of that applies. Neither blocks a tap, so the button and Skip still
   work. Keyboard scrolling (space, PageDown, arrows) stops too, because there is
   no longer a scrollable box to move. */
html.itlr-gate-open,
html.itlr-gate-open body {
  overflow: hidden !important;
  overscroll-behavior: none;
}
html.itlr-gate-open { touch-action: none; }


/* ---------------------------------------------------------------------------
 * 12. The mobile call bar.
 *
 * The homepage is 18,677px tall at 375px wide and nothing keeps a phone number
 * on screen while you read it: there is no sticky header and no fixed bar, so
 * the header's CALL NOW scrolls away in the first swipe and the next of the
 * ten tel: links is thousands of pixels down. Roofing is urgent-intent and
 * phone-driven, and the moment someone decides to call is not the moment they
 * want to scroll back to the top to find out how.
 *
 * Phones only. Above 768px the header is visible often enough, and a fixed bar
 * on a desktop is just lost height.
 *
 * The bar is two targets rather than one because the two intents are different:
 * a leak now wants the phone, a quote next week wants the form. Calling is the
 * louder of the two, in the brand cyan, because that is the one that cannot
 * wait. Both clear 48px, above the 44px minimum.
 *
 * body gets matching bottom padding so the bar never covers the footer or the
 * last field of the form it is pointing at, and the padding is applied to the
 * same breakpoint so desktop keeps its layout untouched.
 * ------------------------------------------------------------------------- */
/* Every rule for the bar sits inside the phone breakpoint, but the script adds
   the element on every screen. Without this, desktop renders it unstyled below
   the footer: two icons stretched to the full page width, ~3000px of empty
   scroll. */
@media (min-width: 768px) {
  .itlr-callbar { display: none; }
}
@media (max-width: 767px) {
  /* The reserve is written by the script from the bar's measured height, because
     the bar grows with the visitor's font size and with the home indicator on an
     iPhone, and a hardcoded number is wrong on the first phone that differs.
     The value here is only the fallback for a first paint before the script has
     measured, and for the case where the script does not run at all. */
  body.itlr-has-callbar {
    padding-bottom: var(--itlr-callbar-h, calc(64px + env(safe-area-inset-bottom, 0px)));
  }

  .itlr-callbar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 9998;
    display: flex;
    gap: 8px;
    padding: 8px 10px calc(8px + env(safe-area-inset-bottom, 0px));
    background: #14161E;
    box-shadow: 0 -2px 12px rgba(0, 0, 0, .28);
  }

  .itlr-callbar__btn {
    /* The number has to fit on one line, and it is the longer of the two
       labels, so the call button takes the larger share rather than both
       splitting the width evenly and wrapping "(484) 553-0213" across two. */
    flex: 1.35 1 0;
    min-width: 0;
    white-space: nowrap;
    min-height: 48px;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 0 10px;
    border-radius: 6px;
    font-family: inherit;
    font-size: 14.5px;
    font-weight: 700;
    line-height: 1.2;
    text-align: center;
    text-decoration: none;
    letter-spacing: .01em;
  }

  .itlr-callbar__btn--call {
    background: #00B6F1;
    color: #14161E;
  }

  .itlr-callbar__btn--quote {
    flex: 1 1 0;
    background: transparent;
    color: #ffffff;
    border: 1px solid rgba(255, 255, 255, .55);
  }

  .itlr-callbar__btn:focus-visible {
    outline: 3px solid #ffffff;
    outline-offset: 2px;
  }

  .itlr-callbar__icon {
    width: 17px;
    height: 17px;
    flex: 0 0 auto;
  }
}

/* The gate owns the viewport while it plays, so the bar must not sit on top of
   it. Hidden rather than removed, so it is back the moment the gate closes. */
@media (max-width: 767px) {
  html.itlr-gate-open .itlr-callbar { display: none; }
}

/* The chat widget is fixed 20px off the bottom right at the largest z-index
   there is, so it cannot be put behind the bar and it lands squarely on the
   estimate button. Lifting it by the bar's own height keeps both usable and
   keeps the widget where its own script expects to find it. */
@media (max-width: 767px) {
  #chat-widget-push-to-talk {
    bottom: calc(20px + var(--itlr-callbar-h, 64px)) !important;
  }
}

/* ------------------------------------------------------------------------
   About Us: crew and van photographs (client supplied, 2 Oct 2026).
   The markup is written into the page at publish time by
   build/seo/photos.mjs, between Bryson's story and "Contact Our Roofing
   Team". Two photos side by side, stacked on phones; same 16px radius as the
   Past Work cards. The shadow is lighter than theirs because this sits on a
   white page, not a dark band.
   ------------------------------------------------------------------------ */
.itlr-crew-photos {
  padding: 24px 20px 56px;
}
.itlr-crew-photos__inner {
  max-width: 1170px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 28px;
}
.itlr-crew-photos__item {
  margin: 0;
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 14px 34px rgb(0 0 0 / .16);
  aspect-ratio: 4 / 3;
}
.itlr-crew-photos__item img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
@media (max-width: 767px) {
  .itlr-crew-photos { padding: 8px 16px 40px; }
  .itlr-crew-photos__inner { grid-template-columns: 1fr; gap: 18px; }
}
