/* client-overrides.css — robertroberts
 *
 * Bespoke, per-site CSS. This file is NEVER written by wp-build-page.py or
 * refresh_static_overrides, and the converter links it LAST so it wins the
 * cascade. Anything client-specific belongs here; never edit
 * static-overrides.css, which is overwritten from shared/templates on every
 * build.
 *
 * Each rule below records WHY it deviates, because a deviation from the captured
 * source is otherwise indistinguishable from a conversion bug at the next review.
 */

/* ---------------------------------------------------------------------------
 * Home project gallery — hover overlay BLACK instead of the source's brand teal.
 *
 * Operator adjustment, home gate 2026-08-25: "captions should only show on hover
 * like source, change overlay to black on hover".
 *
 * The hover-reveal half is fidelity (fixed fleet-wide in the shared template —
 * Elementor's own data-settings say `content_hover_animation: "fade-in"`, and the
 * stripped runtime left the caption permanently on). THIS rule is the other half
 * and is a deliberate DEVIATION: the client's captured CSS paints this specific
 * widget's hover overlay brand teal —
 *
 *   .elementor-56 .elementor-element.elementor-element-b6f286f
 *     .e-gallery-item:hover .elementor-gallery-item__overlay
 *     { background-color:#6DC3E1A1 }        <- brand blue @ ~63%
 *
 * — while their own /portfolio/ galleries already use rgba(0,0,0,0.5). So this
 * makes the home match the portfolio rather than inventing a new treatment.
 *
 * Kept per-widget, NOT in the shared template: a fleet-wide black would stomp the
 * authored overlay colour on every other client's gallery. Specificity mirrors the
 * captured selector so it wins on source order rather than on `!important`.
 * `:focus-within` is added for keyboard parity — these items are anchors. */
.elementor-56 .elementor-element.elementor-element-b6f286f .e-gallery-item:hover .elementor-gallery-item__overlay,
.elementor-56 .elementor-element.elementor-element-b6f286f .e-gallery-item:focus .elementor-gallery-item__overlay,
.elementor-56 .elementor-element.elementor-element-b6f286f .e-gallery-item:focus-within .elementor-gallery-item__overlay,
.elementor-element-b6f286f .e-gallery-item:hover .elementor-gallery-item__overlay,
.elementor-element-b6f286f .e-gallery-item:focus-within .elementor-gallery-item__overlay {
  background-color: rgba(0, 0, 0, 0.55);
}

/* ---------------------------------------------------------------------------
 * WCAG AA text contrast — operator instruction 2026-08-26: "proceed with AA fixes".
 *
 * A deliberate DEVIATION from the source, authorised explicitly. Every failure below
 * is inherited from the live WordPress site; "the source fails the same way" explains
 * the origin but does not make it readable, so each is fixed here rather than logged.
 *
 * HOW THESE WERE MEASURED — and why the obvious method is wrong.
 * Walking the DOM for the nearest painted ancestor CANNOT answer this: every one of
 * these is light text over a PHOTOGRAPH under a semi-transparent scrim, so the real
 * background is a composite that exists only in the compositor. `contrast-sweep.mjs`
 * reported 7 failures here and 4 of them were false positives (it resolved section
 * bands behind translucent cards); a naive probe of mine added 6 more phantom ones
 * from `opacity:0` hover captions. Ground truth came from painting every glyph
 * transparent, screenshotting, and sampling the real pixels inside each text box --
 * scoring the WORST pixel, not the average, because a photo fails exactly where it
 * is brightest and an average hides that spot. 68 visible text nodes, 4 real failures.
 *
 * Values are the minimum that clears 4.5:1 at that worst pixel, so the design moves
 * as little as possible. Re-verify with `probes/rr-aa-truth2.mjs` after any change to
 * these sections -- and re-run it after mass conversion, since only the home page has
 * been measured. */

/* 1. "Our Completed Projects" — white 22px body copy over a kitchen photo. The scrim
 *    is `rgba(0,0,0,0.51)`, which leaves the photo's blown-out highlights at rgb(125,
 *    125,125): white lands at 4.12:1. 0.51 -> 0.58 pulls the worst pixel dark enough.
 *    Scoped to this widget id, NOT to `.elementor-background-overlay` globally — the
 *    same class carries this page's pale `#E8EFF2` wash and the testimonial scrim,
 *    both of which already pass and must not be darkened. */
