/* Hand-written overrides for this migration.
 *
 * NO PREVIOUS CLIENT IS NAMED IN THIS FILE, DELIBERATELY: it is copied verbatim into
 * dist, so a prior client's name here would be PUBLISHED on this client's domain and
 * would make a residue sweep report a correct file dirty. Provenance lives in
 * MIGRATION-NOTES, which does not ship.
 *
 * DELIBERATELY OUTSIDE site/public/styles/ (gotcha 49): tools/port-css.mjs owns that
 * directory and clears its own hashed outputs on every run, so a hand-written sheet
 * placed there is deleted by the next port with no error anywhere — on one site that
 * silently removed the sheet that hides the inactive device bands, and all three
 * headers then rendered at every width.
 *
 * EVERY NUMBER BELOW WAS CENSUSED ON THIS SITE. That sentence keeps having to be
 * written because this file keeps arriving describing an earlier repo in the chain: it
 * arrived here carrying the population of a repo TWO generations back (59 pages, 15
 * posts, 177 captures) in prose that reads as measurement. Those repos are deliberately
 * NOT named: this file is copied verbatim into dist, so a previous client's name here
 * would be PUBLISHED on this client's domain, and it would make a residue sweep report a
 * correct file dirty. The provenance chain is recorded in MIGRATION-NOTES, which does not
 * ship. Gotcha 64 — a document is
 * only generated where it interpolates, and the sentences between the numbers belong to
 * whoever it was copied from. The RULES were sound and are kept; every measurement
 * around them is re-taken.
 *
 * Population, DERIVED and not typed: pages.txt holds 53 paths — 21 static (including
 * /privacy-policy, which is in neither the sitemap nor the feed) plus the 31 blog posts
 * generated/blog.rss declares as <item>s, plus one post the sitemap omits — so 159
 * captures (53 pages x 3 bands) plus all 53 raw served documents. Re-derive from those
 * two files rather than from this line; a count copied out of a comment is what produced
 * every wrong number this header is about.
 *
 * Keep this file minimal. Anything that can come from the ported cascade should.
 */

/* The honeypot. Positioned off-screen rather than display:none — bots skip obviously
 * hidden fields, and taking it out of flow means it costs no layout, which the pixel
 * gate would otherwise measure. Live has NO honeypot on any of this site's forms —
 * measured, 52 of 53 pages carry exactly one form and none has a hidden trap field —
 * so this is ours and must cost nothing. It is also the ONLY spam protection now, since
 * Duda's reCAPTCHA is removed (gotcha 17: its keys are Duda's own and domain-restricted,
 * so they cannot work on the client's domain).
 *
 * /migration phase 8 specifies exactly this and one fleet repo shipped the spec
 * unimplemented — its honeypot rendered as an ordinary 40px text field and read as a
 * 46px layout defect. */
.mg-hp {
  position: absolute !important;
  left: -9999px !important;
  top: auto !important;
  width: 1px !important;
  height: 1px !important;
  overflow: hidden !important;
}

/* SPECIFICITY NOTE — read before editing any rule below.
 *
 * Duda emits a per-widget rule for EVERY widget on the site, shaped
 *
 *     #dm .dmBody div.u_1213107879 { display: block !important }
 *
 * That is (1,2,1) and it is `!important`. A hide written as a plain
 * `.mg-only-t { display:none !important }` is (0,1,0), so with `!important` on both
 * sides SPECIFICITY decides and Duda's rule wins — the element stays visible and
 * nothing in the console says so.
 *
 * So every gate below repeats its class three times behind `#dm`, giving (1,3,0),
 * which outranks (1,2,1) on class count without relying on source order. Do not
 * "tidy" the repetition away.
 */

