From ed4801eeefd39e8a6aa219816b2d59fc0bbd2bc8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 14:52:42 +0000 Subject: [PATCH 1/5] feat(home): add featured case-study slider with metrics and logo tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New homepage section between Efficient Platforms and the testimonials: one dark pine card per case study (client logo pill, category eyebrow, short title, blurb, two headline metrics, CTA, photo) on a sliding track, navigated by a client-logo tab strip with an autoplay progress bar — the Microsoft-customer-stories pattern in Masterpoint's theme. - layouts/shortcodes/case-study-slider.html: markup + self-contained vanilla JS (the global flexslider init would fight logo-tab nav). Autoplay advances on the progress bar's animationend, pauses on hover/focus (tracked independently so mouse-leave can't resume while keyboard focus is inside), has a persistent pause/play rotation control (WCAG 2.2.2), only runs while ≥25% on screen, and is fully disabled under prefers-reduced-motion. Touch swipe + arrow keys. - Card content comes from a new optional `highlight:` front-matter map on each case study (eyebrow/title/blurb/image/logo/2 stats), with fallbacks to title/description/preview_image/stat_bar/client_logo; `highlight: false` opts out, non-map values can't crash the build (reflect.IsMap guard). Single-slide mode drops tab ARIA + autoplay. - Styles in custom.scss (.csh-*), echoing the case-study hero (pine gradient, dot grid, mint/pink glows, brand-gradient stat values). - Docs: new "Homepage highlights slider" section in docs/case-studies.md. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UrsnEwWx53uzzFEyD5coNs --- assets/css/custom.scss | 456 ++++++++++++++++++++++ content/case-studies/marketspark.md | 14 + content/case-studies/power-digital.md | 14 + content/sections/home-case-studies.md | 23 ++ content/sections/home-our-word.md | 2 +- docs/case-studies.md | 89 +++++ layouts/shortcodes/case-study-slider.html | 262 +++++++++++++ 7 files changed, 859 insertions(+), 1 deletion(-) create mode 100644 content/sections/home-case-studies.md create mode 100644 layouts/shortcodes/case-study-slider.html diff --git a/assets/css/custom.scss b/assets/css/custom.scss index 5a41cea0..7d63a325 100644 --- a/assets/css/custom.scss +++ b/assets/css/custom.scss @@ -4250,4 +4250,460 @@ font-size: 1.55rem; } } +// --------------------------------------------------------------------------- +// HOME — CASE STUDY HIGHLIGHTS SLIDER (#case-study-highlights, .csh-*) +// Featured case-study cards on a sliding track, navigated by a client-logo +// tab strip with an autoplay progress bar. Markup lives in +// layouts/shortcodes/case-study-slider.html; see docs/case-studies.md. +// --------------------------------------------------------------------------- +.csh { + margin-top: 2.6rem; + + &__viewport { + overflow: hidden; + border-radius: 24px; + box-shadow: 0 24px 48px -28px rgba(14, 56, 58, 0.45); + } + &__track { + display: flex; + transition: transform 0.65s cubic-bezier(0.33, 0, 0.15, 1); + will-change: transform; + } +} + +.csh-slide { + flex: 0 0 100%; + min-width: 100%; + // Keep off-screen slides out of the accessibility tree / paint until they + // slide in: hide only after the track transition has finished. + visibility: hidden; + transition: visibility 0s 0.65s; + &.is-active { + visibility: visible; + transition-delay: 0s; + } +} + +.csh-card { + display: flex; + height: 100%; + min-height: 460px; + position: relative; + overflow: hidden; + border-radius: 24px; + background: linear-gradient(135deg, #0e383a 0%, #0a2a2c 55%, #061d1e 100%); + color: #fff; + + // soft mint / pink glows (echoes the case-study hero) + &::before { + content: ""; + position: absolute; + inset: 0; + background: + radial-gradient(ellipse at 85% 15%, rgba(42, 217, 194, 0.16), transparent 55%), + radial-gradient(ellipse at 8% 92%, rgba(216, 145, 206, 0.12), transparent 50%); + pointer-events: none; + } + // faint dot grid, fading downward + &::after { + content: ""; + position: absolute; + inset: 0; + background-image: radial-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px); + background-size: 28px 28px; + mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.7), transparent 80%); + -webkit-mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.7), transparent 80%); + pointer-events: none; + } + + &__topline { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + z-index: 2; + background: linear-gradient(92deg, #ede497 -0.66%, #2ad9c2 26%, #d891ce 59.55%); + } + + &__body { + position: relative; + z-index: 1; + flex: 1 1 55%; + min-width: 0; + padding: 3rem 3rem 2.8rem; + display: flex; + flex-direction: column; + align-items: flex-start; + } + + &__logo { + background: #fff; + border-radius: 10px; + padding: 0.6rem 0.95rem; + margin-bottom: 1.6rem; + box-shadow: 0 8px 18px -10px rgba(0, 0, 0, 0.5); + img { + display: block; + height: 26px; + width: auto; + } + } + + &__eyebrow { + color: #2ad9c2; + text-transform: uppercase; + font-weight: 700; + font-size: 0.8rem; + letter-spacing: 0.14em; + margin-bottom: 0.8rem; + } + + &__title { + font-size: 1.85rem; + font-weight: 800; + line-height: 1.22em; + color: #fff; + margin-bottom: 0.9rem; + max-width: 36rem; + } + + &__blurb { + color: rgba(255, 255, 255, 0.82); + font-size: 1rem; + line-height: 1.55em; + margin-bottom: 1.7rem; + max-width: 34rem; + } + + &__stats { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.4rem 2rem; + align-items: start; + width: 100%; + max-width: 34rem; + margin-bottom: 2rem; + } + + &__cta { + margin-top: auto; + } + + &__media { + flex: 1 1 45%; + position: relative; + min-height: 300px; + img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + } + // blend the photo into the pine card + &::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(90deg, rgba(10, 42, 44, 0.55) 0%, rgba(10, 42, 44, 0) 35%); + pointer-events: none; + } + } +} + +.csh-stat { + &__value { + @extend .text-gradient; + font-size: 2.3rem; + font-weight: 800; + line-height: 1em; + white-space: nowrap; + } + &__label { + color: rgba(255, 255, 255, 0.75); + font-size: 0.95rem; + line-height: 1.35em; + max-width: 15rem; + padding-top: 0.45em; + } +} + +.csh-nav { + display: flex; + align-items: center; + // centered on the outer row; the inner __tabs group scrolls when it + // overflows (centering the scroll container itself would clip its left end) + justify-content: center; + gap: 0.9rem; + margin-top: 1.6rem; + min-width: 0; + + &__tabs { + display: flex; + gap: 1rem; + min-width: 0; + overflow-x: auto; + padding: 0.25rem 0.25rem 0.45rem; + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } + } + + // persistent autoplay pause/play control (WCAG 2.2.2) + &__toggle { + flex: 0 0 auto; + appearance: none; + -webkit-appearance: none; + width: 46px; + height: 46px; + display: flex; + align-items: center; + justify-content: center; + background: #fff; + border: 1px solid #dfe7e7; + border-radius: 50%; + cursor: pointer; + transition: + border-color 0.25s, + box-shadow 0.25s; + svg { + display: block; + width: 13px; + height: 13px; + fill: #0e383a; + } + .csh-nav__icon-play { + display: none; + } + &:hover, + &:focus-visible { + border-color: #2ad9c2; + } + &:focus-visible { + outline: 2px solid #2ad9c2; + outline-offset: 2px; + } + } + + // text fallback when a case study has no logo for the tab strip + &__name { + display: block; + font-weight: 700; + font-size: 1rem; + line-height: 30px; + color: #0e383a; + white-space: nowrap; + } + + &__tab { + flex: 0 0 auto; + appearance: none; + -webkit-appearance: none; + background: #fff; + border: 1px solid #dfe7e7; + border-radius: 14px; + padding: 1.05rem 2.2rem 1.2rem; + cursor: pointer; + position: relative; + overflow: hidden; + transition: + border-color 0.25s, + box-shadow 0.25s; + + img { + display: block; + height: 30px; + width: auto; + filter: grayscale(1); + opacity: 0.5; + transition: + filter 0.25s, + opacity 0.25s; + } + &:hover, + &:focus-visible { + border-color: #b9cdcd; + img { + filter: none; + opacity: 1; + } + } + &:focus-visible { + outline: 2px solid #2ad9c2; + outline-offset: 2px; + } + &.is-active { + border-color: rgba(42, 217, 194, 0.5); + box-shadow: 0 12px 24px -14px rgba(14, 56, 58, 0.4); + img { + filter: none; + opacity: 1; + } + } + } + + &__bar { + display: block; + position: absolute; + // inset 1px so the bar hugs the inside of the tab border and its rounded + // corners instead of notching past them + left: 1px; + right: 1px; + bottom: 1px; + height: 4px; + border-radius: 0 0 13px 13px; + overflow: hidden; + background: rgba(14, 56, 58, 0.08); + } + &__progress { + display: block; + height: 100%; + // full-width gradient revealed by the clip-path animation, so the colors + // stay put while the bar fills (a width animation would stretch them) + width: 100%; + background: linear-gradient(92deg, #ede497 -0.66%, #2ad9c2 26%, #d891ce 59.55%); + clip-path: inset(0 100% 0 0); + } +} + +// Autoplay: the active tab's bar fills over 8s (only while the slider is on +// screen); the shortcode JS advances on animationend and toggles the classes. +// The paused/stopped rules must match the run rule's specificity and come +// AFTER it — the `animation` shorthand resets animation-play-state to +// `running`, so a lower-specificity pause rule would silently lose. +.csh.csh--inview .csh-nav__tab.is-active .csh-nav__progress { + animation: cshProgress 8s linear forwards; +} +.csh.csh--paused .csh-nav__tab.is-active .csh-nav__progress { + animation-play-state: paused; +} +.csh.csh--stopped .csh-nav__tab.is-active .csh-nav__progress { + animation: none; +} +.csh.csh--stopped .csh-nav__toggle { + .csh-nav__icon-pause { + display: none; + } + .csh-nav__icon-play { + display: block; + } +} +@keyframes cshProgress { + from { + clip-path: inset(0 100% 0 0); + } + to { + clip-path: inset(0 0 0 0); + } +} + +.csh-see-all { + margin-top: 2.4rem; +} + +@media (prefers-reduced-motion: reduce) { + .csh__track { + transition: none; + } + .csh-slide { + transition: none; + } + .csh .csh-nav__progress { + animation: none !important; + } +} + +@media (max-width: 991px) { + .csh-card { + // media (last child) on top, text below — mirrors the list-page cards + flex-direction: column-reverse; + min-height: 0; + + &__body { + padding: 2.2rem 1.7rem 2rem; + } + &__title { + font-size: 1.45rem; + } + &__media { + flex: 0 0 auto; + width: 100%; + min-height: 0; + height: 240px; + &::after { + background: linear-gradient(0deg, rgba(10, 42, 44, 0.55) 0%, rgba(10, 42, 44, 0) 40%); + } + } + } + .csh-stat { + &__value { + font-size: 2rem; + } + &__label { + font-size: 0.9rem; + } + } + .csh-nav__toggle { + width: 40px; + height: 40px; + } + .csh-nav__tab { + padding: 0.85rem 1.5rem 1rem; + border-radius: 12px; + img { + height: 24px; + } + } + .csh-nav__name { + line-height: 24px; + font-size: 0.95rem; + } +} + +@media (max-width: 575px) { + .csh { + margin-top: 1.8rem; + } + .csh__viewport { + border-radius: 18px; + } + .csh-card { + border-radius: 18px; + &__body { + padding: 1.9rem 1.3rem 1.8rem; + } + // single column so wide values ("0% → 100%") never clip + &__stats { + grid-template-columns: 1fr; + gap: 1.1rem; + } + &__media { + height: 200px; + } + } + // tighten the control strip so toggle + both tabs fit a small phone + .csh-nav { + gap: 0.6rem; + } + .csh-nav__tabs { + gap: 0.6rem; + } + .csh-nav__toggle { + width: 36px; + height: 36px; + } + .csh-nav__tab { + padding: 0.7rem 1.1rem 0.85rem; + img { + height: 22px; + } + } + .csh-nav__name { + line-height: 22px; + font-size: 0.9rem; + } +} + @import "case-studies.scss"; diff --git a/content/case-studies/marketspark.md b/content/case-studies/marketspark.md index 2e1a2671..618c6a8d 100644 --- a/content/case-studies/marketspark.md +++ b/content/case-studies/marketspark.md @@ -29,6 +29,20 @@ stat_bar: preview_image: /img/case-studies/marketspark/marketspark-aws.png og_img: /img/case-studies/marketspark/marketspark-aws.png +# Homepage highlight card (see docs/case-studies.md → Homepage slider) +highlight: + eyebrow: "AWS · Infrastructure as Code" + title: "MarketSpark's cloud, rebuilt as a 100% IaC multi-account platform" + blurb: "Masterpoint rebuilt MarketSpark's single manually-operated AWS account into an 11-account AWS Organization: fully codified, automated, and ready to scale." + image: /img/case-studies/marketspark/telecommunications.jpg + image_alt: "City lights glowing across Earth at night, seen from orbit" + logo: /img/case-studies/marketspark/marketspark-logo-dark.png + stats: + - value: "0% → 100%" + label: "Infrastructure as Code, automated end to end" + - value: "1 → 11" + label: "monolithic account to a full AWS Organization" + sitemap: priority: 0 --- diff --git a/content/case-studies/power-digital.md b/content/case-studies/power-digital.md index 5e2fe24f..78ce4a94 100644 --- a/content/case-studies/power-digital.md +++ b/content/case-studies/power-digital.md @@ -29,6 +29,20 @@ stat_bar: preview_image: /img/case-studies/power-digital/power-digital-case-study-preview.jpg og_img: /img/case-studies/power-digital/power-digital-case-study-preview.jpg +# Homepage highlight card (see docs/case-studies.md → Homepage slider) +highlight: + eyebrow: "IaC Automation · Cost Optimization" + title: "Power Digital cut automation costs 10x and onboards clients 8x faster" + blurb: "Masterpoint migrated Power Digital's 43,000-resource Terraform monolith to Spacelift and OpenTofu, and the platform onboarded 100+ new clients in the first 60 days." + image: /img/case-studies/power-digital/power-digital-team.jpg + image_alt: "Power Digital team collaborating in their office" + logo: /img/case-studies/power-digital/power-digital-logo.png + stats: + - value: "10x" + label: "lower infrastructure automation costs" + - value: "25 → 3 min" + label: "plan & apply deployment cycles" + sitemap: priority: 0 diff --git a/content/sections/home-case-studies.md b/content/sections/home-case-studies.md new file mode 100644 index 00000000..d808c405 --- /dev/null +++ b/content/sections/home-case-studies.md @@ -0,0 +1,23 @@ +--- +title: Case Study Success Stories +weight: 5 +section_categories: + - Home +id: case-study-highlights +--- + +
+
+ +### Real Engagements, Real Results {.groupTitle .text-center .text-rhythm} + +

