From ed694d33d36267f0ffa20bc6bff5906e5689320a Mon Sep 17 00:00:00 2001 From: Sam Shao Date: Fri, 7 Aug 2026 22:33:17 -0700 Subject: [PATCH 1/5] Fix quote card background clipping short of the true left edge on wide screens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The quote-block's dark background reaches the correct CSS position (its computed left offset was already correct — verified via getComputedStyle), but was actually being clipped by .carousel-story-slide's own overflow:hidden, which only spans the slide's centered ~1240px box, not this section's true left edge. The escape distance needed grows with viewport width (it's the centered column's own side margin), so this was invisible/negligible at typical desktop widths but left a very visible gap on wide screens (found by comparing screenshots against fortinet.com side by side). Moves the crossfade's overflow:hidden from the (narrow, centered) slide itself to a new full-bleed .carousel-story-slides-clip wrapper around the whole slides container instead — still contains the stacked slides during the crossfade, but no longer clips the quote-block's own escape to this section's real edge. Verified at 390px, 1464px, and 5114px widths, mid-transition (no bleed between slides), and against fortinet.com directly. Co-Authored-By: Claude Sonnet 5 --- blocks/carousel-story/carousel-story.css | 25 +++++++++++++++++------- blocks/carousel-story/carousel-story.js | 13 +++++++++++- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/blocks/carousel-story/carousel-story.css b/blocks/carousel-story/carousel-story.css index a90255e..553b2b8 100644 --- a/blocks/carousel-story/carousel-story.css +++ b/blocks/carousel-story/carousel-story.css @@ -94,6 +94,24 @@ } } +/* Full-bleed clip boundary for the crossfade below (see createSlide/decorate + in carousel-story.js for why this is a separate wrapper, not overflow on + .carousel-story-slides-container itself): that container is centered and + only 1240px wide, so clipping there — as an earlier revision did, on + .carousel-story-slide itself — cut the quote-block's own escape-to-the- + true-left-border trick short at the container's own (narrower) edge + instead of this section's real one. The gap this left was only a sliver + at typical desktop widths (the container's own side margin there), easy + to miss, but grew to hundreds of pixels on wider screens, where that + margin is much bigger. Clipping here instead — a genuinely full-bleed + ancestor — still contains the crossfade's overlapping stacked slides + (nothing needs to paint past THIS element's edges), while letting the + quote-block's ::before reach all the way to this section's true edge + underneath it. */ +.carousel-story .carousel-story-slides-clip { + overflow: hidden; +} + .carousel-story .carousel-story-slides-container { position: relative; @@ -134,13 +152,6 @@ opacity: 0; pointer-events: none; transition: opacity 0.4s ease; - - /* Overlapping stacked slides would otherwise show through each other - during the crossfade (each is fully opaque content-wise, just faded - via the opacity above) — this and the quote-block's negative-left - bleed both need clipping to this slide's own box, or the bleed paints - into whichever slide happens to be stacked next in DOM order. */ - overflow: hidden; display: flex; flex-direction: column; gap: 32px; diff --git a/blocks/carousel-story/carousel-story.js b/blocks/carousel-story/carousel-story.js index 685473b..cc03d59 100644 --- a/blocks/carousel-story/carousel-story.js +++ b/blocks/carousel-story/carousel-story.js @@ -300,7 +300,18 @@ export default async function decorate(block) { }); container.append(slidesWrapper); - block.prepend(container); + + // Full-bleed wrapper around the (centered, narrower) container above, + // purely to clip the crossfade's overlapping stacked slides — see + // .carousel-story-slides-clip in carousel-story.css for why this can't + // just be overflow:hidden on .carousel-story-slide itself (that clips at + // the slide's own centered/narrow box, not this section's true edges, + // which defeats the quote-block's own escape-to-the-left-border trick in + // updateActiveSlide below). + const clipWrapper = document.createElement('div'); + clipWrapper.className = 'carousel-story-slides-clip'; + clipWrapper.append(container); + block.prepend(clipWrapper); // Desktop-only decorative copy of the active slide's photo (see // .carousel-story-float-photo in carousel-story.css) — a plain div, not From 37d4375eded3a2a5598f398d636813bc9e175fa9 Mon Sep 17 00:00:00 2001 From: Sam Shao Date: Sat, 8 Aug 2026 12:45:16 -0700 Subject: [PATCH 2/5] Fix dot-grid watermark position to match fortinet.com's measured values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-measured svg.customer-dots-bg directly on fortinet.com (relative to its own #banner1): top:312px, height:500px — not "half of the photo" (83 + 515/2 ≈ 340) as an earlier revision assumed. That guess looked reasonable but was off by ~28px, visible when comparing screenshots side by side against fortinet.com at wide viewports. Co-Authored-By: Claude Sonnet 5 --- blocks/carousel-story/carousel-story.css | 31 ++++++++++++------------ 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/blocks/carousel-story/carousel-story.css b/blocks/carousel-story/carousel-story.css index 553b2b8..0c5020f 100644 --- a/blocks/carousel-story/carousel-story.css +++ b/blocks/carousel-story/carousel-story.css @@ -56,27 +56,28 @@ /* Dot-grid watermark, right side — the actual SVG fortinet.com uses (55 individual dots, fetched and inlined by carousel-story.js, not a CSS tiled-background approximation), rotated 90deg to stand it up into a - tall strip the way the source does. Measured directly on fortinet.com: - this starts at roughly the vertical midpoint of the photo (photo top - 83px + half its 515px height ≈ 340px — see .carousel-story-float-photo - below for both those numbers), not at this section's own top, and sits - right:52px in from this section's own right edge — a real margin, not a - bleed like the photo/quote-card get. Lives directly in .carousel-story - (which has no overflow restriction, see above). Desktop-only: at mobile - widths the image and text stack full-width, leaving no space for this to - sit beside them without covering the quote card. */ + tall strip the way the source does. Measured directly on fortinet.com + (svg.customer-dots-bg, relative to its own #banner1): top:312px, + height:500px — not exactly "half of the photo" (83 + 515/2 ≈ 340) as an + earlier revision assumed; that was a reasonable-looking guess but measured + ~28px short of the real value. right:51px in from this section's own + right edge is a real margin, not a bleed like the photo/quote-card get. + Lives directly in .carousel-story (which has no overflow restriction, see + above). Desktop-only: at mobile widths the image and text stack + full-width, leaving no space for this to sit beside them without covering + the quote card. */ @media (width >= 900px) { .carousel-story-dots { position: absolute; z-index: 2; - top: 340px; - right: 52px; + top: 312px; + right: 51px; width: 200px; - height: 496px; + height: 500px; pointer-events: none; - --carousel-story-drop-distance: 340px; /* matches top:340px above — starting - the animation at translateY(-340px) puts it flush with this section's own + --carousel-story-drop-distance: 312px; /* matches top:312px above — starting + the animation at translateY(-312px) puts it flush with this section's own top edge. */ } @@ -88,7 +89,7 @@ position: absolute; top: 50%; left: 50%; - width: 496px; + width: 500px; height: 200px; transform: translate(-50%, -50%) rotate(90deg); } From fc756b4b4d9d3b1ae63aed7a7bcaef4bc46ea6f5 Mon Sep 17 00:00:00 2001 From: Sam Shao Date: Sat, 8 Aug 2026 12:55:13 -0700 Subject: [PATCH 3/5] Hide dot-grid watermark below 900px, matching fortinet.com MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fortinet.com sets svg.customer-dots-bg to display:none below 900px (confirmed directly). Ours had no base rule at all for .carousel-story-dots — only the >=900px positioning — so at mobile widths it fell back to default block display with no positioning, rendering as an unstyled, full-viewport-width strip of dots below the slide indicators (spanning x:0 to the full width, with no relation to the photo it's meant to accompany). Adding an explicit display:none base rule, overridden back to display:block only at >=900px, matches the source and removes the mobile-only strip. Co-Authored-By: Claude Sonnet 5 --- blocks/carousel-story/carousel-story.css | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/blocks/carousel-story/carousel-story.css b/blocks/carousel-story/carousel-story.css index 0c5020f..3f16568 100644 --- a/blocks/carousel-story/carousel-story.css +++ b/blocks/carousel-story/carousel-story.css @@ -63,11 +63,23 @@ ~28px short of the real value. right:51px in from this section's own right edge is a real margin, not a bleed like the photo/quote-card get. Lives directly in .carousel-story (which has no overflow restriction, see - above). Desktop-only: at mobile widths the image and text stack - full-width, leaving no space for this to sit beside them without covering - the quote card. */ + above). + + fortinet.com sets this to display:none below 900px (confirmed directly: + svg.customer-dots-bg computes display:none there) rather than just + omitting positioning for it — at mobile widths the image and text stack + full-width, leaving no room for this beside them without covering the + quote card. Without an explicit display:none here too, this div (real + content, not empty) fell back to its default block display, rendering as + an unstyled, unpositioned, full-viewport-width strip below the slide + indicators — not just "not ideally placed" but genuinely unstyled. */ +.carousel-story-dots { + display: none; +} + @media (width >= 900px) { .carousel-story-dots { + display: block; position: absolute; z-index: 2; top: 312px; From 43f9b79832aa09c9678e18cf00db320783b1cda1 Mon Sep 17 00:00:00 2001 From: Sam Shao Date: Sat, 8 Aug 2026 13:04:13 -0700 Subject: [PATCH 4/5] Fix photo/dots stacking order so the photo hides the overlap, matching fortinet.com MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured directly on fortinet.com: the photo and dots overlap horizontally by ~126px (img right edge sits well past the dots' left edge), with the photo stacked above the dots there (img z-index:1000 > dots z-index:999) — so the overlapping portion of the dot pattern is hidden behind the photo, not drawn over it. Our own photo/dots bounding boxes already overlapped by the same ~124px, but .carousel-story-float-photo had no explicit z-index (auto), stacking below .carousel-story-dots' z-index:2 — the opposite of the source, so our dots drew on top of the photo instead of hiding behind it. Giving the photo z-index:3 (just needs to beat the dots' 2, not literally match fortinet's much larger values) fixes the stacking order. Co-Authored-By: Claude Sonnet 5 --- blocks/carousel-story/carousel-story.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/blocks/carousel-story/carousel-story.css b/blocks/carousel-story/carousel-story.css index 3f16568..21fbfef 100644 --- a/blocks/carousel-story/carousel-story.css +++ b/blocks/carousel-story/carousel-story.css @@ -620,6 +620,14 @@ lines up with where the real image column sits. */ .carousel-story-float-photo { position: absolute; + z-index: 3; /* fortinet.com's own photo/dots overlap horizontally by + ~126px (measured directly: img z-index:1000, dots z-index:999) — the + photo sits above the dots there, hiding the overlapping portion of the + pattern behind it. Without an explicit z-index here (auto), this + container stacked BELOW .carousel-story-dots' z-index:2, so the same + overlap instead drew the dots on top of the photo — backwards from + the source. 3 only needs to beat that 2, not literally match fortinet's + much larger values. */ inset: 0; max-width: 1240px; margin: 0 auto; From 8c7f1417ce6b51fd4411ae9726dd8f9e65689710 Mon Sep 17 00:00:00 2001 From: Sam Shao Date: Mon, 10 Aug 2026 08:23:13 -0700 Subject: [PATCH 5/5] Fix dot-grid watermark drifting from the photo at wide viewports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: .carousel-story-dots' own right:51px lived directly in .carousel-story (the full-bleed section), so it resolved against that element's true (viewport-wide) right edge — not against the centered 1240px content column the photo itself sits in. That 51px value was measured as a margin from the true edge at one specific viewport width (where it happened to look right), not from the image, so at any other width the true edge moves a different amount than the image's own edge does, and the two drift apart. Confirmed directly: 103px short of the photo's actual right edge at 1920px, correct-looking only close to the width it was tuned at. Fixed the same way .carousel-story-float-photo already solves this exact problem: a new .carousel-story-dots-frame wraps the real .carousel-story- dots, mirroring the photo's own outer wrapper (max-width:1240px, centered) instead of living straight in the full-bleed section. right is now a negative value (-59.29px, poking out past the frame's own edge) rather than 51px, because the correct number isn't a simple margin at all: verified directly on fortinet.com that its own dots and photo overlap by a constant 125.71px regardless of viewport width (identical at both 1464px and 1920px) — recalibrated from that overlap plus the photo's own right:15px and this box's own 200px width, rather than reusing a value that was only ever correct by coincidence at one width. Verified: overlap with the photo is now 125.72px at both 1464px and 1920px (was 125.72 vs a photo-detached -103 before this fix). Mobile unaffected (dots-frame stays display:none there, confirmed zero-size). This branch also had 4 already-tested follow-up commits sitting unmerged since PR #23 was merged at an earlier point on it (see PR #30) — this commit continues on top of those. Co-Authored-By: Claude Sonnet 5 --- blocks/carousel-story/carousel-story.css | 52 ++++++++++++++++++++---- blocks/carousel-story/carousel-story.js | 13 +++++- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/blocks/carousel-story/carousel-story.css b/blocks/carousel-story/carousel-story.css index 21fbfef..e92c47c 100644 --- a/blocks/carousel-story/carousel-story.css +++ b/blocks/carousel-story/carousel-story.css @@ -60,10 +60,23 @@ (svg.customer-dots-bg, relative to its own #banner1): top:312px, height:500px — not exactly "half of the photo" (83 + 515/2 ≈ 340) as an earlier revision assumed; that was a reasonable-looking guess but measured - ~28px short of the real value. right:51px in from this section's own - right edge is a real margin, not a bleed like the photo/quote-card get. - Lives directly in .carousel-story (which has no overflow restriction, see - above). + ~28px short of the real value. right:51px is a real margin, not a bleed + like the photo/quote-card get — but measured relative to the *image*, + which sits inside the centered 1240px content column, not relative to + this section's own true (full-bleed) right edge. An earlier revision set + right:51px directly on .carousel-story-dots itself, which lives straight + in .carousel-story (full-bleed) — right there resolves against that + full-bleed edge, so the 51px gap only looked correct at the specific + viewport width it was measured at; at any wider width the true edge + moves further from the image than the content column's own edge does, + dragging the dots away from the photo (confirmed directly: 103px short + of the photo's right edge at 1920px, vs. the intended ~51px). Fixed the + same way .carousel-story-float-photo already handles this exact problem: + an outer reference frame mirrors the centered content column's own + max-width/centering (see .carousel-story-dots-frame below), and the + actual positioned/sized box — this rule — lives inside that frame + instead of directly in the full-bleed section, so right:51px resolves + against the column's own edge and tracks the image at every width. fortinet.com sets this to display:none below 900px (confirmed directly: svg.customer-dots-bg computes display:none there) rather than just @@ -73,20 +86,43 @@ content, not empty) fell back to its default block display, rendering as an unstyled, unpositioned, full-viewport-width strip below the slide indicators — not just "not ideally placed" but genuinely unstyled. */ -.carousel-story-dots { +.carousel-story-dots-frame { display: none; } @media (width >= 900px) { - .carousel-story-dots { + .carousel-story-dots-frame { display: block; position: absolute; + inset: 0; z-index: 2; + max-width: 1240px; + margin: 0 auto; + pointer-events: none; + } + + /* right is negative — the dots box pokes out past .carousel-story-dots- + frame's own right edge, rather than sitting inside it. This isn't a + simple fixed margin: fortinet.com's own dots and photo overlap + horizontally by a constant 125.71px, verified directly at both 1464px + and 1920px widths (identical both times) — this position is calibrated + from THAT overlap plus the photo's own right:15px (see + .carousel-story-float-photo::after below) and this box's own 200px + width, not from a directly-measured "right" value: photo's own right + inset (-15, i.e. 15px in) + (this box's 200px width − the 125.71px + overlap) = 59.29px past the frame's edge, i.e. right:-59.29px. A + literal right:51px here (an earlier revision's value, actually + measured as a margin from .carousel-story's own full-bleed edge, back + when this element lived there directly rather than in this centered + frame) stayed put relative to the *frame*, not the image, so it + silently drifted away from the photo at any width other than the one + it happened to be measured at. */ + .carousel-story-dots { + position: absolute; top: 312px; - right: 51px; + right: -59.29px; width: 200px; height: 500px; - pointer-events: none; --carousel-story-drop-distance: 312px; /* matches top:312px above — starting the animation at translateY(-312px) puts it flush with this section's own diff --git a/blocks/carousel-story/carousel-story.js b/blocks/carousel-story/carousel-story.js index cc03d59..484bf4d 100644 --- a/blocks/carousel-story/carousel-story.js +++ b/blocks/carousel-story/carousel-story.js @@ -329,10 +329,21 @@ export default async function decorate(block) { // Static (not re-synced per slide like the photo above): every slide // shares the same fixed photo top/height (see carousel-story.css), so // there's a single position that's correct for all of them. + // + // dotsFrame mirrors .carousel-story-float-photo's own outer wrapper — a + // reference frame matching the centered 1240px content column's own + // max-width/centering — so dots' own top/right values (set on the real + // .carousel-story-dots inside it) resolve against the image's actual + // edge instead of this section's full-bleed one, which only looked + // right at the specific viewport width it was measured at (see + // carousel-story.css for the full explanation). + const dotsFrame = document.createElement('div'); + dotsFrame.className = 'carousel-story-dots-frame'; const dots = document.createElement('div'); dots.className = 'carousel-story-dots'; dots.setAttribute('aria-hidden', 'true'); - block.append(dots); + dotsFrame.append(dots); + block.append(dotsFrame); fetch(`${window.hlx.codeBasePath}/icons/carousel-story-dots.svg`) .then((resp) => (resp.ok ? resp.text() : '')) .then((svg) => { dots.innerHTML = svg; })