/* ---------------------------------------------------------------------------
 * BLOG PAGINATION — ONE REAL CONTROL, ON ONE PAGE.
 *
 * Censused on the raw served html of all 53 pages (the pages.txt population) and
 * confirmed by driving live's own control. The blog-widget record count is DERIVED,
 * not typed: generated/blog-widgets.json covers /blog alone x 3 bands = 3 records,
 * because on THIS site no post carries a blog-list widget at all:
 *
 *   /blog             declared 31, visible 6    NUMBERED PAGER, REPLACES
 *                     <nav class="pagination-nav"> x1,
 *                     more-posts-text-container x0 (zero on all 53 pages).
 *                     data-paginate-total-elements=31, page size 6; its links carry
 *                     data-page and data-action="paginate" and SWAP the visible set
 *                     rather than appending to it. It is the only page on which the
 *                     live driver finds a control at all, which is that run's own
 *                     positive control (control=true, 6 -> 6 aliases, all three bands).
 *
 *   31 blog posts     NO BLOG WIDGET OF ANY KIND. postArticle x0, pagination-nav x0,
 *                     more-posts-text-container x0, list_slider x0 on every one of
 *                     them. THIS IS THE OPPOSITE OF THE SITE THIS FILE CAME FROM,
 *                     where every post carried a related-posts slider — so the rule
 *                     below has no subject on any post here, and a "related posts"
 *                     exclusion carried forward would have hidden nothing while
 *                     reading as coverage.
 *
 * The numbered pagination-nav IS this site's shape and it is on /blog alone; the
 * "show more / appends" shape has no instance here.
 *
 * *** THE PAGER IS TRUNCATED AND THAT COST SEVEN POSTS ONCE. *** As first rendered it
 * exposes data-page [1,2,3,4,6] — the ellipsis control carries data-page="4" and PAGE 5
 * is not in the DOM until you navigate nearer to it, while 31 posts at 6 per page is
 * SIX pages. A walk that reads the control once therefore recovers 24-25 of 31. The
 * capture tool now computes ceil(total/visible) and re-reads the pager after each
 * click; it recovered 31 of 31 on all three bands.
 *
 * blog.rss independently declares 31 items, which is the cross-check that makes a
 * truncated /blog fail rather than ship quietly, and the alias set driven out of
 * /blog equals it exactly in both directions — 31 = 31, empty each way.
 *
 * A static build has no Duda backend, so every card ships and the ones past the
 * first page are stamped mg-blog-hidden with data-mg-blog-page; runtime.js reveals
 * them in live's own page size of 10, replacing the visible set rather than
 * appending to it. This rule is what hides them initially — 19 declared minus the 10
 * page one shows = 9 cards, on the one page that has a control.
 * ------------------------------------------------------------------------- */
#dm .mg-blog-hidden.mg-blog-hidden.mg-blog-hidden {
  display: none !important;
}

/* display:none DOES NOT CHANGE CHILD INDICES, so shipping the withheld cards
 * silently moves which card is :last-child — gotcha 69.
 *
 * Duda's own rule is `.postArticle:not(:last-child){padding-bottom:Npx}`. On live the
 * last VISIBLE card is genuinely :last-child and takes none of it. In our build it is
 * followed by hidden siblings, so it stops matching and GAINS that padding, pushing
 * the control and the whole footer down by a constant amount — the signature of one
 * shared element rather than a per-page fault.
 *
 * *** MEASURED ON THIS SITE, AND IT IS ARMED HERE — 30px, ON EXACTLY ONE CARD. ***
 * The comment this file arrived with said the rule was "currently inert here" because
 * every `:not(:last-child)` padding rule in THAT site's cascade was scoped
 * `[list-layout=recent_posts]` and its layouts did not match. That was a true
 * measurement of a previous client and is false here, which is exactly why an
 * inherited "this does not apply" is worth re-measuring: the matching rule on this
 * site is
 *
 *   #dm [blog-posts-feature-flag=true][list-layout=recent_posts][posts-padding="15"]
 *       .postArticle:not(:last-child){padding-bottom:30px}
 *
 * *** THE SUBJECT OF THIS RULE ON THIS SITE IS /blog AND NOTHING ELSE. *** The file
 * arrived describing a site where "every blog POST carries TWO related-posts widgets",
 * with a per-widget padding table measured there. NONE OF THAT IS TRUE HERE: censused
 * over all 53 raw served documents, postArticle appears on exactly ONE — /blog — and
 * zero of the 31 posts carry a blog-list widget of any kind. The inherited table has
 * been REMOVED rather than retargeted, because a number kept for its shape is the
 * derived-artifact failure and a wrong one here reads as proof.
 *
 * What IS true here, and why the rule is still needed: /blog ships all 31 cards and
 * hides 25 of them, so the SIXTH card — the last one live actually renders — stops
 * being :last-child and gains the 30px that Duda's rule withholds from the real last
 * card. That is gotcha 69 exactly: display:none does not change child indices. The
 * measured delta is recorded in MIGRATION-NOTES after the gate rather than asserted
 * here before it.
 *
 * *** AND THE OVERRIDE BELOW WAS ALREADY PRESENT AND ALREADY MATCHING — IT LOST THE
 * SPECIFICITY FIGHT, WHICH IS THE FAILURE THAT LOOKS LIKE THE RULE NOT BEING THERE.
 * The selector matched (the last visible card's nextElementSibling really is
 * `.postArticle.mg-blog-hidden`), and the card still computed 30px. Counting:
 *
 *   ours   #dm + .postArticle + :not(.mg-blog-hidden) + :has(.postArticle.mg-blog-hidden)
 *          = (1,4,0)   — :has() takes the specificity of its most specific argument
 *   Duda's #dm + 3 attribute selectors + .postArticle + :not(:last-child)
 *          = (1,5,0)   — and it carries NO !important
 *
 * (1,5,0) beats (1,4,0), so the override was outranked and silently did nothing.
 * `!important` is what settles it, because Duda's rule here is not !important — this
 * is gotcha 70's specificity trap and the "a pixel count identical across runs means
 * the fix never took effect" rule arriving together.
 *
 * Keyed on `:has(+ .mg-blog-hidden)` rather than on a stamped class, because that
 * tracks the SEQUENCE as batches are revealed instead of pinning the initial state;
 * runtime.js does not have to re-stamp anything. */
