body{margin:0;background:#fff;color:#14171a;font-family:'Inter',Arial,sans-serif;-webkit-font-smoothing:antialiased}
a{color:#1D79A0;text-decoration:none}a:hover{color:#DF1A22}
html.ls-loaded{scroll-behavior:smooth}
@keyframes lsFadeUp{from{opacity:0;transform:translateY(36px)}to{opacity:1;transform:translateY(0)}}
@keyframes lsFadeLeft{from{opacity:0;transform:translateX(48px)}to{opacity:1;transform:translateX(0)}}
@keyframes lsBottleIn{0%{opacity:0;transform:translateY(60px) rotate(4deg)}60%{opacity:1}100%{opacity:1;transform:translateY(0) rotate(0deg)}}
@keyframes lsFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-14px)}}
@keyframes lsBarGrow{from{width:0}to{width:120px}}
@keyframes lsStarPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.12)}}
/* Content is VISIBLE BY DEFAULT. The hidden pre-reveal state applies only
   once site.js has booted and stamped `js-anim` on <html> — so a page with
   broken, blocked or absent JS still shows everything. site.js also force-
   reveals anything still hidden shortly after load as a belt-and-braces. */
.ls-reveal,.ls-reveal-l,.ls-reveal-r,.ls-stagger>*{opacity:1}
.js-anim .ls-reveal{opacity:0;transform:translateY(36px);transition:opacity 0.8s cubic-bezier(0.16,1,0.3,1),transform 0.8s cubic-bezier(0.16,1,0.3,1)}
.js-anim .ls-reveal.ls-in{opacity:1;transform:translateY(0)}
.js-anim .ls-reveal-l{opacity:0;transform:translateX(-48px);transition:opacity 0.9s cubic-bezier(0.16,1,0.3,1),transform 0.9s cubic-bezier(0.16,1,0.3,1)}
.js-anim .ls-reveal-l.ls-in{opacity:1;transform:translateX(0)}
.js-anim .ls-reveal-r{opacity:0;transform:translateX(48px);transition:opacity 0.9s cubic-bezier(0.16,1,0.3,1),transform 0.9s cubic-bezier(0.16,1,0.3,1)}
.js-anim .ls-reveal-r.ls-in{opacity:1;transform:translateX(0)}
.js-anim .ls-stagger>*{opacity:0;transform:translateY(28px);transition:opacity 0.7s cubic-bezier(0.16,1,0.3,1),transform 0.7s cubic-bezier(0.16,1,0.3,1)}
.js-anim .ls-stagger.ls-in>*{opacity:1;transform:translateY(0)}
.ls-stagger.ls-in>*:nth-child(1){transition-delay:0s}.ls-stagger.ls-in>*:nth-child(2){transition-delay:0.1s}.ls-stagger.ls-in>*:nth-child(3){transition-delay:0.2s}.ls-stagger.ls-in>*:nth-child(4){transition-delay:0.3s}.ls-stagger.ls-in>*:nth-child(5){transition-delay:0.4s}.ls-stagger.ls-in>*:nth-child(6){transition-delay:0.5s}.ls-stagger.ls-in>*:nth-child(7){transition-delay:0.6s}.ls-stagger.ls-in>*:nth-child(8){transition-delay:0.7s}
@media (prefers-reduced-motion:reduce){.ls-reveal,.ls-reveal-l,.ls-reveal-r,.ls-stagger>*{opacity:1!important;transform:none!important;transition:none!important}}

/* ==================== LOUD LAYER ==================== */
:root{
  /* Brand tricolor — for bars, blocks, rules, fills and icons.
     NEVER use these for text on a light background: measured on #F5F7F8
     green is 1.90:1 and blue 2.44:1, well under WCAG AA. */
  --ls-green:#8DC63F;--ls-blue:#29ABE2;--ls-red:#ED1C24;--ls-ink:#14171a;--ls-char:#0c0e10;
  /* Accessible text tiers, computed against #F5F7F8 (harder than white).
     -lg  = large display type, >=24px or >=18.66px bold   (AA 3:1)
     -txt = body copy, eyebrows, tags, links               (AA 4.5:1) */
  --ls-green-lg:#6F9C32; --ls-green-txt:#577B27;   /* 3.00:1 / 4.59:1 */
  --ls-blue-lg:#2498C9;  --ls-blue-txt:#1D79A0;    /* 3.04:1 / 4.53:1 */
  --ls-red-lg:#ED1C24;   --ls-red-txt:#DF1A22;     /* 4.08:1 / 4.54:1 */
  --ls-gold-txt:#986716;                           /* 4.56:1 */
}