/*    ⚠ The overlay is a GRANDchild — Elementor nests it under
 *    `.elementor-element-populated`, and the captured rule is
 *    `.elementor-56 .elementor-element.elementor-element-34d3fc04 >
 *     .elementor-element-populated > .elementor-background-overlay` (5 classes).
 *    A shorter `.elementor-element-34d3fc04 > .elementor-background-overlay`
 *    silently matches NOTHING and loses on specificity even if it did. Mirroring the
 *    captured selector exactly means equal specificity, so this wins on source order
 *    (client-overrides.css is linked last) rather than on `!important`. */
.elementor-56 .elementor-element.elementor-element-34d3fc04 > .elementor-element-populated > .elementor-background-overlay {
  background-color: rgba(0, 0, 0, 0.58);
}

/* 2. The same section's "View our Portfolio" ghost button was brand `#6DC3E1` on that
 *    photo — 2.07:1, the worst offender on the page and a primary CTA. Brand blue is
 *    too light to sit on a dark scrim at any reasonable alpha (still only ~2.5:1 at
 *    step 1's value), so the label and rule take the site's OWN treatment for this
 *    exact button: the hero twin (`71269a9`) is already white. This copies an existing
 *    pattern rather than inventing one. */
.elementor-element-b63a1fb .elementor-button,
.elementor-element-b63a1fb .elementor-button:visited {
  color: #ffffff;
  border-color: #ffffff;
}
.elementor-element-b63a1fb .elementor-button .elementor-button-text,
.elementor-element-b63a1fb .elementor-button .elementor-button-icon,
.elementor-element-b63a1fb .elementor-button svg {
  color: #ffffff;
  fill: #ffffff;
}

/* 3. Hero — "View our Portfolio" in white measured 4.38:1 against the brightest
 *    masonry behind it. Its scrim is the TEXT COLUMN's own background-color
 *    (`#00000082` = 0.51 on `444dc937`'s widget-wrap), not a `.elementor-background-
 *    overlay` div, so it takes a different selector from rule 1.
 *
 *    ⚠ TWO traps cost real time here, both worth knowing before touching this hero:
 *    (a) The hero has TWO columns. `3b950f60` is the RIGHT one and carries no text —
 *        tinting it changed nothing measurable and would have darkened a photo for
 *        no reason. The text lives in `444dc937` on the left.
 *    (b) The hero background is a ROTATING SLIDESHOW (`.ym-bg-slide` x4, absolutely
 *        positioned, `pointer-events` transparent so it never appears in
 *        `elementsFromPoint` and carries no ancestor `background-image`). A single
 *        screenshot only ever measures the ACTIVE slide, so "the hero passes" is a
 *        claim about one of four photos. The value below was chosen against the
 *        WORST of all four — see `probes/rr-aa-slides.mjs`. */
.elementor-56 .elementor-element.elementor-element-444dc937:not(.elementor-motion-effects-element-type-background) > .elementor-widget-wrap,
.elementor-56 .elementor-element.elementor-element-444dc937 > .elementor-widget-wrap > .elementor-motion-effects-container > .elementor-motion-effects-layer {
  background-color: rgba(0, 0, 0, 0.58);
}

/* ---------------------------------------------------------------------------
   Subcontractor download-card titles — heading LEVEL fixed, size pinned.
   2026-09-22, SEO pass.

   /subcontractors/ jumped <h2> -> <h4>, breaking the §4 logical-heading-hierarchy
   check. The five card titles are now <h3>, which closes the outline.

   Unlike the 33 other headings re-tagged in the same pass, these five are BARE
   tags typed into an Elementor text-editor widget — no `elementor-heading-title`
   wrapper — so the theme sizes them by TAG and the swap grew them 24px -> 28px
   (line-height 28.8 -> 33.6, box 29px -> 34px tall). Measured, not assumed:
   probes/rr-heading-render.mjs fingerprints every heading's computed type and
   box before and after, and it caught exactly these five while proving the other
   33 render identically.

   So the size is pinned back to what the <h4> rendered. Scoped per WIDGET ID
   because that is how Elementor itself styles (a hand-added rule on a shared
   class inherits nothing from its row and leaks into every other text editor
   on the site). line-height is the theme's inherited 1.2, so restoring the
   font-size restores the box.
   --------------------------------------------------------------------------- */
.elementor-element-917bf4f .elementor-widget-container h3,
.elementor-element-80b8890 .elementor-widget-container h3,
.elementor-element-2dac070 .elementor-widget-container h3,
.elementor-element-d63534d .elementor-widget-container h3,
.elementor-element-8776822 .elementor-widget-container h3 {
  font-size: 24px;
}