#dm .postArticle:not(.mg-blog-hidden):has(+ .postArticle.mg-blog-hidden) {
  padding-bottom: 0 !important;
}

/* ---------------------------------------------------------------------------
 * PER-DEVICE WIDGET FORKS.
 *
 * THIS SITE IS DUDA **FLEX**, AND ON FLEX THE CHROME DOES NOT FORK — GOTCHA 5 IS
 * INVERTED. Measured over generated/served-by-device (159 documents, 53 paths x 3
 * device UAs, all 200):
 *
 *   dmtemplateid    FlexHeader on ALL THREE BANDS, all 53 pages
 *   <body> class    `dmRoot fix-mobile-scrolling flex-site dmResellerSite `,
 *                   BYTE-IDENTICAL on all three bands, all 53 pages
 *   data-flex-site  present on all three bands, all 53 pages
 *
 * So dmtemplateid CANNOT discriminate the fork here, and gotcha 48/88's
 * dmDesktopBody / dmTabletBody / dmMobileBody classes DO NOT EXIST on this platform.
 * Note `fix-mobile-scrolling` in that class list is LOAD-BEARING (gotcha 35): Duda's
 * body{overflow:hidden} is undone only by it, and a noise filter that judges a class by
 * how its NAME reads will strip it, after which the page cannot be scrolled at any
 * width while every document height and every pixel stays identical.
 *
 * THE CONTENT FORK IS STILL THREE-WAY, and it is measured on the served BYTES rather
 * than inferred from the template: desktop == tablet on 0 of 53, desktop == mobile on
 * 0 of 53, tablet == mobile on 0 of 53, each disagreement re-sampled to rule out a
 * flake. The two independent desktop corpora taken minutes apart
 * (generated/raw-pages and generated/served-by-device/*.d.html) are 53 of 53
 * byte-identical, which is the control that says the origin was not drifting under the
 * measurement.
 *
 * THE TABLET DECLARES ITS OWN CANVAS, AND THE THREE BANDS DECLARE THREE DIFFERENT
 * VIEWPORTS — not two. Censused 53/53 on each band:
 *
 *   desktop  initial-scale=1, minimum-scale=1, maximum-scale=5, viewport-fit=cover
 *   tablet   width=960px
 *   mobile   width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5,
 *            viewport-fit=cover
 *
 * So gotcha 3's 960 canvas DOES apply here — measured, not carried: gotcha 62 exists
 * because that value is a common default and not a platform constant. And desktop does
 * NOT declare device-width, which the inherited text asserted.
 *
 * THE `mg-only-t` GATE THEREFORE CARRIES A CONTENT FORK, NOT A CHROME FORK. The header,
 * drawer and overlay are one shape on every band (#dmFlexHeaderContainer, #flex-header,
 * one #hamburger-drawer 53/53 per band, one #layout-drawer-overlay 53/53 per band);
 * what differs per band is the page content. THE OCCURRENCE COUNT IS NOT QUOTED because
 * it is not yet measurable — no page has been built — and an inherited figure here would
 * be a count of another client's build.
 *
 * THE BAND BOUNDARIES ARE DUDA'S OWN, RE-COUNTED ON THIS SITE. The media queries the
 * captured cascade actually contains, over all 361 sheets in generated/live/css:
 *
 *     max-width: 767px   x197      min-width: 768px   x97
 *     max-width: 1024px  x103      min-width: 1025px  x43
 *     min-width: 0px     x71
 *
 * 1025 is therefore the desktop edge on Duda's own declarations rather than by assuming
 * 1024+1. (The inherited counts were 136/92/65/64 over 202 sheets — a different site.)
 *
 * NOT TAKEN ON THIS SITE, AND RECORDED AS MISSING RATHER THAN INHERITED: a live drawer
 * ratio sweep across many widths. The inherited copy carried a 12-width sweep from
 * a previous client (40vw for #hamburger-drawer, 85vw for #mobile-hamburger-drawer
 * at 375). NOT ONE of those numbers is this site's — that site forked two ways with no
 * 960 tablet canvas and used two DIFFERENT drawer elements, while this site has ONE
 * #hamburger-drawer on every band — so they are deleted rather than renumbered. Re-take
 * before quoting a behavioural edge, and mind the UA trap (gotcha 66): Duda picks its
 * document by USER-AGENT, so a desktop UA at a narrow viewport gets the DESKTOP document
 * and reads the wrong element.
 *
 * So, on (a) alone: mobile <=767, tablet 768-1024, desktop >=1025.
 *
 * These hide rather than remove, deliberately — the elements stay in the document so
 * the runtime can still address them, and display:none costs no layout.
 *
 * Only the INACTIVE bands are hidden, inside the media queries. The active band is
 * never touched, so it keeps whatever `display` the ported cascade gives it — hiding
 * all three and restoring one with `display: revert` reverts past the author cascade
 * to the UA default and replaces the widget's real display value with a plain block.
 * ------------------------------------------------------------------------- */