/* --- tricolor marquee ticker --- */
.ls-marquee{background:#fff;overflow:hidden;position:relative;padding:34px 0;user-select:none}
.ls-marquee::before,.ls-marquee::after{content:"";position:absolute;top:0;bottom:0;width:120px;z-index:2;pointer-events:none}
.ls-marquee::before{left:0;background:linear-gradient(90deg,#fff,rgba(255,255,255,0))}
.ls-marquee::after{right:0;background:linear-gradient(270deg,#fff,rgba(255,255,255,0))}
.ls-marquee-track{display:flex;width:max-content;animation:lsMarquee 38s linear infinite}
/* Marquee is Inter Bold, uppercase (Rowan, 20 Aug 2026). It was the only
   thing on the site set in Space Grotesk — a second typeface carried for
   one strip of text — so moving it to Inter also drops that family from
   the Google Fonts request entirely. */
.ls-marquee-track>span{display:inline-flex;align-items:center;gap:30px;padding-right:30px;color:#2b3036;font-family:'Inter','Helvetica Neue',Arial,sans-serif;font-style:normal;font-size:29px;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;white-space:nowrap}
.ls-marquee-track b{font-weight:700}
.ls-marquee-track i{display:block;width:11px;height:11px;border-radius:3px;flex:none}
.ls-marquee:hover .ls-marquee-track{animation-play-state:paused}
@keyframes lsMarquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}

/* --- loud CTA buttons --- */
.ls-btn{display:inline-block;transition:transform 0.25s cubic-bezier(0.16,1,0.3,1),box-shadow 0.25s cubic-bezier(0.16,1,0.3,1)}
.ls-btn:hover{transform:translateY(-3px)}
.ls-btn-red:hover{box-shadow:0 14px 30px rgba(237,28,36,0.36)}
.ls-btn-green:hover{box-shadow:0 14px 30px rgba(141,198,63,0.36)}
.ls-btn-dark:hover{box-shadow:0 14px 30px rgba(20,23,26,0.3)}

/* --- giant display numeral --- */
.ls-huge{font-weight:300;letter-spacing:-0.05em;line-height:0.82;font-variant-numeric:tabular-nums}

/* --- full-bleed section break --- */
.ls-break{position:relative;overflow:hidden;padding:130px 64px;text-align:center;color:#fff}
.ls-break h2{font-weight:300;letter-spacing:-0.03em;line-height:1;margin:0}
.ls-break .ser{font-family:'Playfair Display',Georgia,serif;font-style:italic;font-weight:500}
.ls-break-rule{display:flex;height:6px;width:120px;border-radius:3px;overflow:hidden;margin:0 auto 30px}
.ls-break-rule span{flex:1}
/* Supporting-beat variant for inner pages (see build.js, product page). The
   full 130px padding belongs to the homepage statement banner only. */
.ls-break-tight{padding:74px 64px}
.ls-break-tight .ls-break-rule{margin-bottom:24px}

/* --- header lift on scroll (scroll-driven, no JS; applies to every page) --- */
@supports (animation-timeline:scroll()){
  header{animation:lsHeadShadow linear both;animation-timeline:scroll();animation-range:0 140px}
  @keyframes lsHeadShadow{from{box-shadow:0 0 0 rgba(12,14,16,0)}to{box-shadow:0 8px 30px rgba(12,14,16,0.13)}}
}

/* --- stat counters --- */
.ls-statnum{font-variant-numeric:tabular-nums}

/* --- Ken Burns drift for the "Wherever fire risk lives" scene --- */
@keyframes lsKenBurns{
  from{transform:scale(1) translate(0,0)}
  to{transform:scale(1.12) translate(-1.8%,-1.2%)}
}
.ls-kenburns{animation:lsKenBurns 28s ease-in-out infinite alternate;transform-origin:62% 42%;will-change:transform}
@media (prefers-reduced-motion:reduce){.ls-kenburns{animation:none}}

/* The step numerals sit in a ring, not a box or a filled disc (Rowan,
   20 Aug 2026). A faint full-circle track carries a coloured arc that orbits
   continuously, so the card reads as "in progress" rather than static.

   Two colours per step, deliberately different: the STROKE is the pure brand
   colour, because a rule is a rule and brand colours stay pure on strokes and
   fills. The NUMERAL is the darkened -txt tier, because that is text on a
   white card — pure green measures 1.90:1 there and would fail outright.

   r=45 gives a circumference of 282.7, so the dash pattern sums to 283 and
   the offset animates a clean -283 for exactly one lap with no jump. */
@keyframes lsRingSpin{
  from{stroke-dashoffset:0}
  to{stroke-dashoffset:-283}
}
.ls-ring{position:relative;width:104px;height:104px;margin:0 auto;display:grid;place-items:center}
.ls-ring svg{position:absolute;inset:0;width:100%;height:100%;transform:rotate(-90deg)}
.ls-ring circle{fill:none;stroke-width:4.5;stroke-linecap:round}
.ls-ring .trk{stroke:var(--ls-step);opacity:0.18}
.ls-ring .arc{stroke:var(--ls-step);stroke-dasharray:74 209;animation:lsRingSpin 3.2s linear infinite}
.ls-ring span{font-size:40px;font-weight:600;letter-spacing:-0.02em;line-height:1;color:var(--ls-step-ink)}
/* Reduced motion keeps the ring but stops it moving — a closed circle rather
   than a fragment frozen mid-orbit, which would just look broken. */
@media (prefers-reduced-motion:reduce){
  .ls-ring .arc{animation:none;stroke-dasharray:none;opacity:0.9}
}

/* Cap off / Aim / Spray — a proper build-in rather than the house stagger.
   These three cards are the method, the thing the page is actually teaching,
   and the default 28px fade was too quiet to read as a sequence. They travel
   further, come in fractionally scaled down, and the numeral pops after its
   own card has landed, so you see one, two, three rather than three at once.

   Same specificity as the .ls-stagger rules it overrides (0,2,1), so it wins
   on source order alone — it must stay BELOW them in this file. .ls-in is
   added by the IntersectionObserver in site.js, so all of it is scroll-driven
   and nothing fires at load. */
@keyframes lsStepNum{
  from{transform:scale(0.6);opacity:0}
  60%{transform:scale(1.06)}
  to{transform:scale(1);opacity:1}
}
.js-anim .ls-steps>*{
  opacity:0;transform:translateY(44px) scale(0.96);
  transition:opacity 0.75s cubic-bezier(0.16,1,0.3,1),transform 0.75s cubic-bezier(0.16,1,0.3,1)
}
.js-anim .ls-steps.ls-in>*{opacity:1;transform:translateY(0) scale(1)}
.ls-steps.ls-in>*:nth-child(1){transition-delay:0s}
.ls-steps.ls-in>*:nth-child(2){transition-delay:0.14s}
.ls-steps.ls-in>*:nth-child(3){transition-delay:0.28s}
.js-anim .ls-steps.ls-in>*:nth-child(1) .ls-step-num{animation:lsStepNum 0.62s cubic-bezier(0.16,1,0.3,1) 0.22s both}
.js-anim .ls-steps.ls-in>*:nth-child(2) .ls-step-num{animation:lsStepNum 0.62s cubic-bezier(0.16,1,0.3,1) 0.36s both}
.js-anim .ls-steps.ls-in>*:nth-child(3) .ls-step-num{animation:lsStepNum 0.62s cubic-bezier(0.16,1,0.3,1) 0.50s both}
@media (prefers-reduced-motion:reduce){
  .js-anim .ls-steps>*{opacity:1!important;transform:none!important;transition:none!important}
  .js-anim .ls-steps.ls-in>* .ls-step-num{animation:none!important}
}

/* The "10 fire types. One can." figure scales in when its half of the split
   enters view, rather than just sliding with it. Hung off .ls-reveal-r.ls-in
   so it is driven by the same IntersectionObserver as everything else — a
   bare animation would fire at load, long before the section is on screen.
   transform-origin is left so the numeral grows out of the margin, not the
   middle. */
@keyframes lsTenPop{
  from{transform:scale(0.74);opacity:0}
  to{transform:scale(1);opacity:1}
}
.js-anim .ls-reveal-r.ls-in .ls-fs-172{
  display:inline-block;transform-origin:left center;
  animation:lsTenPop 0.8s cubic-bezier(0.16,1,0.3,1) 0.12s both
}
@media (prefers-reduced-motion:reduce){
  .js-anim .ls-reveal-r.ls-in .ls-fs-172{animation:none}
}

/* --- the proof block: deep red gradient, white type -------------------
   Rowan, 20 Aug 2026, matching the concept board: a dark red gradient
   rather than a flat fill. (It was brand green for about an hour; green is
   a LIGHT ground and forced every mark to ink, which is why none of that
   survives here.)

   THE BRIGHTEST STOP IS THE CONSTRAINT, and it is #DF1A22 for a reason:
   solid white on it measures 4.87:1, which carries the 14px uppercase
   labels at AA. Pure brand red #ED1C24 measures 4.38:1 and would fail them.
   Every other stop is darker, so the centre of the gradient is the worst
   case and everything else has more headroom, not less.
     white 100%  4.87:1  — the 14px labels, so they get NO transparency
     white  92%  4.29:1  — 24px/600 and the star row, large text (AA 3:1)
     white  78%  ~3.4:1  — the "/5" and "s" marks at 44px, large text
   Do not lighten the top stop and do not fade the small labels. */
.ls-proof-sec{background:radial-gradient(118% 140% at 64% 40%,#DF1A22 0%,#C2151C 32%,#971117 66%,#6B0B10 100%);color:#fff;padding:96px 64px;text-align:center}
/* Gold stars, per Rowan — but NOT the site's usual #f5a623, which measures
   only 2.40:1 on this red and disappears into it. #FFD166 is 3.38:1, over
   the 3:1 a 40px display run needs, and still reads gold rather than cream.
   Anything darker than about #FFC24B fails here. */
.ls-proof-stars{color:#FFD166;font-size:40px;letter-spacing:8px}
/* The headline figure sizes itself. It was an inline 184px, which at that
   size crowded the label underneath and left no air; 132px reads cleanly
   and the clamp handles every width without a media query. */
.ls-proof-num{font-size:clamp(48px,10.5vw,132px);line-height:1;margin:24px 0 14px}
.ls-proof-sub{font-size:24px;color:rgba(255,255,255,0.92);letter-spacing:0.14em;text-transform:uppercase;font-weight:600}
.ls-proof{display:flex;gap:0;margin-top:64px;justify-content:center;max-width:1120px;margin-left:auto;margin-right:auto}
.ls-proof-i{flex:1;border-right:1px solid rgba(255,255,255,0.22)}
.ls-proof-i:last-child{border-right:0}
.ls-proof-mark{color:rgba(255,255,255,0.78)}
.ls-proof-l{font-size:14px;letter-spacing:0.18em;color:#fff;text-transform:uppercase;margin-top:14px;font-weight:600}
/* the Trustpilot figure is a way through to the reviews, like the one on
   reviews.html. Underline only on the label so the numeral stays a numeral. */
.ls-proof-tp{display:block;color:inherit;text-decoration:none}
.ls-proof-tp .ls-proof-l{border-bottom:1px solid rgba(255,255,255,0.45);display:inline-block;padding-bottom:3px}
.ls-proof-tp:hover .ls-proof-l{border-bottom-color:#fff}

/* --- closing panel: a photograph as the last thing before the footer ---
   Same Ken Burns vocabulary as the "Wherever fire risk lives" plate, but
   slower and shallower: type sits on top of this one, and a 12% scale makes
   words look like they are swimming. The origin is the bottle rather than
   the frame centre, so the drift can never walk the product out of shot.

   THE BOX IS DELIBERATELY TALLER THAN A BANNER (780px). At 660 the box was
   WIDER in aspect than the photograph, so `cover` scaled by width and cropped
   only vertically — which means object-position X did nothing and the bottle
   could not be moved out of the headline's way. Taller than the source aspect
   means the crop is horizontal instead, so X=12% pushes the product into the
   right third where the concept board has it. Changing this height back will
   silently re-break that.

   LIGHT panel, dark type — the photograph is a bright room and the concept
   board it comes from puts near-black copy straight onto it. The scrim is
   therefore a WHITE veil, not a black one, weighted to the left where the
   copy sits: the wall is already almost white, and the veil only guarantees
   the margin if the crop shifts on an unusual viewport. Measured against the
   darkest pixel under the copy: ink 11.45:1, body #3f464c 6.09:1. The
   eyebrow is NOT the brand green — #577B27 comes out at 3.13:1 there, short
   of the 4.5:1 a 14px label needs, so it takes the body colour and the
   tricolor rule above it carries the brand instead. */
.ls-close{position:relative;overflow:hidden;min-height:780px;display:flex;align-items:center}
@keyframes lsKenBurnsSoft{
  from{transform:scale(1) translate(0,0)}
  to{transform:scale(1.06) translate(-0.8%,-0.5%)}
}
.ls-close-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:12% 50%;animation:lsKenBurnsSoft 34s ease-in-out infinite alternate;transform-origin:76% 50%;will-change:transform}
@media (prefers-reduced-motion:reduce){.ls-close-img{animation:none}}
.ls-close-scrim{position:absolute;inset:0;background:linear-gradient(90deg,rgba(252,252,252,0.84) 0%,rgba(252,252,252,0.64) 34%,rgba(252,252,252,0.18) 62%,rgba(252,252,252,0) 86%)}
.ls-close-body{position:relative;padding:0 64px;max-width:640px;color:#14171a}

/* --- full-bleed video plate (replaces the Ken Burns still) ---
   VISIBLE FROM FIRST PAINT. The poster is the clip's own first frame, so
   there is nothing to fade in from and no flash when playback starts.
   Do NOT gate visibility on a JS-set attribute: this node is materialised
   by the Claude Design runtime, which inserts it *after* site.js boots —
   an opacity:0 default meant the whole section rendered empty in
   production. Under reduced motion site.js pauses it and the poster
   simply stays put, which is exactly what this rule already shows. */
.ls-plate{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
/* Inline variant — the plate sits inside a split layout like an ordinary
   image instead of filling a full-bleed section. It keeps the .ls-plate
   class so site.js still re-asserts muted/loop and wires lazy playback;
   only the box model differs. */
.ls-plate-inline{position:static;inset:auto;width:100%;height:auto;object-fit:contain;display:block}