/* ---------------------------------------------------------------------------
   Footer Privacy / Terms links — placement + contrast.
   2026-09-22, SEO pass.

   build-utility-pages.py appends the legal-link row just before </footer>, which
   lands it OUTSIDE the centred copyright block: flush left at x=0, 224px wide,
   in the theme's default link blue (#1684AA) rather than the footer's own text
   colour. On the black footer band it read as an orphaned fragment in the corner.

   Measured, not guessed. The DOM was no help here: an ancestor walk reports the
   copyright line as rgb(51,51,51) on rgb(255,255,255) while it plainly RENDERS
   white-on-black, because the band and its text colour come from layers the walk
   cannot resolve — the same class of false reading recorded in
   `reference_contrast_on_photos_needs_pixels`. The screenshot is the evidence.

   So: centre it under the copyright and match its 17px.

   The colour took two passes and a screenshot each time. First attempt was white,
   on the reading that the strip was the black footer band — it is NOT. The band
   ends above; the copyright sits on a WHITE strip below it as #333 (12.6:1), and
   white links rendered invisible there, with only the dark separator dot showing.
   #15789B is the theme's own link blue #1684AA darkened just enough to clear AA
   on white: 4.27:1 -> 5.01:1, same hue. The underline is there because a link
   distinguished only by colour fails axe `link-in-text-block` (seo-checklist #26
   — same fix, same place, different theme).
   --------------------------------------------------------------------------- */
.ym-legal-links {
  text-align: center;
  padding: 4px 20px 18px;
}
.ym-legal-links [data-ym-legal-links] {
  font-size: 17px;
  line-height: 1.5;
}
.ym-legal-links [data-ym-legal-links] a {
  color: #15789B;
  text-decoration: underline;
}
.ym-legal-links [data-ym-legal-links] a:hover,
.ym-legal-links [data-ym-legal-links] a:focus-visible {
  color: #0F5A78;
}

/* ---------------------------------------------------------------------------
   Link blue: #1684AA -> #15789B, site-wide.
   2026-09-22, SEO pass. Operator: approved with the measured numbers.

   #1684AA as text on white measures 4.27:1 — under the 4.5:1 AA floor, on 84
   nodes across 3 pages (the "Project Details" card CTA, the (423) 551.9555
   line, in-copy links). #15789B is the same hue darkened just enough to clear
   it: 5.01:1. It is the shift already shipped on the footer's legal links
   above, so this makes the site internally consistent rather than introducing
   a second blue.

   Done at the TOKEN, not per node. `--e-global-color-d789dfc` is the kit
   variable behind `.elementor-kit-7 a{color:var(--e-global-color-d789dfc)}`,
   so one declaration moves every link the kit paints — and anything the client
   adds in Elementor later inherits the accessible value instead of needing to
   be found and fixed. Redeclaring the variable on .elementor-kit-7 also means
   we never fight the cascade: no !important, no selector arms race.

   ⚠ NOT touched: --e-global-color-secondary (#6DC3E1). That is the BUTTON FILL
   and the brand colour — it carries black text at 10.54:1 and is not a
   contrast failure. Only the blue used AS TEXT moves. (`reference_brand_as_text_fails_aa`)
   --------------------------------------------------------------------------- */
.elementor-kit-7 {
  --e-global-color-d789dfc: #15789B;
}

/* ---------------------------------------------------------------------------
   Mobile body copy: 14px -> 15px at <=767px.
   2026-09-22, SEO pass. Operator: "Bump to 15px".

   The client's own kit steps body text down twice: 17px base, 15px at
   <=1024px, then 14px at <=767px. The last step is the one that hurts — phone
   copy reads cramped and SEO.md §3 wants 16px+. Bumping to 15px extends the
   client's OWN tablet value down to phones rather than inventing a size, so
   the site still reads as theirs.

   Line-height moves with it, because it has to: the kit pins 22px alongside
   the 14px, and 22px under 15px text is 1.47 — tighter than the site's own
   desktop body (17/26 = 1.53). 24px is 1.6, the standard readability ratio
   for small body text, and sits between the client's 22px and the 26px they
   use with 15px one tier up.

   ⚠ This reflows EVERY page on a phone — that is why it was an operator call
   and not a silent SEO edit. Re-probe at 390px after any change here.
   --------------------------------------------------------------------------- */
@media (max-width: 767px) {
  .elementor-kit-7 {
    --e-global-typography-text-font-size: 15px;
    --e-global-typography-text-line-height: 24px;
  }
}