@media (max-width: 767px) {
  #dm .mg-only-d.mg-only-d.mg-only-d,
  #dm .mg-only-t.mg-only-t.mg-only-t {
    display: none !important;
  }
}

@media (min-width: 768px) and (max-width: 1024px) {
  #dm .mg-only-d.mg-only-d.mg-only-d,
  #dm .mg-only-m.mg-only-m.mg-only-m {
    display: none !important;
  }
}

@media (min-width: 1025px) {
  #dm .mg-only-t.mg-only-t.mg-only-t,
  #dm .mg-only-m.mg-only-m.mg-only-m {
    display: none !important;
  }
}

/* ---------------------------------------------------------------------------
 * SLIDERS: TWO WIDGETS ON ONE PAGE, AND THEY DO MOVE. NO RULE IS NEEDED HERE.
 *
 * RE-CENSUSED ON THIS SITE, and the inherited section described the opposite site.
 * That copy asserted 9 single-slide roots across 9 pages with isAutoPlay:false and
 * isFade:true — "nothing to advance to, no arrows to bind" — which is
 * a previous client's shape, not this one.
 *
 * Measured over the 59 served documents: `ssrimageslider` occurs on exactly ONE page,
 * the home page, 6 occurrences. tools/derive-slider-config.mjs reads each widget's own
 * declared autoPagination config and finds TWO widgets, both:
 *
 *     autoPlay=true   interval=7s   pauseOnHover=false   animation=slide
 *
 * So this site's sliders DO auto-advance, on a 7-second interval, and the interval is
 * read from each widget's own payload rather than from runtime.js's inherited constant
 * (gotcha 90: every widget parameter comes from that page's own config, never from the
 * site the driver was written against).
 *
 * TWO CONSEQUENCES FOR THE GATE, both of them the reason this is written down:
 *
 *   (a) An auto-advancing slider can never diff to zero — the two captures land on
 *       different slides — so the home page's slider region is a legitimate --hide
 *       candidate, and whatever is hidden needs a per-element check that asserts
 *       MOTION, because motion is exactly what hiding removes.
 *   (b) The shared check-slider.mjs asserts on `.flexslider` roots, of which this site
 *       has ZERO. It will FATAL on an empty population. That refusal is honest about
 *       ITS subject and says nothing about this site's sliders — "no FlexSlider" must
 *       not be written up as "no sliders".
 *
 * NOT YET VERIFIED ON THE SERVED BUILD — nothing has been built yet (site/src/pages is
 * empty). The population any such run must cover is DERIVED from the census above and
 * not typed: 2 roots on 1 page, x 3 widths.
 * ------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
 * ACCORDION OPEN STATE — recovering a rule the live CSS read cannot see.
 *
 * RE-CENSUSED ON THIS SITE, AND THE INHERITED SECTION HAD ALREADY CLAIMED TO BE.
 * That is the part worth recording: the block that arrived here opened with the words
 * "RE-CENSUSED ON THIS SITE" and then gave a remodeling company's widget ids and page
 * slugs (/bathrooms, /decks, /kitchens, /outdoor-spaces …), none of which exist on an
 * electrical contractor's site. So gotcha 64 nests: a section can carry the SENTENCE
 * asserting it was re-measured while carrying the previous client's numbers, and the
 * sentence is what makes the numbers believable. All of it is deleted rather than
 * renumbered.
 *
 * THIS SITE, from generated/accordion-config.json (derived from live's own
 * initiateWidget({"type":"SSR_ACCORDION"}) payloads, 0 failing to parse) and
 * corroborated against an `ssraccordion` census of the raw served html:
 *
 *     23 of 53 served documents carry an accordion
 *     8 DISTINCT widget ids
 *       7 unique per-page widgets — /, /commercial-electricians-grand-junction,
 *         /electrical-panel-upgrades-grand-junction,
 *         /ev-charger-installation-grand-junction,
 *         /generator-installation-and-repair-grand-junction,
 *         /lighting-installation-grand-junction,
 *         /service-areas/local-electrician-fruita
 *       1 SHARED widget (2600780144) on 16 blog posts
 *     EVERY widget: firstExpanded FALSE, closeOthers TRUE, 5 items, LAYOUT_1
 *
 * BOTH BEHAVIOURAL FLAGS AGREE SITE-WIDE HERE, which is worth stating rather than
 * leaving implicit: a per-widget stamp is still used, because a site-wide rule that
 * happens to be right today is the shape that ships unnoticed the day one widget
 * differs. Note firstExpanded FALSE everywhere means every accordion ships CLOSED —
 * so, unlike the site two generations back, there is no expandFirstItem height to
 * reproduce.
 *
 * ONE PROPERTY DOES VARY, AND IT IS NOT BEHAVIOURAL. addSchemaMarkup is TRUE on the one
 * shared blog widget and FALSE on all 7 unique ones. That is why live emits a FAQPage
 * JSON-LD block on the blog posts and on the home page but not on every accordion page,
 * and it is reproduced from each page's own captured head rather than from this config.
 *
 * THE REVEAL BEHAVIOUR IS NOT YET MEASURED HERE. The population to cover when it is
 * taken is the 23 pages above.
 *
 * THE CLOSED RULE IS IN THE PORT AND THE OPEN RULE IS NOT, AND THAT IS NOT A PORTING
 * MISTAKE. styled-components insert through the CSSOM, so a class exists in a
 * document's sheet only if that component actually RENDERED with it. Measured over all
 * 59 served documents: 24 <style data-styled> blocks, NONE empty, the CLOSED rule
 * served once per accordion instance, and NO open-state rule served anywhere — zero
 * non-zero max-height declarations in any of those blocks:
 *
 *     closed  .dygwmn  { overflow:hidden; transition:max-height .3s ease-out;
 *                        height:auto; max-height:0 }        served, x22
 *     open    minted per distinct pixel height at runtime   NOT SERVED, x0
 *
 * The inherited copy named three open classes and their heights (.eZPHSz 185px,
 * .eDGbXZ 211px, .crqarE 236px). Those were read off another site and are deleted
 * rather than renumbered; this site's open classes have not been read off live at all,
 * which is exactly why the rule below is keyed on OUR class.
 *
 * THE OPEN CLASS IS MINTED PER DISTINCT PIXEL HEIGHT, so items sharing a height share
 * a class. No open-state name can be hardcoded, and restoring the rule under OUR OWN
 * class is recovering a sheet the live read could not reach.
 * ------------------------------------------------------------------------- */