See how engineering teams partner with Masterpoint to turn tangled infrastructure into platforms that scale.

+ +
+
+ +{{}} + + diff --git a/content/sections/home-our-word.md b/content/sections/home-our-word.md index 772aec2d..0e77fc25 100644 --- a/content/sections/home-our-word.md +++ b/content/sections/home-our-word.md @@ -1,6 +1,6 @@ --- title: Don't take our word for it... -weight: 5 +weight: 6 #position: align-items-center justify-content-end size: col-12 col-md-7 #section_image: /img/doctor_and_patient.jpg diff --git a/docs/case-studies.md b/docs/case-studies.md index 7a6bee88..df0bd530 100644 --- a/docs/case-studies.md +++ b/docs/case-studies.md @@ -72,6 +72,95 @@ card grid to stay scannable as the list grows. Card image = each study's --- +## Homepage highlights slider + +The homepage has a featured case-study slider (section +`content/sections/home-case-studies.md`, id `#case-study-highlights`, weight 5 — +between "Efficient Platforms" and the testimonials). It renders one full-width +dark pine card per case study (client logo on a white pill, category eyebrow, +short title, blurb, **two headline metrics**, CTA, photo) on a sliding track, +navigated by a client-logo tab strip — the Microsoft-customer-stories pattern. + +- **Markup + JS:** `layouts/shortcodes/case-study-slider.html` (self-contained + vanilla JS; no jQuery). It iterates `where site.RegularPages "Section" + "case-studies"` **ByWeight**, so case-study `weight:` also orders the slides. +- **Styles:** `.csh-*` block in `assets/css/custom.scss` (directly above the + `case-studies.scss` import). Scoped to `.csh-`; literal hex colors since the + `$cs-*` vars aren't defined at that point in the cascade. +- **Content comes from an optional `highlight:` front-matter map** on each case + study (see marketspark.md / power-digital.md): + + ```yaml + highlight: + eyebrow: "AWS · Infrastructure as Code" # category label, mint uppercase + title: "... ..." # short card title (HTML ok) + blurb: "One or two sentences." + image: /img/case-studies/CLIENT/photo.jpg # card photo (right column) + image_alt: "..." + logo: /img/case-studies/CLIENT/CLIENT-logo-dark.png # DARK logo variant — used on the white tab strip AND the white card pill + stats: # exactly two headline metrics + - value: "10x" + label: "lower infrastructure automation costs" + - value: "25 → 3 min" + label: "plan & apply deployment cycles" + ``` + + Every field falls back sensibly if omitted: `title` → page title, `blurb` → + `description`, `image` → `preview_image`, `stats` → first 2 `stat_bar` + entries, `logo` → `client_logo`, `eyebrow` → `Case Study · {client}`. + Set `highlight: false` to keep a case study out of the slider entirely. Any + other **non-map** value (`highlight: true`, a string, …) includes the study + with pure fallbacks — the template only reads fields after a `reflect.IsMap` + check, so a scalar can't crash the build (`$h.foo` on a bool is a fatal + template error; `| default dict` does NOT replace `true`). +- **Use the dark logo variant** for `highlight.logo` — it sits on white + surfaces (tab strip + card pill). The hero `client_logo` may be a white + variant (Power Digital), which would vanish there; that's why the field + exists. A study with no logo at all gets its client name as a text tab + (`.csh-nav__name`). +- **Slider mechanics:** custom vanilla JS in the shortcode, NOT flexslider/owl — + plugins.js initializes ALL `.flexslider` elements globally with dot controls, + which would fight the logo-tab navigation. Autoplay is driven by the active + tab's progress-bar CSS animation (8s, `cshProgress` keyframes): JS advances on + `animationend`, so pausing the animation pauses the slider exactly in sync + with the bar. The bar is a full-width gradient revealed via `clip-path` (a + width animation would stretch the colors). Touch swipe + roving-tabindex + arrow keys included; off-screen slides are `visibility: hidden` (delayed + until the track transition ends) with their links untabbable. +- **Autoplay gating — four independent conditions, all in CSS classes:** + - `.csh--inview` (IntersectionObserver): on once ≥25% visible, off only when + fully off-screen. Gate on `intersectionRatio`, not bare `isIntersecting` — + that flips true at the first visible pixel regardless of `threshold`. + - `.csh--paused` (transient): mouse hover OR keyboard focus, tracked as two + separate booleans — `focusout` only clears it if focus actually left the + root (`relatedTarget` check), so mouse-leave can't resume rotation while + focus is still inside (an auto-advance would hide the focused element). + Hover uses `pointerenter`/`pointerleave` gated to `pointerType === "mouse"` + — a tap's synthetic `mouseenter` never gets a matching `mouseleave` on + touch screens and would freeze rotation forever. + - `.csh--stopped` (persistent): the pause/play toggle button before the tab + strip — the WCAG 2.2.2 / APG-required rotation control, first in the + carousel's tab order, `aria-pressed` + swapped `aria-label`. + - `prefers-reduced-motion: reduce` disables the animation outright → no + `animationend` → no autoplay (manual navigation still works). +- **Specificity gotcha:** the `animation:` shorthand resets + `animation-play-state` to `running`, so the paused/stopped rules must MATCH + the run rule's 5-class specificity and come after it. A shorter + `.csh.csh--paused .csh-nav__progress` rule silently loses — hover-pause + then never works. +- **Single-slide degradation:** with one highlighted study the tab strip, + toggle, and autoplay are omitted and the slide renders WITHOUT + `role="tabpanel"`/`aria-labelledby` (they'd reference a tab that doesn't + exist — dangling ARIA idrefs fail axe). +- **Tab strip centering:** `justify-content: center` lives on the OUTER + `.csh-nav` row; the inner `.csh-nav__tabs` group is the `overflow-x: auto` + scroller. Centering the scroll container itself would clip its left end once + tabs overflow. +- The section heading/intro and the "See All Case Studies" button live in the + section content file, not the shortcode. + +--- + ## Modern layout — front matter schema ```yaml diff --git a/layouts/shortcodes/case-study-slider.html b/layouts/shortcodes/case-study-slider.html new file mode 100644 index 00000000..294c77c9 --- /dev/null +++ b/layouts/shortcodes/case-study-slider.html @@ -0,0 +1,262 @@ +{{- /* + Homepage case-study highlights slider (see docs/case-studies.md → Homepage slider). + One featured card per case study (logo pill, eyebrow, title, blurb, two stats, + CTA, photo) sliding in a track, with a client-logo tab strip as the navigation. + Card content comes from each case study's optional `highlight:` front matter, + falling back to title / description / preview_image / stat_bar / client_logo. + Set `highlight: false` on a case study to keep it out of the slider; any other + non-map value (e.g. `highlight: true`) includes it with pure fallbacks. +*/ -}} +{{- $pages := slice -}} +{{- range (where site.RegularPages "Section" "case-studies").ByWeight -}} + {{- if ne .Params.highlight false -}}{{- $pages = $pages | append . -}}{{- end -}} +{{- end -}} +{{- $multi := gt (len $pages) 1 -}} +
+
+
+ {{- range $i, $p := $pages -}} + {{- $h := dict -}} + {{- if reflect.IsMap $p.Params.highlight -}}{{- $h = $p.Params.highlight -}}{{- end -}} + {{- $client := $p.Params.client | default $p.Title -}} + {{- $stats := $h.stats | default (first 2 ($p.Params.stat_bar | default slice)) -}} +
+
+ +
+ {{- with $h.logo | default $p.Params.client_logo }} + + {{- end }} +
+ {{ $h.eyebrow | default (printf "Case Study · %s" $client) | safeHTML }} +
+