/* ================================================================
 * Operator adjustments 2026-09-23 (ledger row 15) — operator-2026-09-23
 * ================================================================ */

/* (a) Carousel images render at NATURAL size, as the source does.
 * The fleet default in static-overrides.css forces every
 * `.elementor-widget-image-carousel .swiper-slide img` to width:240px
 * !important. On this site that STRETCHED the four "Companies We Sponsor"
 * logos (a 118x118 logo became 225x225, a 99x118 one 225x240) and SHRANK the
 * three home badges from the source's 283px to 240px. Measured on the source
 * at 1440: logos 237x60 / 118x118 / 237x79 / 99x118 (natural, capped by the
 * slide width), badges 283x144. That file's own comment says the sizing is
 * content-dependent and belongs here — so here it is. */
.elementor-widget-image-carousel .swiper-slide img {
  width: auto !important;
  max-width: 100% !important;
  max-height: none !important;
  height: auto !important;
}

/* (b) Post-card featured images are one shape. The WordPress cards all ship
 * 2560x854 (3:1) featured images, so they only LOOKED uniform; the curated
 * award post's aerial photo is 760x1000-ish and rendered 652x870 next to
 * 652x217 cards. Source card image box measured 652x217 @1440 = 3:1. */
.e-loop-item .elementor-widget-theme-post-featured-image img {
  width: 100%;
  aspect-ratio: 3 / 1;
  object-fit: cover;
}

/* (c) The "random underlining". Hello Elementor's theme.css says
 * `.page-content a { text-decoration: underline }` and it loads AFTER
 * Elementor's `.elementor a { text-decoration: none }` at the same
 * specificity, so on every page that carries the theme's `.page-content`
 * wrapper — the blog list, the community list and both post templates —
 * hero buttons, post titles, dates and "Read More" all underline. The other
 * pages have no `.page-content`, which is why it looked random. Same defect
 * on the live WordPress site. Running-text links inside a post body keep
 * their underline (that is the only place the theme rule is right). */
.page-content .elementor a { text-decoration: none; }
.page-content .elementor-widget-theme-post-content a { text-decoration: underline; }

/* (d) Per-carousel geometry, the way the CSS companion intends (it reads
 * `--ym-carousel-gap` and `--e-image-carousel-slides-to-show`; its 240px
 * `width` is a fleet default that L96 says belongs here).
 *
 * Home badges (4c86f84): operator 2026-09-23 — "only the 3 badges, centered,
 * no continuous scroll". Autoplay/loop/dots are off in the widget's
 * data-settings (the shim's autoplay is gated on that flag). Source measured
 * 283x144 per badge with the widget's own 40px spacing (20px on tablet):
 * (929 - 2*40) / 3 = 283. Three across on every breakpoint. */
.elementor-element-4c86f84 { --ym-carousel-gap: 40px; --e-image-carousel-slides-to-show: 3; }
@media (max-width: 1024px) { .elementor-element-4c86f84 { --ym-carousel-gap: 20px; } }
.elementor-element-4c86f84 .swiper-wrapper { justify-content: center; }
.elementor-element-4c86f84 .swiper-slide {
  /* the companion sizes slides with `flex: 0 0 240px`, so a flex-basis is what
   * actually wins here, not `width` */
  width: calc((100% - 2 * var(--ym-carousel-gap)) / 3) !important;
  flex: 0 0 calc((100% - 2 * var(--ym-carousel-gap)) / 3) !important;
}
/* Community "Companies We Sponsor" (3087475): source = 4 slots of 948/4 = 237px,
 * no gutter, logos natural-size and vertically centred in the row. */
.elementor-element-3087475 { --ym-carousel-gap: 0px; --e-image-carousel-slides-to-show: 4; }
.elementor-element-3087475 .swiper-wrapper { align-items: center; }
.elementor-element-3087475 .swiper-slide { width: 25% !important; flex: 0 0 25% !important; }
/* …and each logo CENTRED in its slot (source: 118px logo at x297 of a 237px slot,
 * 99px logo at x780 of the fourth) — the companion makes the <img> display:block,
 * so it sat flush-left and the wide logos touched their neighbours (operator,
 * 2026-09-23: "looks better but spacing is off"). */
.elementor-element-3087475 .swiper-slide img { margin-left: auto !important; margin-right: auto !important; }

/* (e) The card image's <a> is inline-block, so `width:100%` on the <img>
 * resolved against the image's own natural width (the 570px award photo sat
 * in a 652px card at 570px). Block it. */
.e-loop-item .elementor-widget-theme-post-featured-image a { display: block; }