#dm .mg-acc-open.mg-acc-open.mg-acc-open {
  max-height: 2000px;
}

/* ---------------------------------------------------------------- flex popup --
 * THE TWO HOSTS DUDA'S RUNTIME CREATES FOR A CLICK-TRIGGERED POPUP, AND THE ONLY
 * REASON THEY ARE HAND-WRITTEN IS THAT THEY DO NOT EXIST AT CAPTURE TIME.
 *
 * #flex-runtime-popup itself is NOT here: its rules came through the ported cascade
 * normally (width 712px, height 810px, border-radius 8px, background var(--color_4)),
 * because the <dialog> IS in the served markup of /form. What is missing is the
 * overlay and the container, which Duda's runtime builds on the fly when the trigger
 * is clicked -- so no capture can contain them and port-css has nothing to port.
 *
 * EVERY VALUE BELOW WAS READ OFF LIVE with a real hit-tested click at 1440, not
 * chosen. Measured on the settled popup:
 *
 *   #flex-popup-overlay             position fixed  z-index 200  rgba(0, 0, 0, 0.533)
 *                                   1440x900 at (0,0)
 *   #flex-runtime-popup-container   position fixed  z-index 201  transparent
 *                                   1440x900 at (0,0)  display flex
 *   dialog#flex-runtime-popup       712x810 at (364, 45), background rgb(233,233,233)
 *
 * The dialog's position is CENTRING, not a coordinate to copy: (1440-712)/2 = 364 and
 * (900-810)/2 = 45 exactly. So the container centres and the dialog keeps the UA's own
 * `position:absolute; margin:auto` for `dialog[open]`. Reproducing the mechanism rather
 * than the numbers is what makes it correct at every viewport, per gotcha 46 -- the
 * captured coordinates are a function of the viewport they were measured in.
 *
 * Scroll lock goes on BODY here. That is not a guess either: live's popup sets
 * overflow:hidden on body and leaves documentElement visible, which is the OPPOSITE of
 * this site's hamburger drawer, where the lock is on documentElement and body stays
 * visible. The two are measured separately and must not be unified.
 */
