/* client_custom.css — 2026-10-06
   Loaded AFTER m-and-g-pools-global.css on every page. Post-launch tweaks go
   HERE, never in global.css, which is regenerated from the approved
   stylesheet every time Phase 6A runs.
   Anything added here is outside the client-approved design and carries a
   dated one-line reason. */

/* ── slider slide ratio — operator, 2026-10-06, scoped 2026-10-07 ──────────
   The approved sheet sets .slider__slide img to aspect-ratio 16/9. Operator
   asked for 16/7 - a wider, shallower slide - while reviewing the six
   service hubs. Overridden here rather than in global.css so that file
   stays byte-verifiable against the approved r13 stylesheet.

   SCOPED to body.hub (operator, 2026-10-07). Unscoped it reached all
   fifteen pages that carry a slider, including the two APPROVED pages that
   already have one - /services/pool-safety-covers/ and
   /pool-safety-cover-buying-guide/ - and silently restyled them. Only the
   six service hubs carry body.hub, which is exactly where the change was
   asked for.

   Consequence, deliberate: the other nine revert to the approved 16/9 -
   the two approved pages above, and the seven resource guides. */
body.hub .slider__slide img{aspect-ratio:16/7}

/* ── numbered steps: an incomplete last row CENTRES — operator, 2026-10-06 ──
   Already the build's own standard for card grids; the approved sheet says so
   on .donot: "flex rather than grid so the incomplete last row CENTRES, which
   is the build's standard for card grids."

   .steps--media is a GRID, so its orphans sat hard left — 05 and 06 under 01
   and 02. A grid cannot centre an orphan row on whole columns when the
   remainder is even, so the tracks are DOUBLED and every item spans two; an
   orphan row can then start on a half-column and sit centred. Column count,
   gaps and badge positions are unchanged, so the .steps--rail connector still
   meets the outer badges.

   SCOPED to .steps--centre, which only the six service hubs carry. 21 of the
   carried-over LSP pages also have an incomplete steps row, and they are
   approved as they stand — this was asked for on the hubs only. */
.steps--media.steps--centre{grid-template-columns:repeat(8, 1fr)}
.steps--media.steps--centre > .steps__item{grid-column:span 2}
.steps--media.steps--centre.steps--3{grid-template-columns:repeat(6, 1fr)}

/* four columns / eight tracks: two orphans start at track 3, one at track 4 */
.steps--media.steps--centre:not(.steps--3) > .steps__item:nth-child(4n+1):nth-last-child(2){grid-column:3 / span 2}
.steps--media.steps--centre:not(.steps--3) > .steps__item:nth-child(4n+1):nth-last-child(1){grid-column:4 / span 2}

/* three columns / six tracks: two orphans start at track 2, one at track 3 */
.steps--media.steps--centre.steps--3 > .steps__item:nth-child(3n+1):nth-last-child(2){grid-column:2 / span 2}
.steps--media.steps--centre.steps--3 > .steps__item:nth-child(3n+1):nth-last-child(1){grid-column:3 / span 2}

/* stacked: one item per row, so there is no orphan row to centre. This has to
   restate the single column because the rules above raised the track count. */
@media (max-width:760px){
  .steps--media.steps--centre,
  .steps--media.steps--centre.steps--3{grid-template-columns:1fr}
  .steps--media.steps--centre > .steps__item{grid-column:auto}
}

/* -- sticky media column: unblock it -- operator, 2026-10-06 -----------------
   The approved sheet sets body{overflow-x:hidden}. That makes the body a
   SCROLL CONTAINER, and position:sticky then resolves against it rather than
   the viewport - so .mediasplit--sticky has never actually stuck, on the hub
   pages or on the two approved pages that already use it
   (/services/safety-cover-installation/ and /twinsburg-oh/).

   overflow-x:clip clips exactly the same way but does NOT create a scroll
   container, so sticky works. Scoped to body.hub rather than applied to
   body, so it reaches only the pages sticky was actually asked for and the
   approved pages are left exactly as they are. Carried by the six service
   hubs, and (operator, 2026-10-06) by /moreland-hills-oh/ and
   /orange-village-oh/, where the map pins beside a very long text column.
   The other fourteen location hubs keep a plain <body>. */
body.hub{overflow-x:clip}

/* -- the thank-you page's closing band, on white -- operator, 2026-10-07 ---
   .cta-band is var(--navy-deep) with a photograph and a scrim over it. On
   /thank-you/ that put a second dark band between a dark hero and the dark
   footer, so the operator asked for this one white.

   #FFFFFF is the Brand Guide white, so no colour outside the palette is
   introduced. Scoped to body.thanks, which only /thank-you/ carries, so
   every other cta-band on the site is untouched.

   The photo and scrim come off rather than being covered, and the two
   on-dark treatments flip: the heading colour, and .btn--ghost-dark, whose
   white border and white label would otherwise vanish. It becomes the same
   outline button the light bands already use. */