+ {{ $h.title | default $p.Title | safeHTML }} +

+

+ {{ $h.blurb | default $p.Description | safeHTML }} +

+ {{- with $stats }} +
+ {{- range . }} +
+
{{ .value | safeHTML }}
+
{{ .label | safeHTML }}
+
+ {{- end }} +
+ {{- end }} + + Read the Case Study → + +
+ {{- with $h.image | default $p.Params.preview_image }} +
+ {{ $h.image_alt | default (printf `%s case study` $client) }} +
+ {{- end }} +
+
+ {{- end }} +
+
+ {{- if $multi }} +
+ {{- /* Rotation control first in the carousel's tab order (WCAG 2.2.2 / APG) */}} + +
+ {{- range $i, $p := $pages -}} + {{- $h := dict -}} + {{- if reflect.IsMap $p.Params.highlight -}}{{- $h = $p.Params.highlight -}}{{- end -}} + {{- $client := $p.Params.client | default $p.Title -}} + {{- $logo := $h.logo | default $p.Params.client_logo -}} + + {{- end }} +
+
+ {{- end }} +
+ From 54f81bf13afe7a1d66388dbadab72182270345e4 Mon Sep 17 00:00:00 2001 From: oycyc Date: Mon, 20 Jul 2026 10:37:37 -0400 Subject: [PATCH 2/5] feat(case-study): home page slider cards for the stories Simplify the homepage highlight cards: drop the eyebrow and the two-stat row, reuse each study's gradient hero title, and render the client logo directly on the pine card (white variant, sized via client_logo_height) instead of inside a white pill. --- assets/css/custom.scss | 61 ++---------- content/case-studies/marketspark.md | 9 +- content/case-studies/power-digital.md | 11 +-- content/sections/home-case-studies.md | 2 +- docs/case-studies.md | 107 ++++++---------------- layouts/shortcodes/case-study-slider.html | 26 ++---- 6 files changed, 45 insertions(+), 171 deletions(-) diff --git a/assets/css/custom.scss b/assets/css/custom.scss index 7d63a325..03089602 100644 --- a/assets/css/custom.scss +++ b/assets/css/custom.scss @@ -4335,30 +4335,21 @@ font-size: 1.55rem; display: flex; flex-direction: column; align-items: flex-start; + // slides share the tallest card's height; centering the content group keeps + // a shorter card (fewer title lines / blurb) balanced instead of leaving a + // dead gap above a bottom-pinned CTA + justify-content: center; } &__logo { - background: #fff; - border-radius: 10px; - padding: 0.6rem 0.95rem; margin-bottom: 1.6rem; - box-shadow: 0 8px 18px -10px rgba(0, 0, 0, 0.5); img { display: block; - height: 26px; + height: var(--cs-client-logo-h, 32px); width: auto; } } - &__eyebrow { - color: #2ad9c2; - text-transform: uppercase; - font-weight: 700; - font-size: 0.8rem; - letter-spacing: 0.14em; - margin-bottom: 0.8rem; - } - &__title { font-size: 1.85rem; font-weight: 800; @@ -4376,18 +4367,8 @@ font-size: 1.55rem; max-width: 34rem; } - &__stats { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1.4rem 2rem; - align-items: start; - width: 100%; - max-width: 34rem; - margin-bottom: 2rem; - } - &__cta { - margin-top: auto; + margin-top: 0.4rem; } &__media { @@ -4412,23 +4393,6 @@ font-size: 1.55rem; } } -.csh-stat { - &__value { - @extend .text-gradient; - font-size: 2.3rem; - font-weight: 800; - line-height: 1em; - white-space: nowrap; - } - &__label { - color: rgba(255, 255, 255, 0.75); - font-size: 0.95rem; - line-height: 1.35em; - max-width: 15rem; - padding-top: 0.45em; - } -} - .csh-nav { display: flex; align-items: center; @@ -4637,14 +4601,6 @@ font-size: 1.55rem; } } } - .csh-stat { - &__value { - font-size: 2rem; - } - &__label { - font-size: 0.9rem; - } - } .csh-nav__toggle { width: 40px; height: 40px; @@ -4674,11 +4630,6 @@ font-size: 1.55rem; &__body { padding: 1.9rem 1.3rem 1.8rem; } - // single column so wide values ("0% → 100%") never clip - &__stats { - grid-template-columns: 1fr; - gap: 1.1rem; - } &__media { height: 200px; } diff --git a/content/case-studies/marketspark.md b/content/case-studies/marketspark.md index 618c6a8d..9530f461 100644 --- a/content/case-studies/marketspark.md +++ b/content/case-studies/marketspark.md @@ -31,17 +31,10 @@ og_img: /img/case-studies/marketspark/marketspark-aws.png # Homepage highlight card (see docs/case-studies.md → Homepage slider) highlight: - eyebrow: "AWS · Infrastructure as Code" - title: "MarketSpark's cloud, rebuilt as a 100% IaC multi-account platform" - blurb: "Masterpoint rebuilt MarketSpark's single manually-operated AWS account into an 11-account AWS Organization: fully codified, automated, and ready to scale." + blurb: "Masterpoint rebuilt MarketSpark's single manually-operated AWS account into an 11-account AWS Organization: fully codified, automated, and scalable to business growth." image: /img/case-studies/marketspark/telecommunications.jpg image_alt: "City lights glowing across Earth at night, seen from orbit" logo: /img/case-studies/marketspark/marketspark-logo-dark.png - stats: - - value: "0% → 100%" - label: "Infrastructure as Code, automated end to end" - - value: "1 → 11" - label: "monolithic account to a full AWS Organization" sitemap: priority: 0 diff --git a/content/case-studies/power-digital.md b/content/case-studies/power-digital.md index 78ce4a94..ee87ae45 100644 --- a/content/case-studies/power-digital.md +++ b/content/case-studies/power-digital.md @@ -1,7 +1,7 @@ --- title: "Power Digital: 10x Reduction in Infrastructure Automation Costs, 8x Faster Client Onboarding" weight: 3 -description: "Masterpoint migrated Power Digital's 43,000-resource Terraform monolith from Terraform Cloud to Spacelift and OpenTofu: 10x lower projected automation costs, plan/apply cycles down from 25 minutes to under 3, failed runs eliminated, and 100+ new clients onboarded in the first 60 days." +description: "Masterpoint decomposed & migrated Power Digital's 43,000-resource Terraform monolith from Terraform Cloud to Spacelift and OpenTofu: 10x lower projected automation costs, plan/apply cycles down from 25 minutes to under 3, failed runs eliminated, and 100+ new clients onboarded in the first 60 days." layout: immersive @@ -31,17 +31,10 @@ og_img: /img/case-studies/power-digital/power-digital-case-study-preview.jpg # Homepage highlight card (see docs/case-studies.md → Homepage slider) highlight: - eyebrow: "IaC Automation · Cost Optimization" - title: "Power Digital cut automation costs 10x and onboards clients 8x faster" - blurb: "Masterpoint migrated Power Digital's 43,000-resource Terraform monolith to Spacelift and OpenTofu, and the platform onboarded 100+ new clients in the first 60 days." + blurb: "Masterpoint decomposed & migrated Power Digital's 43,000-resource Terraform monolith to Spacelift and OpenTofu, and the platform onboarded 100+ new clients in the first 60 days." image: /img/case-studies/power-digital/power-digital-team.jpg image_alt: "Power Digital team collaborating in their office" logo: /img/case-studies/power-digital/power-digital-logo.png - stats: - - value: "10x" - label: "lower infrastructure automation costs" - - value: "25 → 3 min" - label: "plan & apply deployment cycles" sitemap: priority: 0 diff --git a/content/sections/home-case-studies.md b/content/sections/home-case-studies.md index d808c405..32ac244a 100644 --- a/content/sections/home-case-studies.md +++ b/content/sections/home-case-studies.md @@ -11,7 +11,7 @@ id: case-study-highlights ### Real Engagements, Real Results {.groupTitle .text-center .text-rhythm} -