#flex-popup-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  background-color: rgba(0, 0, 0, 0.533);
}
#flex-runtime-popup-container {
  position: fixed;
  inset: 0;
  z-index: 201;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: transparent;
}

/* ---- Pre-launch review 2026-09-29 ------------------------------------------ */

/* Drawer sub-groups: the caret (runtime.js) is the only thing that opens one.
 * On /pressure-washing and its two children the nested "Pressure Washing" group was
 * held open by Duda's `.dmNavItemSelected ~ .unav-sub { max-height: 200em }` with no
 * open class, so its caret did nothing (64px before and after each tap; live the
 * same). Every group now starts closed and follows the open class alone. */
#dm#dm #hamburger-drawer .unifiednav__item-wrap:not(.unifiednav__item-wrap_open) > .unav-sub {
  max-height: 0 !important;
  opacity: 0 !important;
  pointer-events: none !important;
}

/* Legal pages (tools/build-legal-pages.mjs). Duda's reset zeroes margin on p, ul
 * and h2, so the template text would otherwise render as one slab with every
 * heading pressed against the paragraph above it. */
#dm .gy-legal-row {
  max-width: 1200px;
  margin: 0 auto;
  padding: 40px 40px 80px;
  box-sizing: border-box;
}
#dm .gy-legal-row h2 {
  margin: 36px 0 12px;
}
#dm .gy-legal-row p {
  margin: 0 0 14px;
}
#dm .gy-legal-row ul {
  margin: 0 0 14px;
  padding-left: 1.4em;
  list-style: disc;
}
#dm .gy-legal-row li {
  margin: 0 0 4px;
}
@media (max-width: 767px) {
  #dm .gy-legal-row {
    padding: 24px 16px 60px;
  }
}

/* Desktop nav: on a page that lives inside a dropdown (the six service pages, the two
 * service-area pages) the current page is marked only inside the closed dropdown, so
 * the bar showed no current item at all (live the same). The parent takes the same
 * treatment the current top-level item gets: rgb(37,170,226) behind white ink. */
#dm#dm #flex-header .unav-top > .unifiednav__item-wrap:has(.unav-sub .dmNavItemSelected) > a.unav-item {
  background-color: rgb(37, 170, 226);
}

/* Footer legal line (review 2026-09-29, manual): it sits inside the copyright
 * custom widget, which Duda sizes to a fixed 15px, as are its wrapper and group. The
 * line overflowed that box, past the end of the page, and was cut in half. Let every
 * box from the widget up to the footer section size to its content, and give the
 * row air below the links. */
#dm#dm #flex-footer :is(.flex-element, .dmCustomWidget):has(.gy-legal-links) {
  height: auto !important;
  min-height: 0 !important;
}
#dm#dm #flex-footer .gy-legal-links {
  margin-top: 6px;
  padding-bottom: 14px;
}