body.thanks .cta-band{background:#FFFFFF}
body.thanks .cta-band__bg,body.thanks .cta-band__scrim{display:none}
body.thanks .cta-band .eyebrow{color:var(--ink-strong)}
body.thanks .cta-band .btn--ghost-dark{
  background:transparent;color:var(--primary);border-color:var(--primary)}
body.thanks .cta-band .btn--ghost-dark:hover{
  background:var(--primary);color:var(--white);border-color:var(--primary)}

/* Centred, because .cta-band__in is a 760px block pinned left and with no
   heading in it the eyebrow and the two buttons sat in the left third of a
   white band. Same three declarations the sheet already uses to centre a
   section head - .sechead--center and its wavemark rule - so this is the
   design's own centring, not a new one. .btn-row is display:flex, so the
   buttons take justify-content rather than text-align. */
body.thanks .cta-band__in{margin-inline:auto;text-align:center}
body.thanks .cta-band .wavemark{margin-inline:auto}
body.thanks .cta-band .btn-row{justify-content:center}

/* -- chat widget and the mobile bar -- operator, 2026-10-07 --------------
   REVERTED. A transform on <chat-widget> was used to lift the bubble clear
   of .mobilebar, by making the host the containing block for the widget's
   own position:fixed descendants. It moved the bubble correctly (655-713,
   clear of the bar's 733 top edge) but it re-based the OPEN chat panel too:
   the host was 0x0, so the panel had no area to lay out in and the widget
   vanished on tap. A chat widget that will not open is worse than one that
   overlaps, so the rule is gone and the widget is back on its own
   positioning.

   The overlap it was solving is real and still there on mobile: the bubble
   is fixed at bottom:20px (an INLINE style inside the widget's shadow root,
   which no stylesheet here can reach) and lands at 734-792, while
   .mobilebar occupies 733-812 - and at z-index 99999999 against the bar's
   9999 it covers the Call/Specials buttons.

   SOLVED FROM OUR SIDE INSTEAD. The widget cannot be moved, so the bar
   gets out of its way: .mobilebar reserves the strip the bubble occupies,
   and its two buttons stop short of it.

   86px = the bubble's own footprint: 58px wide, sitting 20px off the right
   edge, plus 8px of breathing room. Measured at 375px - "Call Us" ran
   192-366 under a bubble at 297-355, and now runs 154-289, clear by 8px.
   Both buttons keep their full labels (136x59, nothing clipped) and stay
   well above the 44px tap target.

   This touches only our own component. No shadow DOM, no containing-block
   trick, nothing that can reach the chat panel - which is what broke the
   previous attempt.

   The bubble still sits over the BAR, but over its background rather than
   over a button, which reads as deliberate. If the client would rather it
   sat above the bar entirely, that is a widget setting: raise the chat
   widget's bottom spacing to ~100px in the GoHighLevel dashboard, and this
   padding can then come back out.
   (A second attempt, padding .mobilebar to reserve the strip, was reverted
   too: squeezing the bar into 75% of the width wrapped "Request a Quote"
   onto two lines and read as broken.)

   WHAT ACTUALLY WORKS. The host carries data-active, "false" closed and
   "true" open, so it can be sized differently in each state - which is what
   the first attempt got wrong. It used one 0x0 host for both, and the open
   panel had no area to lay out in, so the widget vanished on tap.

     closed  a 78x78 box parked one bar-height off the bottom. The bubble is
             58px with the widget's own 20px inset, so that is exactly its
             footprint. It lands at 655-713, clear of the bar at 745.
     open    the full viewport MINUS the bar, so the panel lays out at its
             natural size and simply stops above the bar instead of under
             it. Measured: host 375x733, panel renders complete - greeting,
             name, phone, message, consent, Send.

   pointer-events is deliberately NOT used to stop the open host covering
   the page: it inherits through the shadow boundary and disabled the bubble
   outright (hit-testing returned the page element behind it). It is not
   needed either - when the panel is open it covers the screen anyway, and
   when closed the host is only 78px square.

   z-index 9997 keeps it under the bar (9999) and the nav drawer (9998), so
   an open drawer covers it, as it should. Mobile only, on the same 768px
   breakpoint the bar uses; above that .mobilebar is display:none and the
   widget is left entirely alone.

   79px = .mobilebar's height. If that bar is re-sized, this moves with it. */
/* -- trust strip: let a long chip wrap -- operator, 2026-10-07 -----------
   56 pages ran their trust-strip text off the right edge at phone width,
   and `body{overflow-x:hidden}` clipped it, so the copy was CUT OFF
   mid-sentence rather than merely overflowing. 10 pages did it at 1440 too.
   Worst case /waite-hill-oh/: the item was 526px wide with its right edge
   at 571 on a 390 viewport.

   The markup is `<span><b>Family-owned since 2012; 30+ years in the trade,
   50+ years combined</b></span>` and the `<b>` inherits
   `white-space: nowrap`. That is RIGHT for what the approved design puts
   there - r13's own trustline carries short chips: "30+ / 50+", "Years in
   the trade", "7", "Field technicians" - and wrong for a sentence. 6B put
   sentence-length copy into a chip slot.

   So the nowrap is relaxed only where it cannot be honoured. A chip that
   fits still renders on one line, because normal wrapping never breaks a
   line that has room - the approved short chips are untouched. Measured on
   /waite-hill-oh/ at 390px: item width 526 -> 301, right edge 571 -> 346,
   0 still overflowing.

   COST, accepted deliberately: the band grows 59px taller on mobile
   (105 -> 164) where a chip wraps to three lines. The alternative was
   rewriting the copy on 56 pages, which is a content decision, not a
   build one. */
.trustline b{white-space:normal}

/* -- inline link inside a tile -- operator, 2026-10-07 --------------------
   An inline .tlink in a tile's prose rendered as a FULL-WIDTH WHITE CARD in
   the middle of its sentence: "...it belongs to the close — see [ cover-on
   timing in Solon ] for how the correct band is set."

   Not a markup bug. The anchor is a plain inline <a class="tlink"> inside a
   <p> inside a .tile. The approved sheet styles the tile-as-link pattern
   with a DESCENDANT selector —

     .nexttiles a{display:block;border:1px solid var(--hair);
                  border-top:3px solid var(--aqua);border-radius:var(--r);
                  padding:22px 24px;background:rgba(255,255,255,.035)}

   — so it paints every <a> anywhere inside a .nexttiles, including one
   nested in a tile's prose. .tlink sets display:inline-flex but loses the
   cascade: .tlink is (0,1,0) and .nexttiles a is (0,1,1).

   This is Cortex L-047 exactly: a container rule written for layout
   repainting something nested inside it. The sheet's own author knew tiles
   were not tiles-as-links — the comment on .nexttiles .tile says they
   "cannot just reuse the .nexttiles a rule" — but nothing guarded an <a>
   INSIDE a tile, because r13 never produces that shape: it has zero .tlink
   inside a .tile. 6B introduced it, legitimately, by linking out of tile
   prose.

   Specificity, deliberately exact: the strongest competing rule is
   .theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .nexttiles a
   at (0,4,1). The doubled .tlink below is also (0,4,1), and client_custom
   loads after the global sheet, so it wins on source order rather than by
   escalating — which is what L-047 prescribes ("make the guard the literal
   last rule"). No !important.

   Every value restored is .tlink's own, read off the approved sheet; the
   transparent bottom border is what its :hover underline animates. */
.nexttiles .tile a.tlink.tlink{
  display:inline-flex;padding:0;border:0;border-radius:0;
  border-bottom:2px solid transparent;background:transparent
}

/* -- <picture> wrappers for the WebP fallback -- operator, 2026-10-07 -----
   The image pass built a WebP sibling for all 95 AVIFs, but every one of
   the site's 164 references was a plain <img src="*.avif"> - no <picture>
   anywhere - so no browser could ever request a sibling. They are now
   wrapped (picture_wrap.py), which is what makes the fallback real.

   A wrapper changes one thing: the image is no longer a direct child of
   the element it used to be a child of. Three rules in the approved sheet
   depend on exactly that, and would otherwise break:

     .cta-band__bg{position:absolute;inset:0}         95 images
     .mediasplit--fill .mediasplit__media img{flex:1}  8 images, a flex ITEM
     .trust__grid > .trustmedia > img                 14 images, direct CHILD

   display:contents removes the <picture> box from the box tree altogether,
   so the <img> is laid out exactly as if the wrapper were not there - it
   is once more the flex item of .mediasplit__media and the absolutely
   positioned child of .cta-band. That restores the first two with no
   per-component rules at all. This is the whole reason the wrapper is
   safe; without it the fill-variant media columns collapse.

   display:contents does NOT change selector matching, so the third one -
   the only direct-child selector in the sheet that targets an image - is
   restated below for the wrapped shape. Both declarations are copied
   verbatim from m-and-g-pools-global.css, including the 900px breakpoint;
   nothing here is a new design decision, it is the approved rule reaching
   the element it was written for. */
picture{display:contents}
.trust__grid > .trustmedia > picture > img{
  position:absolute;inset:0;width:100%;height:100%;
  aspect-ratio:auto;object-fit:cover}
@media (max-width:900px){
  .trust__grid > .trustmedia > picture > img{
    position:static;width:100%;height:auto;aspect-ratio:4/3}
}

@media (max-width:768px){
  chat-widget[data-active="false"]{
    display:block;position:fixed;right:0;bottom:79px;width:78px;height:78px;
    transform:translateZ(0);z-index:9997
  }
  chat-widget[data-active="true"]{
    display:block;position:fixed;top:0;left:0;right:0;bottom:79px;
    transform:translateZ(0);z-index:9997
  }
}

/* Mobile bar - "Request a Quote" must hold one line on every phone.
   Operator request 2026-10-07. The label is NOT changed; this is spacing.

   The bar gives each button `flex:1 1 0`, so each gets exactly half the
   viewport minus the bar's padding and gap. At 320px that left 118.5px of
   content box for a label whose single line measures 146.4px at the
   approved 15px/700 Open Sans - a 28px shortfall, which is more than the
   button's own padding can return. Hence two steps:

     1. <=768px  tighten the bar's gap 9->6, its padding 9->6 and each
        button's padding-inline 12->6, and set white-space:nowrap. That
        buys 23px/button and holds the approved 15px down to ~352px.
     2. <=359px  tighten once more (4px) AND step the label to .875rem.
        Below ~352px no amount of spacing fits 15px, so the type gives.

   nowrap alone is NOT the fix and was rejected: the buttons are centered
   flex, so an overlong label overflows both edges silently without
   growing scrollWidth - it reports one line while visibly spilling.
   The measured test is content-box width minus rendered text width.

   Verified Chromium + WebKit at 320/344/350/359/360/375/384/390/393/412/
   414/430: one line everywhere, slack +5.4px at worst (WebKit 320), 48px
   height retained at every width. "Call Us" is unaffected (+80px slack). */
@media (max-width:768px){
  .mobilebar{gap:6px;padding-inline:6px}
  .mobilebar a.btn{padding-inline:6px;white-space:nowrap}
}
@media (max-width:359px){
  .mobilebar{gap:4px;padding-inline:4px}
  .mobilebar a.btn{padding-inline:4px;font-size:.875rem}
}

/* Hero eyebrow - the accent rule sits on the FIRST line when the text wraps.
   Operator request 2026-10-07.

   `.hero__eyebrow` is an inline-flex with `align-items:center`, so its
   `::before` rule is centred against the whole text block. On one line that
   is correct and on two it floats into the gap between them, which is what
   the phone screenshot showed.

   Fixed on the pseudo-element alone via `align-self`, so the container's
   `align-items:center` is left as the approved sheet wrote it, and the
   wrapped text keeps its indent under the first line (a `display:block` +
   inline-block rule would also solve the alignment, but it would pull line
   two back under the rule and change the wrap shape, which was not asked
   for).

   Offset = half the line box, less half the rule: (1lh - 3px)/2. The `em`
   value is the same arithmetic against the inherited line-height of 1.65
   (13px x 1.65 = 21.45px -> 9.225px) and carries browsers without the `lh`
   unit; where `lh` is supported it takes over and self-corrects if the type
   scale ever moves.

   One line is unaffected by construction: centring and first-line alignment
   are the same position when there is only one line. Measured, not assumed -
   rule centre vs first line-box centre is within 0.01px at 320/375/390/430/
   768/1280, before and after, in Chromium and WebKit. */
.hero__eyebrow::before{align-self:flex-start;margin-top:calc((1.65em - 3px)/2)}
@supports (margin-top:1lh){
  .hero__eyebrow::before{margin-top:calc((1lh - 3px)/2)}
}

/* Trust band chips flow as text on mobile - operator, 2026-10-07.
   Desktop (>900px) is deliberately untouched.

   `.trustline > span` is `display:flex`, so every `<b>` AND every run of
   plain text between them becomes its own flex item. Wide enough, the items
   sit on one row and the chip reads as a sentence, which is the desktop
   appearance. Narrow, each item wraps INSIDE ITS OWN COLUMN, and the chip
   turns into a ragged grid - "Family-owned since 2012" over one column, "30+
   years" over the next, "in the trade," over the next. That is the phone
   screenshot.

   `display:block` returns the chip to normal inline flow: the `<b>` run
   inline, the text between them inline, and the whole chip wraps as one
   paragraph the way it reads on desktop. The markup already carries the
   real spacing ("</b>; <b>", " in the trade, "), so the flex `gap:.45em`
   that stood in for those spaces is not needed once the items are inline
   again - and `align-items:baseline` is replaced by the inline baseline
   alignment that mixed 13px/18px text gets for free.

   900px is this sheet's own breakpoint for the band - the one that collapses
   `.trust__grid` to a single column (25 uses, the dominant breakpoint in the
   file), so the chips switch exactly where the band itself goes to its
   stacked form.

   Measured on /aurora-oh/, the worst case (two chips, one a full sentence):
   inner column breaks 4 -> 1 at 320/390/430/560, and the one that remains is
   the long bold phrase wrapping as ordinary text, which is correct. The band
   also gets SHORTER - 189 -> 139px at 390px - recovering most of the 59px
   the `white-space:normal` note above booked as a cost. Unchanged at 901,
   1024 and 1280. */
@media (max-width:900px){
  .trustline > span{display:block}
}

/* 1. Hairline under the NON-STICKY header only - operator, 2026-10-07.
   The stuck header is explicitly out of scope: it keeps the 2px Sunshine
   border the approved sheet gives it, and `:not(.is-stuck)` is what keeps
   this rule off it. Desktop and mobile alike.

   The header is `position:absolute;left:0;right:0`, so the line runs the
   full viewport width over the hero. It adds 1px to the header's own box
   (102 -> 103px) and moves nothing inside it: `.header-inner` is a separate
   box, so the logo, nav and burger do not shift, and `.mega` anchors to
   `.header-inner` rather than to the header, so the panel is unaffected. */
.header:not(.is-stuck){border-bottom:1px solid rgba(255,255,255,.15)}

/* 2. Mobile menu opens BELOW the header, which stays put - operator, 2026-10-07.

   Measured first, because the complaint and the cause are different things:
   the real header never moved. `.header` reads top=31px closed AND open, and
   top=0 when stuck, in both states. What moves is that `.drawer` is
   `position:fixed;inset:0` - a full-screen panel that COVERS the header and
   paints its own logo at a different size and offset (22px/56px, against the
   header's 49px/66px, or 10px/54px when stuck). The eye reads the duplicate
   as the header having jumped. It had not.

   So the panel is moved down to start exactly at the header's bottom edge,
   and the duplicate logo is dropped - the real one is now visible directly
   above it. The two offsets are the header's own geometry, written out so
   they stay readable: 31 + 18 + 66 + 18 + 1 = 134px, and when stuck
   10 + 54 + 10 + 2 = 76px.

   `.drawer` is a SIBLING that follows `.header`, which is what makes
   `.header.is-stuck ~ .drawer` able to switch the offset with the state.

   `.drawer__top` KEEPS its close button. The burger only ever opens the
   drawer - `b.addEventListener('click',function(){set(true)})` - so the
   close button, a link tap, or Escape are the only ways out. Hiding that row
   wholesale would trap the menu open. Only the `<img>` goes, and the row is
   pushed right so the button keeps its corner.

   The `:has()` rule covers the one scroll position where this would
   otherwise leave a gap: between 1 and 90px the header is still `absolute`
   and has scrolled partly off-screen, while the drawer is fixed. Pinning it
   for the duration of the open menu makes the two meet at every scroll
   position. It changes nothing at rest, since `absolute` at top:31px and
   `fixed` at top:31px are the same place when the page is at the top - and
   the script sets `documentElement.overflow:hidden` while open, so the page
   cannot scroll underneath it either way.

   Verified at 390px, header top / logo top / logo height identical open and
   closed, scrolled and not: 31/49/66 unstuck, 0/10/54 stuck, and the drawer
   meets the header edge with a 0px gap in all four. */
@media (max-width:900px){
  body:has(.drawer[data-open="true"]) .header{position:fixed}
  .drawer{top:calc(0px + 18px + 66px + 18px + 1px)}   /* header top is 0 since the ribbon went */
  .header.is-stuck ~ .drawer{top:calc(10px + 54px + 10px + 2px)}
  .drawer__top{display:none}
}

/* 3. The burger IS the close control - operator, 2026-10-07, sitewide.

   Supersedes the "KEEPS its close button" note above. That constraint was
   real while the burger could only open: removing the drawer's own button
   would have trapped the menu. The markup change below removes that
   constraint rather than working around it, so `.drawer__top` is now hidden
   outright - which also closes the empty-panel-band item that change left
   open.

   Markup/script, applied to all 135 built pages by `burger_toggle.py`:
     - the listener toggles on `data-open` instead of always opening
     - `set()` writes `aria-label` (Open menu / Close menu) beside the
       `aria-expanded` it already wrote
     - `<button class="drawer__close">` and its now-dangling var and
       listener are gone

   The icon swap is CSS and rides on `aria-expanded`, which set() was
   already maintaining - so the state lives in exactly one place and the
   icon cannot disagree with what a screen reader is told.

   The bars are pseudo-elements rather than a second SVG because the mark is
   a single <path> of three lines ("M0 1h22M0 8h22M0 15h22") that CSS cannot
   reshape. The SVG goes `visibility:hidden`, not `display:none`, so it
   keeps holding the button's content box and the X lands on exactly the
   same pixels the bars occupied - the button does not resize or shift.
   22px wide and 2px thick are the SVG's own width and stroke-width. */
.burger{position:relative}
.burger[aria-expanded="true"] svg{visibility:hidden}
.burger[aria-expanded="true"]::before,
.burger[aria-expanded="true"]::after{
  content:"";position:absolute;left:50%;top:50%;
  width:22px;height:2px;background:currentColor;border-radius:1px
}
.burger[aria-expanded="true"]::before{transform:translate(-50%,-50%) rotate(45deg)}
.burger[aria-expanded="true"]::after{transform:translate(-50%,-50%) rotate(-45deg)}

/* Header sits at the very top - the preview ribbon it cleared is gone.
   Operator, 2026-10-07, sitewide, desktop and mobile.

   `.pvbar` is the preview chrome from L-058: a flow-level strip at the top
   of the page, ~32px rendered (7px padding, 11px/1.65 text, 7px padding).
   The approved sheet therefore starts the header BELOW it -
   `.header{top:35px}`, and `top:31px` under 900px where the strip is
   shorter. **The strip itself is no longer emitted - 0 of 134 pages contain
   a .pvbar element - but both offsets stayed**, which is the band of empty
   hero showing above the header.

   One declaration covers it. `.header{top:35px}` and the 900px
   `.header{top:31px}` are both (0,1,0), so a (0,1,0) rule in this later
   file beats each of them on source order. `.header.is-stuck` is (0,2,0)
   and already `top:0`, so the stuck header is untouched by construction -
   it cannot lose to this.

   Swept for the rest of the ribbon's footprint rather than assuming:
     - body padding-top .................. none (the 74px at <=900px is
       padding-BOTTOM, reserving the mobile bar, unrelated)
     - scroll-padding / scroll-margin .... none anywhere in either sheet
     - mobile menu ....................... YES - the .drawer offset above
       was written as `31px + ...`; corrected to `0px + ...` in the same
       pass, so the panel still meets the header edge exactly
     - any other top:3Xpx ................ none; the only two were the two
       header rules

   `.pvbar`'s own rules are left in the approved sheet untouched. Dead CSS
   for an element nothing emits is harmless, and that sheet stays
   byte-verifiable against r13. */
.header{top:0}


/* ===== QA 2026-10-10 - the eligibility / exclusions banner ==============
   One treatment for both "what we don't take on" sections. The band, the
   .mediasplit and the .donotgrid tile row are all approved components;
   what is new here is (a) tile sizing inside the narrower right-hand
   column, because .donotgrid's own min-width:250px wraps three tiles in
   a half-width grid cell, and (b) the yes/no corner badge the mock puts
   on each tile. Brand tokens only - no colour outside the palette. */
.mediasplit--elig{align-items:center;gap:clamp(24px,3vw,44px)}
.mediasplit--elig .sechead{margin-bottom:0}
.mediasplit--elig .donotgrid{justify-content:flex-start}
/* .donotgrid's own gap is 18px, so three tiles need 36px subtracted,
   not 24px - the first cut overflowed by 12px and wrapped 2+1. */
.mediasplit--elig .donotgrid li{flex:0 1 calc((100% - 36px) / 3);min-width:0;
  padding:22px 12px;gap:10px}
.mediasplit--elig .donotgrid b{overflow-wrap:anywhere}
.mediasplit--elig .donotgrid__ic{width:46px;height:46px}
.mediasplit--elig .donotgrid b{font-size:.9375rem;line-height:1.3}
@media (max-width:900px){
  .mediasplit--elig .donotgrid li{flex:0 1 calc((100% - 24px) / 3)}
}
@media (max-width:560px){
  .mediasplit--elig .donotgrid li{flex:0 1 100%}
}
/* the yes/no badge */
.donotgrid--elig li{position:relative}
.donotgrid__state{position:absolute;top:9px;right:9px;width:22px;height:22px;
  border-radius:50%;display:grid;place-items:center}
.donotgrid__state svg{width:12px;height:12px}
.donotgrid--elig li.is-yes .donotgrid__state{background:var(--txt-accent);
  color:var(--ink-navy)}
.donotgrid--elig li.is-no .donotgrid__state{background:rgba(255,255,255,.12);
  color:var(--txt);border:1px solid var(--edge)}
.donotgrid--elig li.is-no{opacity:.78}


/* ===== QA 2026-10-10 - the steps rail on wrapped rows ==================
   .steps--media.steps--rail drew ONE absolutely-positioned dotted line on
   the container, inset from each edge. That spans the first row only, so
   any grid that wraps - six steps in three columns, five in four - left
   04/05/06 with no connector at all. Six instances across six pages.

   Drawn per item instead: each step runs a line from its own centre to
   the next step's centre (its width plus the 20px grid gap), suppressed
   on the last item of each row and on the last item overall. Row count
   stops mattering. Same 2px dotted var(--rule), same top:24px, same
   z-index:1 under the number badge, and still hidden under 900px where
   the grid goes single-column. */
.steps--media.steps--rail::before{content:none}
.steps--media.steps--rail .steps__item{position:relative}
.steps--media.steps--rail .steps__item::before{
  content:"";position:absolute;top:24px;left:50%;width:calc(100% + 20px);
  border-top:2px dotted var(--rule);z-index:1;pointer-events:none}
.steps--media.steps--rail:not(.steps--3) .steps__item:nth-child(4n)::before,
.steps--media.steps--rail.steps--3 .steps__item:nth-child(3n)::before,
.steps--media.steps--rail .steps__item:last-child::before{content:none}
@media (max-width:900px){
  .steps--media.steps--rail .steps__item::before{content:none}
}


/* ===== QA 2026-10-10 - steps go vertical on mobile ====================
   The base sheet already intends this: line 1539 carries
   @media (max-width:900px){.reach,.steps{grid-template-columns:1fr}}.
   But .steps--media re-declares 4 columns ~650 lines later, and its own
   900px override only goes to 2 - so the collapse never reached the
   rail variant and the grid stayed 3 across on a phone, with the first
   column rendering 57px wide and single words per line.

   One column for every .steps variant below 900px. The badge already
   sits above left-aligned text there (line 2140), and the connector is
   already suppressed, so nothing else needs to change. */
@media (max-width:900px){
  .steps,.steps--media,.steps--media.steps--3,
  .steps--media.steps--rail,.steps--media.steps--rail.steps--3,
  .steps--media.steps--centre,.steps--media.steps--centre.steps--3,
  .steps--media.steps--rail.steps--centre{grid-template-columns:1fr}
  .steps--media > .steps__item,
  .steps--media.steps--centre > .steps__item{grid-column:auto}
  /* The orphan-centring rules above pin an explicit grid-column on the
     item that starts an incomplete row, at (0,5,1). The existing 760px
     reset is only (0,3,1), so that item kept its placement on a phone and
     forced the grid open again - which is why one column rendered 57px
     wide. Same selectors restated here, later in the file, so they lose
     on source order rather than by escalating with !important. */
  .steps--media.steps--centre:not(.steps--3) > .steps__item:nth-child(4n+1):nth-last-child(2),
  .steps--media.steps--centre:not(.steps--3) > .steps__item:nth-child(4n+1):nth-last-child(1),
  .steps--media.steps--centre.steps--3 > .steps__item:nth-child(3n+1):nth-last-child(2),
  .steps--media.steps--centre.steps--3 > .steps__item:nth-child(3n+1):nth-last-child(1){
    grid-column:auto}
}


/* ===== QA 2026-10-10 - "Where We Work" as two columns =================
   .coverage is a four-column grid at full width; in half a .mediasplit it
   has to drop to three (operator, 2026-10-10), to two once the split
   stacks, and to one on a phone. */
.mediasplit--coverage .coverage{grid-template-columns:repeat(3,1fr)}
/* the row is now just the list and its paragraph, so the map
   stretches to exactly that - no min-height, and no 71px lead-in
   from .mapwrap--fill, because the heading is no longer in the row. */
/* stretched to the list row, then capped - operator asked for it shorter
   again. align-self:stretch with a max-height sits it at the top of the
   row, so it still starts level with the first town. */
/* .coverage carries margin-top:16px, so the list starts 16px below the
   row. The map was aligned to the ROW, which left it hanging above the
   first town and finishing short of the last line. Same 16px here lines
   its top edge up with Twinsburg. */
.mediasplit--coverage .mapwrap{align-self:stretch;margin-top:16px;
  min-height:0}
/* the closing paragraph carries its own margin-bottom:34px, which made the
   column - and so the stretched map - 34px taller than the text it sits
   beside. Nothing follows it inside the split any more. */
.mediasplit--coverage .mediasplit__body > *:last-child{margin-bottom:0}
/* Between 901 and 1100 the split is still side-by-side but the text
   column is only ~400px, and three town names per row wrap. Two there. */
@media (max-width:1100px){
  .mediasplit--coverage .coverage{grid-template-columns:repeat(2,1fr)}
}
/* .mediasplit collapses at 880px, not 900 - once it stacks the list has
   the full container back and three fit again. Matching 880 exactly:
   at 900 the split is still side-by-side and three-up wrapped every
   label on all 31 pages. */
@media (max-width:880px){
  .mediasplit--coverage .coverage{grid-template-columns:repeat(3,1fr)}
}
@media (max-width:560px){
  .mediasplit--coverage .coverage{grid-template-columns:1fr}
}

/* ===== QA 2026-10-10 - sticky media columns that never stuck ==========
   body{overflow-x:hidden} in the approved sheet makes <body> a scroll
   container, so position:sticky resolves against it, not the viewport.
   body.hub{overflow-x:clip} already fixes that, but 41 pages carry
   .mediasplit--sticky and only 31 carry .hub. Keyed off the component so
   every page that uses it is covered, now and later. */
body:has(.mediasplit--sticky){overflow-x:clip}


/* ===== QA 2026-10-10 - 4-up tiles centre their last row ===============
   --2 and --3 are centred flex rows precisely so an incomplete last row
   CENTRES instead of hanging left. --4 was left as the original grid, so
   a 7-tile set had nowhere to go: 3+3+1 under --3 (a lone card), and 4+3
   under --4 would have hung left with a hole in the fourth column. Same
   pattern, same arithmetic - 4 x basis + 3 x 18px gap = 100% - so the
   complete-row instance on /twinsburg-oh/ renders exactly as before. */
.nexttiles--4{display:flex;flex-wrap:wrap;justify-content:center;gap:18px}
.nexttiles--4 > *{flex:0 1 calc((100% - 54px) / 4)}
@media (max-width:1000px){.nexttiles--4 > *{flex-basis:calc((100% - 18px) / 2)}}
@media (max-width:640px){.nexttiles--4 > *{flex-basis:100%}}


/* ===== QA 2026-10-10 - a tile left alone on its row spans that row ====
   Choosing the column count per grid fixes the lone card at desktop, but
   the ladder re-creates it lower down: --3 and --4 both drop to 2-up at
   <=1000px, so EVERY odd-count grid orphans again between 641 and 1000.
   21 of them did.

   Column count is what decides whether the last tile lands alone, and it
   differs per modifier and per breakpoint, so the "alone" case is spelled
   out for each: alone at 3-up means nth-child(3n+1), at 4-up 4n+1, at
   2-up simply odd. Such a tile takes the whole row rather than floating
   centred under the gap - a deliberate wide card instead of a leftover.
   Below 641px every tile is already full width, so nothing to resolve. */
.nexttiles--2 > :last-child:nth-child(odd),
.nexttiles--3 > :last-child:nth-child(3n+1),
.nexttiles--4 > :last-child:nth-child(4n+1){flex-basis:100%}

@media (max-width:1000px){
  /* 2 columns here, so the 3-up and 4-up cases no longer apply and have
     to be put back on the 2-up basis before the odd case is applied -
     they are more specific than the plain `> *` basis rules. */
  .nexttiles--3 > :last-child:nth-child(3n+1),
  .nexttiles--4 > :last-child:nth-child(4n+1){
    flex-basis:calc((100% - 18px) / 2)}
  .nexttiles--2 > :last-child:nth-child(odd),
  .nexttiles--3 > :last-child:nth-child(odd),
  .nexttiles--4 > :last-child:nth-child(odd){flex-basis:100%}
}

@media (max-width:640px){
  .nexttiles--2 > :last-child:nth-child(odd),
  .nexttiles--3 > :last-child:nth-child(3n+1),
  .nexttiles--3 > :last-child:nth-child(odd),
  .nexttiles--4 > :last-child:nth-child(4n+1),
  .nexttiles--4 > :last-child:nth-child(odd){flex-basis:100%}
}


/* ===== QA 2026-10-10 - /why-choose-us/ "go all the way across" =======
   .sechead is capped at 62ch (567px on this page) and .claims at 960px,
   inside a 1232px content width, so the heading and its two intro lines
   wrapped at well under the width of the list below them. Scoped to the
   one section that asked for it rather than lifting the sitewide
   measure, which every other page is set by. */
.section--fullmeasure > .container > .sechead,
.section--fullmeasure > .container > p{max-width:none}
.section--fullmeasure .claims{max-width:none}


/* ===== QA 2026-10-11 - table spacing inside a .dec block ==============
   Part 2 of /mesh-vs-solid-safety-covers/ had its image column removed
   and its .decsplit grid unwrapped, so the table now sits directly in
   .dec. The grid's row gap went with it, and .decgrid > p carries
   margin:0, so the paragraphs ran hard against the table's bottom rule -
   measured 0px, against 22px after the .ctable-wrap on
   /services/pool-safety-covers/. 22px matches that control. */
.ctable-wrap:has(+ .decgrid){margin-bottom:22px}  /* re-pointed: .dec is gone */


/* ===== QA 2026-10-11 - show a map whole, not cropped ==================
   .mediasplit__media img is aspect-ratio:16/10 + object-fit:cover, which
   is right for a photograph and wrong for a map: ne-ohio-map.webp is
   1360x771 (1.76), so cover trimmed the sides and took the map's own
   title with it - "Northeast Ohio / 5 Counties, 17 Great Communities"
   rendered as "ortheast Ohio".

   The build has no no-crop convention; every image slot crops. So this
   is a named modifier in the same style as --fill / --sticky / --tall,
   applied to the one media column that holds a map. Letting the image
   keep its intrinsic ratio means no crop AND no letterboxing. */
.mediasplit__media--whole img{aspect-ratio:auto;height:auto;
  object-fit:contain}

/* ---- .colstack: a split column of "image then cards" --------------
   /services/commercial-pool-maintenance/ "What a Commercial Visit
   Covers". The column has to start at the heading's intro line AND
   finish level with the last line of the text column. .split's
   align-items:center can do neither: centring places the block, it
   cannot span one, so pinning the top left a gap at the bottom and
   pinning the bottom lifted the top back over the heading.
   align-self:stretch gives the column the full row height, the tiles
   sit at its foot, and the figure takes flex:1 - so the IMAGE absorbs
   any change in the text column's height rather than a fixed ratio
   having to predict it. 901px mirrors the global collapse at
   max-width:900px; below it the columns stack, the pad would be a
   hole above the image, and the ratio takes over again.
   These styles are here rather than inline because an inline
   aspect-ratio beats a stylesheet rule without !important. */
.colstack > figure{margin:0 0 18px}
.colstack > figure > img{width:100%;aspect-ratio:11/5;object-fit:cover;
  border-radius:var(--r-sig)}
@media (min-width:901px){
  .split > .colstack{display:flex;flex-direction:column;align-self:stretch;
    padding-top:var(--pad,121px)}
  .split > .colstack > figure{flex:1 1 0;min-height:140px;display:flex;
    overflow:hidden}
  .split > .colstack > figure > img{height:100%;aspect-ratio:auto}
}
/* The H2 takes a second line somewhere in the 1000s - at 1027px on the
   commercial page, elsewhere not at all - so the narrow value is per page
   too. Same fallback chain. */
@media (min-width:901px) and (max-width:1027px){
  .split > .colstack{padding-top:var(--pad-sm,var(--pad,155px))}
}

/* .ctarow puts its body and its button on one flex row and lets the row
   wrap. Giving the body the full basis makes the button wrap to the next
   line on its own - the component's own behaviour, not a new layout. The
   row is justify-content:space-between, so a button alone on the second
   line sits at the start, under the text. Below 901px .ctarow already
   wraps anyway, so this changes nothing there. */
.ctarow--stack > .ctarow__body{flex-basis:100%}

/* ===== /specials/ - pin the figure beside the price table ==============
   .split is align-items:center, so a 495px figure floats in the middle of
   a 917px text column. align-self:start puts it at the top of the row and
   position:sticky holds it while the table and the terms scroll past.
   top:96px is the house offset, the same value .mediasplit--sticky uses.

   body{overflow-x:hidden} in the approved sheet makes <body> a scroll
   container, and sticky then resolves against THAT rather than the
   viewport - the documented reason .mediasplit--sticky never actually
   stuck. overflow-x:clip clips identically without creating one. Keyed off
   the component, exactly as the .mediasplit--sticky fix above is, so any
   later page using it is covered.

   Below 900px .split is one column; a pinned figure there would hang over
   the copy, so it goes static - mirroring .mediasplit--sticky's own
   breakpoint behaviour. */
.split > .stickyfig{align-self:start;position:sticky;top:96px;
  margin:192px 0 0}
/* The figure should start level with the price TABLE, not with the
   heading - so the offset is the height of the sechead plus the lead
   line above the table, and that changes when the H2 wraps to a
   second line at 1200px and below. Measured at each step, not
   guessed: 192px holds from 1280 to 1440, 218 at 1200, 255 at 1100.
   (115px, dialled in devtools, was measured against the pre-sticky
   layout where the figure was still centred.) */
@media (max-width:1279px){.split > .stickyfig{margin:218px 0 0}}
@media (max-width:1150px){.split > .stickyfig{margin:255px 0 0}}
body:has(.stickyfig){overflow-x:clip}
@media (max-width:900px){.split > .stickyfig{position:static;margin:0}}

/* ===== /specials/ - start the figure at the heading, not above it ======
   .split is align-items:center, so the figure's top drifts with the text
   column's height: measured +6px from the column top at 1440 but +104px at
   950, while the H2 sits at a constant +63px. No single margin can track
   that. align-self:start pins the figure to the row top and the 63px puts
   its first pixel on the H2's, at every width - the wavemark and eyebrow
   above it are deliberately skipped, which is the same thing the house
   .mediasplit media rule does with its own fixed drop.
   Stacked below 900px the figure follows the copy, so the drop goes. */
/* The row must top-align BOTH columns. Pinning only the figure made
   its margin-box the tallest thing in the row (63 + 382 = 445 against
   a 395px text column), and the still-centred text column then
   dropped 25px - moving the heading down, not the image up. */
.split:has(> .headalign){align-items:start}
.split > .headalign{align-self:start;margin:63px 0 0}
@media (max-width:900px){.split > .headalign{margin:0}}

/* ===== QA 2026-10-11 - a four-tile exclusions grid wraps 3 + 1 =========
   The 3-up basis above was sized for the narrow right-hand column, which
   is right for three tiles and wrong for four: the fourth has nowhere to
   go and hangs alone under the first column. Exactly four tiles go 2 x 2
   instead. ":first-child:nth-last-child(4)" matches only when the list
   holds precisely four, so the three-tile grids and the one eight-tile
   grid keep the basis they have.
   The gap arithmetic mirrors the rules above at each step: 18px at
   desktop, 12px below 900px, and one per row below 560px as before. */
.mediasplit--elig .donotgrid li:first-child:nth-last-child(4),
.mediasplit--elig .donotgrid li:first-child:nth-last-child(4) ~ li{
  flex:0 1 calc((100% - 18px) / 2)}
@media (max-width:560px){
  .mediasplit--elig .donotgrid li:first-child:nth-last-child(4),
  .mediasplit--elig .donotgrid li:first-child:nth-last-child(4) ~ li{
    flex:0 1 100%}
}

/* ===== QA 2026-10-11 - square town maps cropped in a 16/10 slot =======
   The town maps are square (viewBox 0 0 256 256) and .mediasplit__media
   img is 16/10 with object-fit:cover, so only 63% of the artwork showed -
   the title pill and the "M&G Pools Service" label were cut off top and
   bottom. The section height is fixed by the text column and must not
   grow (a square slot would add 217-279px at 1440), so the map is
   CONTAINED instead: it renders 367x367, whole, centred, at exactly the
   height it already occupied. The band shows either side of it, which is
   the cost of a square asset in a landscape slot.
   Scoped to the four maps carrying the class, not to every map. */
.mediasplit__media img.mapwhole{object-fit:contain;
  background:linear-gradient(180deg,#1E1E3A 0%,#2B2B5C 100%)}
/* The band either side of the contained map is filled with the map's
   OWN card gradient, read out of the four SVGs - all share
   <linearGradient id="bgGradient"> #1E1E3A -> #2B2B5C, top to
   bottom. The artwork and the box are the same height, so the two
   gradients run over the same distance and meet without a seam:
   the slot reads as one card instead of a small card in a white
   box. #1E1E3A is the brand navy; #2B2B5C is taken from the
   approved asset rather than introduced here. */

/* ===== QA 2026-10-11 - a grid holding exactly ONE tile ================
   The lone-card rules above give a leftover tile the full row, which is
   right when it is the remainder of a longer list - a deliberate wide
   card rather than an orphan. A grid with only ONE tile in it is not that
   case: stretched full width it reads as a banner, not a card. It keeps
   the two-up basis instead and .nexttiles--2's own justify-content:center
   centres it. ":first-child:nth-last-child(1)" matches only when the grid
   holds precisely one, so every multi-tile grid is untouched.
   Repeated inside the 1000px query because the rule it overrides is
   itself in that query; below 640px every tile is full width anyway. */
.nexttiles--2 > :first-child:nth-last-child(1){
  flex-basis:calc((100% - 18px) / 2)}
@media (max-width:1000px){
  .nexttiles--2 > :first-child:nth-last-child(1){
    flex-basis:calc((100% - 18px) / 2)}
}
@media (max-width:640px){
  .nexttiles--2 > :first-child:nth-last-child(1){flex-basis:100%}
}

/* ── /services/pool-safety-covers/ slider depth — operator, 2026-10-12 ─────
   The full-width slider on this page stood 692px tall at the approved 16/9
   while its six sibling service hubs run 538px at 16/7 — a 154px outlier,
   and the only full-width slider in the build still at 16/9.

   Deliberately NOT done by adding .hub to the body: that would also apply
   body.hub{overflow-x:clip} to this page, a scroll-containment change that
   was not asked for, and would re-open the 2026-10-07 decision to keep the
   two approved slider pages off the hub rules. A modifier moves this one
   slider and leaves /pool-safety-cover-buying-guide/ at 16/9 as approved.

   (0,2,1) — beats the approved sheet's .slider__slide img at (0,1,1). */
.slider--shallow .slider__slide img{aspect-ratio:16/7}

/* ── /404 and /thank-you/ — utility-page layout — operator, 2026-10-12 ─────
   Rebuilt to the reference layout the operator supplied: the eyebrow sits
   OVER a short accent rule rather than beside one, the H1 blocks into two
   lines instead of running the full 1232px container, the body copy takes
   a reading measure, and /404 offers a row of quick links above the
   buttons. No pill component existed in the build, so .quicklinks is new.

   Layout only. Every word on both pages is M & G's own, and /thank-you/
   keeps its nine cloned body words untouched — its content doc carries the
   client instruction "keep as-is" under a clone-fidelity waiver, so no
   paragraph was added there even though the reference carries two.

   Button variants are deliberately unchanged: which fill a button takes is
   a colour-role decision in the approved system, not layout.

   Scoped to .hero--utility, which exists on exactly these two pages. */
/* 4px sat between the breadcrumb and the eyebrow, which read as one
   cluttered stack of three small things; the reference gives that block
   air before the H1 lands. inline-block, so this margin cannot collapse
   into the crumbs above. */
.hero--utility .hero__eyebrow{
  display:inline-block;position:relative;padding-bottom:13px;
  margin-top:26px;margin-bottom:26px}
/* the rule matches the LABEL width, as the reference does - the
   inline-block eyebrow hugs its text, so 100% is that text. The
   34px floor is the build's own original rule width, which keeps
   a short label like "404" from carrying a stub. */
.hero--utility .hero__eyebrow::before{position:absolute;left:0;bottom:0;width:100%;min-width:34px}
.hero--utility h1{max-width:20ch}
/* 58ch broke "...to the right page." onto a second line: the sentence
   measures 595px and 58ch is 564px. 62ch is the exact fit, 64ch
   leaves headroom so a slightly wider font render cannot re-wrap it. */
.hero--utility .hero__sub{max-width:64ch}

.quicklinks{display:flex;flex-wrap:wrap;gap:10px;margin:0 0 30px;padding:0;
  list-style:none}
.quicklinks a{display:inline-block;padding:9px 19px;border-radius:999px;
  border:1px solid rgba(255,255,255,.45);color:var(--white);
  font-size:.9375rem;font-weight:600;text-decoration:none;
  transition:background-color .18s,border-color .18s,color .18s}
.quicklinks a:hover,.quicklinks a:focus-visible{
  background:var(--white);border-color:var(--white);color:var(--ink-navy)}
.quicklinks a.quicklinks__accent{color:var(--sunshine);border-color:var(--sunshine)}
.quicklinks a.quicklinks__accent:hover,
.quicklinks a.quicklinks__accent:focus-visible{
  background:var(--sunshine);color:var(--ink-navy);border-color:var(--sunshine)}
@media (max-width:560px){
  .quicklinks{gap:8px}
  .quicklinks a{padding:8px 15px;font-size:.875rem}
}

/* ── /404 and /thank-you/ — white header — operator, 2026-10-12 ───────────
   The reference pages carry a solid white header. The build already has
   that exact treatment as .header.is-stuck, the scrolled state: white
   bar, navy nav (17.39:1), blue logo variant, Sunshine rule beneath.
   Reused verbatim rather than invented, and pinned on for these two pages.

   NOT applied by writing is-stuck into the markup: the inline toggle is
   `h.classList.toggle('is-stuck', scrollY > 90)`, which would strip a
   hand-placed class the first time a visitor scrolled back to the top.

   The Sunshine underline keeps its 10.67:1 here because the dark hero is
   still directly below it - the condition the approved sheet's own note
   says that 2px line depends on. It would NOT have survived the full
   light-page version of this redesign.

   body.thanks is on exactly these two pages and nowhere else. */
body.thanks .header{
  position:fixed;top:0;background:var(--white);
  box-shadow:0 2px 18px rgba(30,30,58,.13);
  border-bottom:2px solid var(--sunshine)}
body.thanks .header .header-inner{padding-block:10px}
body.thanks .header .brand img{height:54px}
body.thanks .header .brand__white{display:none}
body.thanks .header .brand__blue{display:block}
body.thanks .header .nav > ul > li > a{color:var(--ink-navy)}
body.thanks .header .nav > ul > li > a:hover{color:var(--primary)}
body.thanks .header .nav > ul > li > a::after{background:var(--primary)}
body.thanks .header .burger{color:var(--ink-navy);border-color:rgba(0,0,0,.22)}
body.thanks .header .mega{margin-top:1px}

/* -- /404 and /thank-you/ - white page - operator, 2026-10-12 -------------
   Hero ground to Brand Guide White, the CTA band below removed, and the
   breadcrumb dropped (the reference carries none).

   Same approach as the body.thanks .cta-band block above: the photo and
   its overlay come OFF rather than being covered, and every on-dark
   treatment flips - heading, body copy, eyebrow and its rule, the quick
   links, and .btn--ghost-dark, whose Sunshine fill reads 1.51:1 on white.
   It becomes the outline button the light bands already use, which is
   exactly what the CTA-band block above does with that same button.

   The eyebrow goes to Primary Blue, not the aqua it carries on dark:
   aqua is ~3:1 on white and this is 13px text.

   .crumbs was carrying padding-top:150px (118 at <=900px) and that is
   what held the content clear of the fixed 76px header. With the
   breadcrumb gone, that padding moves to the hero.

   The Sunshine rule under the header now sits against white rather than
   the dark hero, so it reads as a quiet brand line rather than a hard
   edge; the header's own box-shadow is what separates it from the page.

   .wavediv is a white wave drawn to carry a DARK hero into a light
   section. With a white hero and no section beneath it, it is white on
   white with nothing to transition. */
body.thanks .hero{background:#FFFFFF}
body.thanks .hero::before{display:none}
body.thanks .wavediv{display:none}
body.thanks .hero--inner{padding-top:150px}
@media (max-width:900px){body.thanks .hero--inner{padding-top:118px}}

body.thanks .hero h1{color:var(--ink-strong)}
body.thanks .hero .hero__sub{color:var(--ink)}
body.thanks .hero .hero__eyebrow{color:var(--primary)}
body.thanks .hero .hero__eyebrow::before{background:var(--primary)}

body.thanks .quicklinks a{color:var(--ink-navy);border-color:rgba(30,30,58,.28)}
body.thanks .quicklinks a:hover,body.thanks .quicklinks a:focus-visible{
  background:var(--ink-navy);border-color:var(--ink-navy);color:var(--white)}
body.thanks .quicklinks a.quicklinks__accent{
  color:var(--primary);border-color:var(--primary)}
body.thanks .quicklinks a.quicklinks__accent:hover,
body.thanks .quicklinks a.quicklinks__accent:focus-visible{
  background:var(--primary);border-color:var(--primary);color:var(--white)}

/* /thank-you/ has no sub-paragraph, so the button row sat on h1's own
   18px margin - tight under a 54px heading. The adjacent-sibling selector
   matches ONLY where the buttons follow the heading directly, which is
   that page; on /404/ the quick links sit between the two and keep their
   own spacing. Sibling margins collapse, so this is the gap, not 40+18. */
body.thanks .hero h1 + .hero__cta{margin-top:40px}

body.thanks .hero .btn--ghost-dark{
  background:transparent;color:var(--primary);border-color:var(--primary)}
body.thanks .hero .btn--ghost-dark:hover{
  background:var(--primary);color:var(--white);border-color:var(--primary)}

/* -- "What an Ohio Winter Does to a Cover" section head - operator,
   2026-10-13, set in DevTools -------------------------------------------
   62ch put the intro on four line-boxes in a 567px measure and left a 44px
   gap above the tile grid. 75ch (686px) and a 14px gap were chosen on the
   live page.

   A modifier, not a change to .sechead: that base rule is every section
   head on all 136 pages. Same shape as the sheet's own .sechead--wide,
   which is (0,1,0) like .sechead and wins only by sitting after it -
   client_custom.css loads after the global sheet, so this does too. */
.sechead--wide-tight{max-width:75ch;margin-bottom:14px}

/* -- /service-areas/ town pins: optical alignment - operator, 2026-10-14 ---
   An inline <svg> sits with its BOTTOM on the text baseline, so the 15px
   pin stood 1.5px above the centre of the 12px capitals beside it -
   measured, not estimated: cap height from TextMetrics, pin centre from
   its client rect, across all 17 tiles.

   -1.5px brings every one of the 17 to an offset of exactly 0.

   vertical-align rather than the flex centring the mega menu and
   .coverage use for the same glyph: those flex the LINK, which here also
   holds the tile photo, so flexing would stand the image beside the text.
   Making the <b> itself a flex box would change how a two-word name like
   "Orange Village" wraps. This moves the glyph and nothing else.

   Matches only an <svg> that is a direct child of a tile's name - 17
   elements in the build, all on /service-areas/. */
.nexttiles--media .nexttiles__txt b > svg{vertical-align:-1.5px}

/* -- icons on .tile - operator, 2026-10-14 -------------------------------
   "What gets recorded?" on /twinsburg-oh/pool-safety-covers/ carried
   photos; the operator asked to see it on icons instead and kept them.

   No .tile in the build carried an icon before this (0 of 105), so the
   ring is copied from .donotgrid__ic rather than invented: 54px circle,
   Sky Blue at .16 over a .42 border, glyph at --txt-accent, and the same
   hover shift to --bar-hover. The glyphs sit on the 24 viewBox at stroke
   1.7 the sheet's own icons use.

   Scoped to .tile inside .nexttiles, and only tiles that carry a .tileic
   span - 5 elements in the build today. */
.nexttiles .tile .tileic{width:54px;height:54px;border-radius:50%;
  background:rgba(40,145,243,.16);border:1px solid rgba(40,145,243,.42);
  color:var(--txt-accent);display:grid;place-items:center;margin:0 0 14px;
  transition:background-color .18s,border-color .18s,color .18s}
.nexttiles .tile .tileic svg{width:24px;height:24px}
.nexttiles .tile:hover .tileic{border-color:rgba(255,255,255,.45);
  color:var(--white)}

/* -- LSP article figures to 16/7 - operator, 2026-10-14 ------------------
   The figure on /twinsburg-oh/pool-safety-covers/ was cut to 16/7 to match
   the .slider--shallow ratio, and the operator asked for every Location x
   Service page to follow.

   LSP pages had NO body class, so `lsp` is added to the 33 of them for
   this. Same convention as body.hub on the service pages, which this file
   already scopes two blocks with. The class was unused in both stylesheets
   and in all markup before this.

   One rule rather than 26 inline styles - and the inline value that was on
   the Twinsburg figure is removed, so the ratio lives in exactly one place.

   .artfig is on 59 pages in all; blog posts and guides keep the approved
   16/9. Only aspect-ratio is set, so width, object-fit, the signature
   corner and the border all still come from the component. */
body.lsp .artfig img{aspect-ratio:16/7}

/* -- .step's hover on every boxed card - QA (Cha), operator 2026-10-14 --
   Copied verbatim from the .step rules in the approved sheet, not
   rebuilt: the gradient is the same two Brand Guide stops at the same
   135deg, faded by an overlay's opacity (a background-image cannot be
   transitioned), the children are lifted clear of it, and the white text
   comes from flipping the four band-role tokens exactly as .step does.

   ACCEPTED AA EXCEPTION. On hover the text is white on the gradient and
   measures 3.26:1 at the blue end, 4.18:1 at the violet. The card h3 is
   17px/700 and the body 15px/400 - both under the 18.66px-bold large-text
   threshold, so both need 4.5:1 and neither reaches it. The reference
   card on /contact/ has always had this; the operator asked for the hover
   to stay identical to it and for the shortfall to be recorded rather
   than designed around. Logged in 00_Build_Standards.md.
   Rest state is untouched: body copy stays 16.13:1, headings 5.86:1.

   Image tiles (.nexttiles--media .tile) are excluded by the brief, and
   .scard is excluded because the sheet records an operator decision that
   the two .scard cards on /services/pool-safety-covers/ keep their own
   hover. */
.donotgrid:not(.donotgrid--serviced) li,
.reach > a, .reach > div,
.nexttiles:not(.nexttiles--media) .tile{
  position:relative;isolation:isolate;overflow:hidden}

.donotgrid:not(.donotgrid--serviced) li::before,
.reach > a::before,
.reach > div::before,
.nexttiles:not(.nexttiles--media) .tile::before{
  content:"";position:absolute;inset:0;z-index:0;border-radius:inherit;
  background:linear-gradient(135deg,var(--primary-hover) 0%,var(--sky) 100%);
  opacity:0;transition:opacity .22s}

.donotgrid:not(.donotgrid--serviced) li:hover::before,
.reach > a:hover::before,
.reach > div:hover::before,
.nexttiles:not(.nexttiles--media) .tile:hover::before{opacity:1}

.donotgrid:not(.donotgrid--serviced) li > *,
.reach > a > *,
.reach > div > *,
.nexttiles:not(.nexttiles--media) .tile > *{position:relative;z-index:1}

.donotgrid:not(.donotgrid--serviced) li:hover,
.reach > a:hover,
.reach > div:hover,
.nexttiles:not(.nexttiles--media) .tile:hover{
  background:var(--surface);border-color:transparent;
  --txt:var(--white);--txt-strong:var(--white);--ink-strong:var(--white);
  --txt-accent:var(--white)}

/* --ink-navy only where the census found it read: the light-theme rules
   `.theme-light :is(.band-navy,.band-lift) .reach.reach--2 > div:hover
   :is(b,.reach__ic){color:var(--ink-navy)}`. It must NOT go on the other
   families - .donotgrid--elig's status badge takes `color:var(--ink-navy)`
   on a disc whose background is var(--txt-accent), so flipping both left a
   blank white circle on the gradient. */
.reach > a:hover,
.reach > div:hover{--ink-navy:var(--white)}


/* -- links inside a gradient-filled card - 2026-10-14 -------------------
   .tlink is coloured by `.band-light .tlink{color:var(--link-on-light)}`,
   and --link-on-light is var(--primary) - #4B4DED on the gradient, which
   is the one thing on these cards that still read as a dark mark after
   the token flip. Class doubled so this also clears the (0,4,0)
   `.theme-light :is(.band-navy,.band-deep) .tlink` rule.
   Scoped to the hovered card only; .tlink everywhere else is untouched. */
.donotgrid li:hover .tlink.tlink,
.nexttiles:not(.nexttiles--media) .tile:hover .tlink.tlink{
  color:var(--white)}

/* -- "In-ground pools only" status treatment - operator, 2026-10-16 -----
   From a supplied reference: emphasis on "only", a red X on the two
   tiles that are not serviced, and a status label under each name.

   SEMANTIC RED, --no-red. Red is NOT in the locked brand palette; the
   operator approved it on 2026-10-16 as a status signal only - never as
   a brand colour, never on a CTA, never as a surface. Logged in
   00_Build_Standards.md. #E03131 is a flat red that holds its own
   against the Deep Indigo band without going pink on the gradient.

   Everything is scoped to .donotgrid--serviced, which only the 29
   "In-ground pools only" grids carry. The same component builds a "What
   We Do Not Do" section on the 17 location hubs, where "NOT SERVICED"
   would be the wrong words - those are untouched.

   On hover the tile fills with the signature gradient and the labels go
   white with the rest of the text; the red disc stays red and carries
   the signal, which is why the badge colour is a literal and not one of
   the flipped tokens. */
:root{
  --no-red:#E03131;      /* badge fill: white glyph on it = 4.51:1      */
  --no-red-ink:#FF6B6B;  /* 11px label on the tile ground = 5.27:1      */
  --yes-ink:#5AA9F7;     /* ditto, Sky tint; --sky itself is 4.48 = AA fail */
}

.elig-only{color:var(--sky)}

/* The `> *` lift above sets position:relative on every direct child,
   which silently beat .donotgrid__state's own position:absolute (0,1,1 or
   0,2,1 against 0,1,0). The badge has been in normal flow since the card
   hover landed - stacked over the icon, pushing each tile's contents down
   32px. Symmetric, so it read as the design until one tile was freed.
   Position only; z-index:2 so it also clears the gradient where one is
   still painted. */
.donotgrid li .donotgrid__state{position:absolute;z-index:2}

.donotgrid--serviced li.is-no .donotgrid__state{
  background:var(--no-red);border-color:transparent;color:var(--white)}

.donotgrid--serviced .elig-state{
  display:block;margin-top:5px;font-size:.6875rem;font-weight:700;
  letter-spacing:.07em;text-transform:uppercase}
.donotgrid--serviced li.is-yes .elig-state{color:var(--yes-ink)}
.donotgrid--serviced li.is-no .elig-state{color:var(--no-red-ink)}

/* ===== QA 2026-10-23 - "Where We Work" towns link to their location pages ====
   Ammar, QA item 1. The 17 towns on the homepage, the 7 service hubs and the
   24 service spokes are now anchors to /{town}-oh/ (544 links in all).

   Two rules are needed, and neither is cosmetic.

   1. There is no global a{text-decoration:none} on this build - every
      component sets it for itself (.tlink, .nav a, .crumbs a, .napcard a) -
      so without this the 544 new links would all render underlined.
      The anchor also takes over the li's flex box (the li is already
      display:flex;align-items:center;gap:10px) so the row renders exactly as
      before while the WHOLE row becomes the click target, pin included.

   2. The light-band link mirror repaints every anchor on a lift/navy/deep
      band to --link-on-light (#4B4DED):

        .theme-light :is(.band-navy,.band-deep):not(.band--keepdark) a
        .theme-light .band-lift:not(.band--keepdark) a

      at (0,3,1), which beats a plain .coverage li a at (0,1,2). Left alone it
      turned all 544 towns indigo, and the approved design has them in body
      navy with .coverage li:hover swapping them to the accent - so the hover
      would have become a no-op too. The coverage lists live in exactly two
      band contexts on this build: "band-lift band--tint" (24 pages) and
      "band-navy" (8). Both are mirrored here, in the same shape the global
      sheet already uses to exempt a.btn.btn--outline from the same rule.
      color:inherit keeps .coverage li:hover driving the colour, as before. */
.coverage li a{display:flex;align-items:center;gap:10px;flex:1;
  color:inherit;text-decoration:none}
.theme-light .band-lift:not(.band--keepdark) .coverage li a,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .coverage li a,
.band-light .coverage li a{color:inherit}

/* ===== QA 2026-10-23 - footer SERVICES column fits its longest label ========
   Ammar, QA item 3. "Pool Equipment Repair & Replacement" (fix 2) needs 271px
   on one line at 15px Open Sans; the shipped grid gave the column 233px, so it
   wrapped to two lines.

   The global sheet is byte-verified against r13 and must not be edited, so the
   override lives here. Global rule, for reference:
     .footer__grid{grid-template-columns:1.5fr 1fr 1fr 1.15fr;gap:44px}  (L1137)
   Column 1 is the brand block, 2 SERVICES, 3 COMPANY, 4 GET IN TOUCH.

   Scoped to >=1150px deliberately. The grid stays 4-up all the way down to
   901px, but below ~1150 there is not enough width for four columns AND 271px:
   measured at 901px, forcing SERVICES to 275px squeezed COMPANY to 104px and
   wrapped "Brands We Service" (130px) instead - trading one wrapped label for
   another. Below this breakpoint the global ratio is left alone, where the
   label wraps exactly as it already did before this fix, so nothing regresses.

   Untouched: gap, padding, and both collapse breakpoints (2-up <=900px,
   1-up <=640px). */
@media (min-width:1150px){
  .footer__grid{grid-template-columns:1.25fr 1.35fr 0.9fr 1.15fr}
}

/* ===== QA 2026-10-23 - footer bottom bar centred on mobile ==================
   Ammar, QA item 5. The copyright line, the Privacy/Terms pair and the
   "Powered by Efficiency Boss" mark sat hard left on a phone with 87-122px of
   dead space to their right.

   .footer__bar is a wrapping flex row. The global sheet sets it to
   space-between, then overrides to flex-start at <=640px (L~1231), which is
   what produced the left edge. Same breakpoint and same specificity here, and
   client_custom.css loads after the global sheet, so this wins on order.

   justify-content alone centres each wrapped LINE; text-align handles the
   copyright sentence if it ever wraps to two lines, and the inner
   justify-content keeps the two legal links centred if they wrap apart on a
   very narrow phone.

   BREAKPOINT WIDENED 640 -> 900 on 2026-10-24 (Ammar QA r2 item 6, re-raised).
   640 was wrong: it matched the global sheet's existing breakpoint rather than
   the width where this bar actually needs centring. Measured, the bar wraps onto
   two lines from roughly 850px down, and the sticky mobile CTA bar appears below
   769px - so between 641 and 768 the page reads as mobile (sticky bar up, footer
   already stacked) while the footer stayed hard left. That band is what Ammar
   was looking at, which is why the round-1 fix looked like it had not been
   applied. 900 covers both the wrap band and the sticky-bar band; the one-line
   desktop layout above it keeps space-between. */
@media (max-width:900px){
  .footer__bar{justify-content:center}
  .footer__bar p{text-align:center}
  .footer__legal{justify-content:center}
  .footer .eb{justify-content:center}
}

/* ===== QA 2026-10-23 - /about/ founder portrait vertical position ===========
   Ammar, QA item 7, value verified by the operator in DevTools: margin-top:24%
   drops the portrait so its top edge sits level with the first paragraph rather
   than with the "Who We Are" sechead above it.

   Scoped to this ONE image by src. 30 pages carry a <figure><img> directly
   inside a .split--wide-l, and a selector like `.split--wide-l > figure > img`
   would move every one of them.

   NOT applied inline: a percentage margin in the element's style attribute
   could only be cancelled on mobile with !important. In the stylesheet it
   cancels cleanly below the stack breakpoint.

   The zeroing mirrors a rule the global sheet already has for the same reason -
   `@media(max-width:900px){.split--wide-l > div:has(> .bigstat){margin-top:0}}`.
   .split--wide-l collapses to one column at <=900px, where a 24% top margin
   resolves against the FULL container width and would open a ~90px gap above
   the photo. */
.split--wide-l > figure > img[src$="mgpools-about-team-1.webp"]{margin-top:24%}
@media (max-width:900px){
  .split--wide-l > figure > img[src$="mgpools-about-team-1.webp"]{margin-top:0}
}

/* QA #20 - one tile size across all seven service-hub spoke straps.

   The design system deliberately stretches a lone orphan tile to full width:
     .nexttiles--3 > :last-child:nth-child(3n+1){flex-basis:100%}
   With four spokes, pool-maintenance's fourth tile matches (4 = 3*1+1) and
   rendered as a 1217px banner beside three 394px cards. Ammar asked for one
   tile size on every hub, so on these straps the orphan keeps the third-width
   basis instead.

   Scoped to .nexttiles--spokes, which only the seven straps carry, so the one
   live page that still relies on the full-width orphan
   (/orange-village-oh/pool-safety-covers/) is untouched.

   Desktop only: at <=1000px the global orphan rule already resolves to the same
   calc(50% - 9px) as its siblings, and at <=640px everything is 100%. Without
   the min-width guard this rule would load after those media blocks and break
   both. */
@media (min-width:1001px){
  .nexttiles--spokes > :last-child:nth-child(3n+1){flex-basis:calc(33.3333% - 12px)}
}

/* QA #23 - the Northeast Ohio service-area map sits at 75% width, centred.

   Ammar's DevTools example set `width:75%` with `align:center !important` (not a
   real property - DevTools flagged it) and `margin-left:12%` approximating the
   centre. `margin-inline:auto` is the correct expression of that intent, and it
   needs `display:block` because the global `.artfig img` rule declares no display
   and an <img> is inline by default, so auto margins would do nothing.

   Scoped to this one image, NOT to `.artfig img`, which styles 88 figures across
   59 approved pages. The same src$= pattern is already used in this file for the
   about-page portrait.

   Cancelled below 900px, mirroring the about-portrait rule above: 75% of an
   already-narrow phone column would leave the map around 245px wide and its town
   labels unreadable. */
.artfig img[src$="ne-ohio-map.webp"]{width:75%;display:block;margin-inline:auto}
@media (max-width:900px){
  .artfig img[src$="ne-ohio-map.webp"]{width:100%}
}

/* QA #24 - the map's caption lines up with the map.

   The image is 75% and centred, so it starts 12.5% in; the figcaption was still
   full figure width and hard left, hanging out past the image it describes.
   Giving the caption the same 75% box and the same auto margins puts its left
   edge on the image's left edge, with the text itself still ranged left.

   Adjacent-sibling rather than :has() - the figcaption directly follows the img
   inside .artfig, so `+` is enough and is better supported. Cancelled below 900px
   in step with the image rule, where the image goes back to full width. */
.artfig > img[src$="ne-ohio-map.webp"] + figcaption{width:75%;margin-inline:auto}
@media (max-width:900px){
  .artfig > img[src$="ne-ohio-map.webp"] + figcaption{width:100%}
}

/* QA #25 - the service icon inside a button vanishes on hover.

   Measured on the homepage button "Chemical & Equipment Checks":

                  icon stroke        button background    contrast
     rest         rgb(75,77,237)     transparent/white    5.86:1
     hover        rgb(75,77,237)     rgb(75,77,237)       1.00:1

   Not low contrast - the SAME colour. .svctabs__ic carries its own
   `color:var(--txt-accent)` (global sheet line 203), which resolves to #4B4DED
   here and does not change on hover, while `.band-light .btn--outline:hover`
   paints the button background that same #4B4DED. The icon is still painted; it
   is painted in the background colour.

   .btn__arrow in the same button is the control for this: it sets no colour of
   its own, inherits the button's, and turns white on hover (5.86:1). The icon
   should behave identically.

   `color:inherit` is the global sheet's OWN idiom for this - lines 254-256 use it
   to the same end when a service tab is :checked. Scoped to .btn so the tabs bar
   (4 icons on 2 pages, which the census found) is untouched; (0,2,0) beats the
   global's (0,1,0) without !important. No new colour: the icon simply takes the
   button's, which is already brand palette in both states.

   Present in the approved 6B static too, so this is a deliberate divergence from
   it, not a conversion repair. Census: 1 button on 1 page sitewide. */
.btn .svctabs__ic{color:inherit}