See how engineering teams partner with Masterpoint to turn tangled infrastructure into platforms that scale.

+

See case study success stories and learn how engineering teams partner with Masterpoint to turn tangled infrastructure into platforms that scale.

diff --git a/docs/case-studies.md b/docs/case-studies.md index df0bd530..6a9c9983 100644 --- a/docs/case-studies.md +++ b/docs/case-studies.md @@ -74,90 +74,41 @@ card grid to stay scannable as the list grows. Card image = each study's ## Homepage highlights slider -The homepage has a featured case-study slider (section -`content/sections/home-case-studies.md`, id `#case-study-highlights`, weight 5 — -between "Efficient Platforms" and the testimonials). It renders one full-width -dark pine card per case study (client logo on a white pill, category eyebrow, -short title, blurb, **two headline metrics**, CTA, photo) on a sliding track, -navigated by a client-logo tab strip — the Microsoft-customer-stories pattern. - -- **Markup + JS:** `layouts/shortcodes/case-study-slider.html` (self-contained - vanilla JS; no jQuery). It iterates `where site.RegularPages "Section" - "case-studies"` **ByWeight**, so case-study `weight:` also orders the slides. -- **Styles:** `.csh-*` block in `assets/css/custom.scss` (directly above the - `case-studies.scss` import). Scoped to `.csh-`; literal hex colors since the - `$cs-*` vars aren't defined at that point in the cascade. -- **Content comes from an optional `highlight:` front-matter map** on each case - study (see marketspark.md / power-digital.md): +Featured case-study slider on the homepage (section +`content/sections/home-case-studies.md`, id `#case-study-highlights`, weight 5). +One dark pine card per study (logo pill, title, blurb, CTA, photo) on a sliding +track navigated by a client-logo tab strip. + +- **Files:** `layouts/shortcodes/case-study-slider.html` (self-contained vanilla + JS, no jQuery — NOT flexslider, which `plugins.js` would hijack) + the `.csh-*` + block in `assets/css/custom.scss` (literal hex; `$cs-*` vars aren't defined + there yet). Iterates case studies **ByWeight**, so `weight:` orders the slides. +- **Content: optional `highlight:` front-matter map** (see marketspark.md / + power-digital.md), all fields optional with fallbacks: ```yaml highlight: - eyebrow: "AWS · Infrastructure as Code" # category label, mint uppercase - title: "... ..." # short card title (HTML ok) - blurb: "One or two sentences." - image: /img/case-studies/CLIENT/photo.jpg # card photo (right column) + title: "... ..." # → hero_title → page title + blurb: "..." # → description + image: /img/case-studies/CLIENT/photo.jpg # → preview_image image_alt: "..." - logo: /img/case-studies/CLIENT/CLIENT-logo-dark.png # DARK logo variant — used on the white tab strip AND the white card pill - stats: # exactly two headline metrics - - value: "10x" - label: "lower infrastructure automation costs" - - value: "25 → 3 min" - label: "plan & apply deployment cycles" + logo: /img/case-studies/CLIENT/CLIENT-logo-dark.png # tab strip only (white bg) → DARK variant; falls back to client_logo ``` - Every field falls back sensibly if omitted: `title` → page title, `blurb` → - `description`, `image` → `preview_image`, `stats` → first 2 `stat_bar` - entries, `logo` → `client_logo`, `eyebrow` → `Case Study · {client}`. - Set `highlight: false` to keep a case study out of the slider entirely. Any - other **non-map** value (`highlight: true`, a string, …) includes the study - with pure fallbacks — the template only reads fields after a `reflect.IsMap` - check, so a scalar can't crash the build (`$h.foo` on a bool is a fatal - template error; `| default dict` does NOT replace `true`). -- **Use the dark logo variant** for `highlight.logo` — it sits on white - surfaces (tab strip + card pill). The hero `client_logo` may be a white - variant (Power Digital), which would vanish there; that's why the field - exists. A study with no logo at all gets its client name as a text tab - (`.csh-nav__name`). -- **Slider mechanics:** custom vanilla JS in the shortcode, NOT flexslider/owl — - plugins.js initializes ALL `.flexslider` elements globally with dot controls, - which would fight the logo-tab navigation. Autoplay is driven by the active - tab's progress-bar CSS animation (8s, `cshProgress` keyframes): JS advances on - `animationend`, so pausing the animation pauses the slider exactly in sync - with the bar. The bar is a full-width gradient revealed via `clip-path` (a - width animation would stretch the colors). Touch swipe + roving-tabindex - arrow keys included; off-screen slides are `visibility: hidden` (delayed - until the track transition ends) with their links untabbable. -- **Autoplay gating — four independent conditions, all in CSS classes:** - - `.csh--inview` (IntersectionObserver): on once ≥25% visible, off only when - fully off-screen. Gate on `intersectionRatio`, not bare `isIntersecting` — - that flips true at the first visible pixel regardless of `threshold`. - - `.csh--paused` (transient): mouse hover OR keyboard focus, tracked as two - separate booleans — `focusout` only clears it if focus actually left the - root (`relatedTarget` check), so mouse-leave can't resume rotation while - focus is still inside (an auto-advance would hide the focused element). - Hover uses `pointerenter`/`pointerleave` gated to `pointerType === "mouse"` - — a tap's synthetic `mouseenter` never gets a matching `mouseleave` on - touch screens and would freeze rotation forever. - - `.csh--stopped` (persistent): the pause/play toggle button before the tab - strip — the WCAG 2.2.2 / APG-required rotation control, first in the - carousel's tab order, `aria-pressed` + swapped `aria-label`. - - `prefers-reduced-motion: reduce` disables the animation outright → no - `animationend` → no autoplay (manual navigation still works). -- **Specificity gotcha:** the `animation:` shorthand resets - `animation-play-state` to `running`, so the paused/stopped rules must MATCH - the run rule's 5-class specificity and come after it. A shorter - `.csh.csh--paused .csh-nav__progress` rule silently loses — hover-pause - then never works. -- **Single-slide degradation:** with one highlighted study the tab strip, - toggle, and autoplay are omitted and the slide renders WITHOUT - `role="tabpanel"`/`aria-labelledby` (they'd reference a tab that doesn't - exist — dangling ARIA idrefs fail axe). -- **Tab strip centering:** `justify-content: center` lives on the OUTER - `.csh-nav` row; the inner `.csh-nav__tabs` group is the `overflow-x: auto` - scroller. Centering the scroll container itself would clip its left end once - tabs overflow. -- The section heading/intro and the "See All Case Studies" button live in the - section content file, not the shortcode. + Two logo surfaces, two variants: the **card** (dark pine) uses the WHITE + `client_logo` — same lockup as the hero / sticky nav, no pill — overridable + with `highlight.card_logo`; the **tab strip** (white bg) uses the DARK + `highlight.logo`. `highlight: false` excludes a study; any other non-map value + includes it with pure fallbacks (template guards with `reflect.IsMap` so a + scalar can't crash the build). No logo → client name renders as a text tab. +- **Autoplay** is driven by the active tab's progress-bar CSS animation (advances + on `animationend`). Gated off by four independent CSS classes: `.csh--inview` + (≥25% visible), `.csh--paused` (hover/focus), `.csh--stopped` (WCAG pause/play + toggle), and `prefers-reduced-motion`. Pause/stop rules must match the run + rule's 5-class specificity or they silently lose. +- **Single study** → tab strip, toggle, autoplay, and tabpanel ARIA all omitted. +- Section heading/intro and "See All Case Studies" button live in the section + content file, not the shortcode. --- diff --git a/layouts/shortcodes/case-study-slider.html b/layouts/shortcodes/case-study-slider.html index 294c77c9..c9fec8c1 100644 --- a/layouts/shortcodes/case-study-slider.html +++ b/layouts/shortcodes/case-study-slider.html @@ -1,9 +1,9 @@ {{- /* Homepage case-study highlights slider (see docs/case-studies.md → Homepage slider). - One featured card per case study (logo pill, eyebrow, title, blurb, two stats, - CTA, photo) sliding in a track, with a client-logo tab strip as the navigation. + One featured card per case study (logo pill, title, blurb, CTA, photo) + sliding in a track, with a client-logo tab strip as the navigation. Card content comes from each case study's optional `highlight:` front matter, - falling back to title / description / preview_image / stat_bar / client_logo. + falling back to title / description / preview_image / client_logo. Set `highlight: false` on a case study to keep it out of the slider; any other non-map value (e.g. `highlight: true`) includes it with pure fallbacks. */ -}} @@ -19,7 +19,6 @@ {{- $h := dict -}} {{- if reflect.IsMap $p.Params.highlight -}}{{- $h = $p.Params.highlight -}}{{- end -}} {{- $client := $p.Params.client | default $p.Title -}} - {{- $stats := $h.stats | default (first 2 ($p.Params.stat_bar | default slice)) -}}
- {{- with $h.logo | default $p.Params.client_logo }} -