/* "Get in touch" popup (review 2026-09-29, manual). Its dialog carries a FIXED
 * height from the ported sheets (810px desktop, 432px tablet, 388px phone): on a
 * 768px-tall laptop the top (with the close X) and the bottom were off-screen, and
 * on tablet and phone the 788-1061px form spilled out below the box. The dialog now
 * takes its content's height up to the screen's, scrolls inside, and keeps its
 * first row, the one holding the close X, pinned at the top while it scrolls. */
#dm#dm #flex-runtime-popup,
#flex-runtime-popup#flex-runtime-popup {
  height: auto !important;
  max-height: calc(100vh - 32px) !important;
  max-height: calc(100dvh - 32px) !important;
  overflow-y: auto !important;
  overscroll-behavior: contain;
}
#flex-runtime-popup#flex-runtime-popup .dmRespRowsWrapper > [data-auto="flex-section"]:first-child {
  position: sticky;
  top: 0;
  z-index: 5;
  background-color: var(--color_4, rgb(233, 233, 233));
}

/* Logo -> homepage (tools/review-logo-link.py). The anchor covers the logo widget. */
#dm .imageWidget:has(> .gy-logo-link) {
  position: relative;
}
#dm .gy-logo-link {
  position: absolute;
  inset: 0;
  z-index: 2;
}

/* GBP map strip in the footer (tools/review-footer-map.py): full width, 300px. */
#dm #flex-footer .gy-footer-map {
  width: 100%;
  height: 300px;
  max-height: 300px;
  overflow: hidden;
  line-height: 0;
}
#dm #flex-footer .gy-footer-map iframe {
  width: 100%;
  height: 300px;
  border: 0;
}

/* "Book An Appointment" CTA on /about-us and /gallery (tools/review-cta.py), in the
 * site's own filled-button style (measured on the home hero's phone button):
 * Montserrat 700, white on rgb(37,170,226), square corners, 242px x 50px. */
#dm .gy-cta {
  width: 100%;
  display: flex;
  justify-content: center;
  padding: 16px 0 8px;
}
#dm .gy-cta .gy-cta-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 242px;
  min-height: 50px;
  padding: 0 24px;
  box-sizing: border-box;
  background-color: rgb(37, 170, 226);
  border: 2px solid rgb(37, 170, 226);
  color: rgb(255, 255, 255);
  font-family: Montserrat, "Montserrat Fallback", sans-serif;
  font-size: 16px;
  font-weight: 700;
  text-transform: uppercase;
  text-decoration: none;
  transition: background-color .2s, color .2s;
}
#dm .gy-cta .gy-cta-btn:hover {
  background-color: transparent;
  color: rgb(37, 170, 226);
}

/* Phone: the "Contact Raw Window Cleaning" / "Request Your Free ... Estimate" boxes
 * keep Duda's desktop row (copy left, button right) at 390px, which squeezed the
 * GET IN TOUCH WITH US button to 44px x 94px, one word per line (17 buttons on 9
 * pages; live the same, measured 2026-09-29). Stack the button under the copy at
 * its normal width. Row ids measured on /, the service pages and the area pages. */
@media (max-width: 767px) {
  #dm#dm :is([id="04b63c74"], [id="fac8d874"], [id="d044b04c"], [id="9433ba7c"], [id="4daaef88"],
             [id="df2b478e"], [id="8b84ba0b"], [id="48771c10"], [id="7863da1d"]) {
    flex-direction: column !important;
    align-items: stretch !important;
  }
  #dm#dm :is([id="04b63c74"], [id="fac8d874"], [id="d044b04c"], [id="9433ba7c"], [id="4daaef88"],
             [id="df2b478e"], [id="8b84ba0b"], [id="48771c10"], [id="7863da1d"]) > .flex-element.group {
    width: 100% !important;
    min-height: 0 !important;
  }
  #dm#dm :is([id="04b63c74"], [id="fac8d874"], [id="d044b04c"], [id="9433ba7c"], [id="4daaef88"],
             [id="df2b478e"], [id="8b84ba0b"], [id="48771c10"], [id="7863da1d"]) > .flex-element.group > .flex-element.widget-wrapper:has(> a.dmButtonLink) {
    width: 242px !important;
    max-width: 100%;
  }
}
