diff --git a/.prettierignore b/.prettierignore index 8e97de80..3d3da4ee 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,5 +5,6 @@ layouts/shortcodes/* layouts/_default/_markup/render-heading.html layouts/case-studies/immersive.html +layouts/case-studies/single.html layouts/case-studies/_markup/render-link.html layouts/partials/case-study-entry.html diff --git a/TODO.md b/TODO.md index a472f03f..1b70b310 100644 --- a/TODO.md +++ b/TODO.md @@ -7,3 +7,4 @@ - [ ] Revamp the index listing page of Case Studies (https://masterpoint.io/case-studies/) once we get more case studies. - [ ] OpenTofu Exclude Flag Blog Post: cross link to Cursor case study as a way to see where we took use of this feature or something like that, at the bottom callout section. - [ ] OpenTofu Exclude Flag -- link to a dedicated OpenTofu OpenTelemetry blog araticle +- [ ] Add "drift" in TF terminology breakdown blog post. diff --git a/assets/css/case-studies.scss b/assets/css/case-studies.scss index 31b64f27..2963e263 100644 --- a/assets/css/case-studies.scss +++ b/assets/css/case-studies.scss @@ -104,6 +104,33 @@ $cs-mint-faint: rgba(42, 217, 194, 0.06); } } + // Opt-in via front matter `hero_photo_inset: true` (currently only Cursor): + // the photo sits in the RIGHT 75% with a plain-pine inseam on the left and a + // LEFT-biased crop, replacing the default full-bleed cover + scrim. Scoped + // to the modifier class so other case-study heroes are unaffected. + &--photo-inset { + .cs-hero__bg { + &::after { + display: none; // no default scrim; the mask does the blending + } + img { + position: absolute; // anchor to the RIGHT so the inseam is on the left + top: 0; + right: 0; + width: 75%; // photo fills the right 75% (25% plain-pine inseam) + object-position: 25% center; // favor the LEFT of the photo + mask-image: + linear-gradient(90deg, transparent 0%, #000 20%), + linear-gradient(180deg, #000 0%, #000 90%, transparent 100%); + -webkit-mask-image: + linear-gradient(90deg, transparent 0%, #000 20%), + linear-gradient(180deg, #000 0%, #000 90%, transparent 100%); + mask-composite: intersect; + -webkit-mask-composite: source-in; + } + } + } + // decorative element: soft mint-glow + faint dotted grid (suppressed when bg image present) &::before { content: ""; @@ -149,7 +176,7 @@ $cs-mint-faint: rgba(42, 217, 194, 0.06); // Frosted card so title stays readable over the hero background image. &__inner--framed { padding: 1.8rem 2rem 1.6rem; - max-width: 720px; + max-width: 800px; background: linear-gradient(135deg, rgba(6, 29, 30, 0.72) 0%, rgba(14, 56, 58, 0.62) 100%); backdrop-filter: blur(8px) saturate(120%); -webkit-backdrop-filter: blur(8px) saturate(120%); @@ -429,6 +456,7 @@ $cs-mint-faint: rgba(42, 217, 194, 0.06); font-weight: 900; @include cs-brand-gradient-text; letter-spacing: -0.01em; + width: fit-content; // Keep the value comfortably below the hero title at every breakpoint // (title is 2.45rem ≤991 / 1.9rem ≤575), so it never visually outweighs @@ -486,20 +514,20 @@ $cs-mint-faint: rgba(42, 217, 194, 0.06); &__lockup { display: flex; align-items: center; - gap: 0.6rem; + gap: 0.7rem; flex-shrink: 0; } &__client { display: block; // scale from the page's hero logo height so wide lockups (MarketSpark // 48px) and compact ones (Power Digital 34px) both read balanced - height: calc(var(--cs-client-logo-h, 36px) * 0.6); + height: calc(var(--cs-client-logo-h, 36px) * 0.72); width: auto; - max-width: 170px; + max-width: 200px; } &__mp { display: block; - height: 15px; + height: 18px; width: auto; } &__x { @@ -545,18 +573,19 @@ $cs-mint-faint: rgba(42, 217, 194, 0.06); } } } + // the section links get cut off once the (enlarged) logo lockup crowds the + // bar — drop them below the lg breakpoint and let the lockup carry it alone + @media (max-width: 991.98px) { + &__links { + display: none; + } + } @media (max-width: 575px) { &__client { - height: calc(var(--cs-client-logo-h, 36px) * 0.48); + height: calc(var(--cs-client-logo-h, 36px) * 0.68); } &__mp { - height: 12px; - } - &__links { - gap: 1rem; - a { - font-size: 0.68rem; - } + height: 18px; } } } @@ -1136,7 +1165,6 @@ $cs-mint-faint: rgba(42, 217, 194, 0.06); &__body { font-size: 1.45rem; - font-weight: 600; line-height: 1.45; color: #fff; p { @@ -1164,6 +1192,7 @@ $cs-mint-faint: rgba(42, 217, 194, 0.06); letter-spacing: normal; } &__avatar img { + margin: 0; width: 88px; height: 88px; border-radius: 50%; @@ -1187,6 +1216,11 @@ $cs-mint-faint: rgba(42, 217, 194, 0.06); color: $cs-mint; padding-top: 0.35rem; } + &:not(.cs-pullquote--portrait) { + .cs-pullquote__role { + padding-top: 0; + } + } // -- LIGHT variant -- editorial cream/mint card; pine text. Good for an // end-of-article testimonial where it sits next to a dark CTA card. @@ -1210,6 +1244,44 @@ $cs-mint-faint: rgba(42, 217, 194, 0.06); text-transform: uppercase; font-size: 0.78rem; } + .cs-pullquote__name { + color: $pine; + } + } + + // -- MOBILE -- on a phone the 4rem left gutter + side-by-side attribution + // is too cramped (the role wraps awkwardly next to the name and the big + // body doesn't fit). Drop the quote mark into the text flow so the card + // can use its full width, shrink the body a touch, and stack the name + // above the role. Portrait quotes own their own mobile layout (grid + + // avatar), so leave them alone. + @media (max-width: 640px) { + padding: 1.2rem 1.4rem 1.5rem; + + .cs-pullquote__body, + .cs-pullquote__body p { + font-size: 1.2rem; + line-height: 1.45; + } + + &:not(.cs-pullquote--portrait) { + .cs-pullquote__mark { + position: static; + display: block; + margin: 0 0 -0.35rem; + font-size: 2.6rem; + line-height: 0.7; + } + .cs-pullquote__by { + flex-direction: column; + align-items: flex-start; + gap: 0.1rem; + margin-top: 0.9rem; + } + .cs-pullquote__role { + padding-top: 0.15rem; + } + } } } @@ -1466,7 +1538,7 @@ html:has(.cs-sticky-nav) { margin: 0 auto; padding: 0 2.6rem; @media (max-width: 575px) { - padding: 0 1.4rem; + padding: 0 1rem; } } @@ -1580,6 +1652,15 @@ html:has(.cs-sticky-nav) { } } + &--left { + .csi-section__head, + .csi-prose { + max-width: 880px; + margin-left: auto; + margin-right: auto; + } + } + // ---- COMPACT band — applied automatically to any section containing a // csi-list or csi-questions (no opt-in arg): same content, less // vertical real estate. Tighter padding, smaller head, denser prose @@ -1656,6 +1737,7 @@ html:has(.cs-sticky-nav) { } .cs-pullquote__avatar img { display: block; + margin: 0; width: 116px; height: 116px; border-radius: 50%; @@ -1767,6 +1849,28 @@ html:has(.cs-sticky-nav) { color: $csi-ink; font-weight: 800; } + // in-card subheads — the internal chapters of a consolidated single-card + // section (Results, What Changed), so they carry real structural weight + h3, + h4 { + font-size: 1.45rem; + font-weight: 800; + line-height: 1.3; + color: $csi-ink; + margin: 2.8rem 0 1rem; + } + h4 { + font-size: 1.08rem; + margin-top: 2rem; + } + // the heading render hook wraps heading text in an anchor — keep it on + // the heading colour, not the prose link treatment + h3 a:not(.button), + h4 a:not(.button) { + color: inherit; + font-weight: inherit; + background-image: none; + } // `:not(.csi-list):not(.csi-compare__rows)` — those uls carry their own // grid/ledger layout; keep the prose bullet/spacing treatment off them ul:not(.csi-list):not(.csi-compare__rows), @@ -1819,6 +1923,36 @@ html:has(.cs-sticky-nav) { margin-top: 0.7rem; margin-bottom: 0; } + // inline chart images (consolidated single-card sections place charts in + // the prose flow instead of a csi-split media column) + img { + display: block; + width: 100%; + height: auto; + border-radius: 16px; + margin: 1.9rem auto; + box-shadow: 0 18px 40px -30px rgba(8, 30, 30, 0.45); + } + } + // hairline edge so pine-on-pine chart exports keep a visible card boundary + .csi-section--pine .csi-prose img, + .csi-section--pine-deep .csi-prose img { + border: 1px solid rgba(255, 255, 255, 0.09); + } + // prose subheads flip to white on the dark faces (literal selectors — the + // headings set an explicit ink colour, so inheritance can't recolour them). + // The anchor variants out-specify the dark-face prose-link recolour, which + // would otherwise paint hook-wrapped heading text mint. + .csi-section--pine .csi-prose h3, + .csi-section--pine .csi-prose h4, + .csi-section--pine-deep .csi-prose h3, + .csi-section--pine-deep .csi-prose h4, + .csi-section--pine .csi-prose h3 a:not(.button), + .csi-section--pine .csi-prose h4 a:not(.button), + .csi-section--pine-deep .csi-prose h3 a:not(.button), + .csi-section--pine-deep .csi-prose h4 a:not(.button) { + color: #fff; + background-image: none; } // --------------------------------------------------------------------------- @@ -2169,6 +2303,323 @@ html:has(.cs-sticky-nav) { } } + // --------------------------------------------------------------------------- + // CAROUSEL — scroll-snap slideshow of result charts (csi-carousel). Slides + // are deliberately narrower than the viewport so the next chart PEEKS in + // from the right — the peek (plus the edge fade, arrows and dots) is what + // tells the reader there's more to scroll. Scrolling itself is native CSS + // scroll-snap; the shortcode's small script only syncs the arrows, dots and + // the data-at-start / data-at-end attributes that drive the edge fades. + // Designed for the LIGHT face (white slide cards on the light band). + // --------------------------------------------------------------------------- + .csi-carousel { + margin-top: 1.9rem; + &__viewport { + position: relative; + } + &__scroller { + display: flex; + gap: 1.4rem; + overflow-x: auto; + scroll-snap-type: x mandatory; + scroll-behavior: smooth; + // vertical breathing room so card shadows / the focus ring aren't + // clipped; the side padding is half the leftover viewport so EVERY + // slide (first and last included) can snap to the exact centre. + // Slides are flex-basis 100% of the CONTENT box (width minus this + // padding), so 13% side padding ⇒ 74%-wide slides — padding % and + // flex-basis % resolve against different boxes, don't mix the bases. + padding: 0.25rem 13% 0.6rem; + // mobile: a stronger peek — 7% gutters minus the 1.4rem gap left a ~5px + // sliver of the next chart, which read as "nothing more here". Wider + // gutters + a tighter gap keep ~25px of the neighbour visible. + @media (max-width: 700px) { + gap: 0.7rem; + padding-inline: 11%; // ⇒ 78%-wide slides + } + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } + &:focus-visible { + outline: 2px solid $cs-mint; + outline-offset: 4px; + border-radius: 8px; + } + @media (prefers-reduced-motion: reduce) { + scroll-behavior: auto; + } + } + &__slide { + // 100% of the scroller's padded content box — the scroller's side + // padding (above) is what sets the real slide width. NO card chrome + // here: the chart exports are already self-contained white cards + // (own margins, header, logos), so a wrapper card double-boxes them. + // The image carries the same radius + shadow as `.csi-prose img`, so + // the carousel charts match the ones in The Results sections. + flex: 0 0 100%; + scroll-snap-align: center; + margin: 0; + } + &__zoom { + display: block; + width: 100%; + margin: 0; + padding: 0; + border: 0; + background: none; + border-radius: 16px; + cursor: zoom-in; + &:focus-visible { + outline: 2px solid $cs-mint; + outline-offset: 4px; + } + } + &__media { + display: block; + width: 100%; + aspect-ratio: 16 / 9; + object-fit: cover; + border-radius: 16px; + box-shadow: 0 18px 40px -30px rgba(8, 30, 30, 0.45); + transition: box-shadow 0.25s ease; + } + // literal nested selectors (3 classes) — the broad `.csi-prose img` rule + // (2 classes + element) would otherwise out-specify `&__media` and leak + // its 1.9rem margins into the slides + .csi-carousel__zoom .csi-carousel__media { + margin: 0; + } + .csi-carousel__zoom:hover .csi-carousel__media, + .csi-carousel__zoom:focus-visible .csi-carousel__media { + box-shadow: 0 22px 48px -28px rgba(8, 30, 30, 0.6); + } + // LIGHTBOX — native , opened by clicking a slide; Esc, the × + // button, and a backdrop click all close it + &__lightbox { + padding: 0; + border: 0; + background: transparent; + overflow: visible; + &::backdrop { + background: rgba(6, 29, 30, 0.88); + } + } + .csi-carousel__lightbox-img { + display: block; + margin: 0; + width: auto; + height: auto; + max-width: 92vw; + max-height: 86vh; + border-radius: 14px; + box-shadow: 0 40px 90px -40px rgba(0, 0, 0, 0.7); + } + &__lightbox-close { + position: absolute; + top: 0.7rem; + right: 0.7rem; + z-index: 1; + width: 40px; + height: 40px; + border-radius: 50%; + border: 1px solid $csi-line; + background: #fff; + color: $csi-ink; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.85rem; + cursor: pointer; + box-shadow: 0 12px 26px -16px rgba(0, 0, 0, 0.6); + transition: + background 0.2s ease, + color 0.2s ease, + border-color 0.2s ease; + &:hover { + @include csi-grad-solid; + color: $cs-pine-dark; + border-color: transparent; + } + &:focus-visible { + outline: 2px solid $cs-mint; + outline-offset: 3px; + } + } + &__caption { + display: flex; + align-items: center; + gap: 1.15rem; + padding: 1.1rem 1.4rem 1.2rem; + text-align: left; + @media (max-width: 575px) { + gap: 0.85rem; + padding: 0.9rem 1rem 1rem; + } + } + &__stat { + flex: 0 0 auto; + font-size: 2.1rem; + font-weight: 900; + letter-spacing: -0.02em; + line-height: 1; + @media (max-width: 575px) { + font-size: 1.6rem; + } + } + &__text { + font-size: 0.92rem; + line-height: 1.5; + color: $csi-ink-soft; + strong { + color: $csi-ink; + font-weight: 800; + } + a { + color: $csi-link; + font-weight: 600; + } + @media (max-width: 575px) { + font-size: 0.86rem; + } + } + // edge fades — the soft clip that says "the track continues"; each fades + // away (via data-at-start / data-at-end below) once its side is exhausted + &__viewport::before, + &__viewport::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + width: 76px; + z-index: 2; + pointer-events: none; + transition: opacity 0.25s ease; + // keep the mobile fade narrower than the peek sliver, or it whites the + // neighbouring card out entirely + @media (max-width: 700px) { + width: 20px; + } + } + &__viewport::before { + left: 0; + background: linear-gradient(90deg, rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0)); + } + &__viewport::after { + right: 0; + background: linear-gradient(270deg, rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0)); + } + &__arrow { + position: absolute; + top: 50%; + transform: translateY(-50%); + z-index: 3; + width: 44px; + height: 44px; + border-radius: 50%; + border: 1px solid $csi-line; + background: #fff; + color: $csi-ink; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; + cursor: pointer; + box-shadow: 0 12px 26px -16px rgba(8, 30, 30, 0.5); + transition: + background 0.2s ease, + color 0.2s ease, + border-color 0.2s ease, + opacity 0.25s ease; + &:hover:not(:disabled) { + @include csi-grad-solid; + color: $cs-pine-dark; + border-color: transparent; + } + &:focus-visible { + outline: 2px solid $cs-mint; + outline-offset: 3px; + } + &:disabled { + opacity: 0; + pointer-events: none; + } + &--prev { + left: -8px; + } + &--next { + right: -8px; + } + @media (max-width: 700px) { + display: none; // swipe is the native gesture; peek + dots still cue "more" + } + } + &__dots { + display: flex; + justify-content: center; + gap: 0.5rem; + margin-top: 1.15rem; + } + &__dot { + width: 8px; + height: 8px; + border-radius: 999px; + border: 0; + padding: 0; + background: rgba(20, 48, 47, 0.18); + cursor: pointer; + transition: + width 0.25s ease, + background 0.25s ease; + &:focus-visible { + outline: 2px solid $cs-mint; + outline-offset: 3px; + } + } + .csi-carousel__dot.is-active { + width: 26px; + @include csi-grad-solid; + } + // mobile swipe hint — arrows are hidden ≤700px and the peek is narrow on + // a phone, so spell the gesture out; fades once the reader swipes + // (data-swiped, set by the layout JS) + &__hint { + display: none; + align-items: center; + justify-content: center; + gap: 0.5rem; + margin-top: 0.7rem; + font-size: 0.72rem; + font-weight: 800; + letter-spacing: 0.14em; + text-transform: uppercase; + color: rgba(20, 48, 47, 0.5); + transition: opacity 0.35s ease; + i { + font-size: 0.72rem; + animation: csi-hint-nudge 1.8s ease-in-out infinite; + @media (prefers-reduced-motion: reduce) { + animation: none; + } + } + @media (max-width: 700px) { + display: flex; + } + } + } + .csi-carousel[data-swiped="true"] .csi-carousel__hint { + opacity: 0; + } + // literal selectors (not `&`-nested) — attribute state on the root toggles + // pseudo-elements of a descendant, and `&__` expansion inside nested rules + // produces broken ancestor chains (see the SCSS `&__foo` trap in the docs) + .csi-carousel[data-at-start="true"] .csi-carousel__viewport::before { + opacity: 0; + } + .csi-carousel[data-at-end="true"] .csi-carousel__viewport::after { + opacity: 0; + } + // --------------------------------------------------------------------------- // COMPARE — "then / now" migration ledger (csi-compare). Old world reads // muted on the left, new world bold on the right, gradient arrow between. @@ -2186,12 +2637,12 @@ html:has(.cs-sticky-nav) { &__head, &__row { display: grid; - grid-template-columns: minmax(0, 0.85fr) minmax(0, 1fr) 2.2rem minmax(0, 1fr); + grid-template-columns: minmax(0, 1fr) minmax(0, 0.6fr) 1.9rem minmax(0, 1.5fr); align-items: center; - column-gap: 0.7rem; + column-gap: 0.55rem; } &__head { - padding: 0.8rem 1.6rem; + padding: 0.85rem 1.75rem; background: rgba(20, 48, 47, 0.03); border-bottom: 1px solid $csi-line; } @@ -2202,6 +2653,7 @@ html:has(.cs-sticky-nav) { text-transform: uppercase; &--before { color: #8aa09f; + text-align: right; } &--after { @include csi-grad-light; @@ -2213,7 +2665,7 @@ html:has(.cs-sticky-nav) { padding: 0; } &__row { - padding: 1rem 1.6rem; + padding: 1.4rem 1.75rem; } &__metric { font-size: 0.95rem; @@ -2229,17 +2681,45 @@ html:has(.cs-sticky-nav) { } &__before { color: #7d9291; + text-align: right; } &__after { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.35rem 0.5rem; font-weight: 800; @include csi-grad-light; } + &__after--plain { + color: $csi-ink; + background: none; + -webkit-background-clip: border-box; + background-clip: border-box; + -webkit-text-fill-color: $csi-ink; + } &__arrow { text-align: center; font-size: 1.05rem; font-weight: 900; line-height: 1; } + &__delta { + display: inline-block; + margin: 0; + padding: 0.2rem 0.6rem; + border-radius: 999px; + font-size: 0.68rem; + font-weight: 800; + letter-spacing: 0.05em; + text-transform: uppercase; + line-height: 1.35; + @include csi-grad-solid; + -webkit-background-clip: border-box; + background-clip: border-box; + color: #06201f; + -webkit-text-fill-color: #06201f; + } // per-cell Before/After tags — mobile only (the header row hides there) &__tag { display: none; @@ -2271,6 +2751,7 @@ html:has(.cs-sticky-nav) { } .csi-compare__before { grid-area: before; + text-align: left; } .csi-compare__arrow { grid-area: arrow; @@ -2287,9 +2768,24 @@ html:has(.cs-sticky-nav) { margin-bottom: 0.15rem; color: #8aa09f; } + // __after is a flex row — force its mobile tag onto its own line above + // the value instead of sitting inline with it. .csi-compare__after .csi-compare__tag { + flex: 0 0 100%; color: $csi-link; } + // The delta pill sits in the same narrow flex cell as the value, so a long + // label ("45.8% reduction in duplication") wrapped into a left-ragged blob + // under the 999px radius. Give it its own full-width line, centre the text, + // and use a modest radius so multi-line reads as a tidy rounded chip. + .csi-compare__after .csi-compare__delta { + flex: 0 0 100%; + margin-top: 0.35rem; + padding: 0.3rem 0.7rem; + border-radius: 10px; + text-align: center; + line-height: 1.3; + } } // NOTE: the ledger stays a WHITE card on dark faces too (no pine override) // — the hard numbers read best on white, and the card pops off the band. @@ -2533,6 +3029,123 @@ html:has(.cs-sticky-nav) { } } + // --------------------------------------------------------------------------- + // PHASE CARD — the "What Masterpoint Did" engagement timeline: dated + // chapters as wide cards off a single gradient rail (ported verbatim from the + // PR 154 design so the boxes match it exactly). Nested csi-compare ledgers + // render normally inside the card body (which carries `.csi-prose`). + // --------------------------------------------------------------------------- + --csi-phase-rule: rgba(20, 48, 47, 0.14); + #the-work .csi-prose { + max-width: 1040px; + margin-left: auto; + margin-right: auto; + } + #the-work .csi-prose:not(.csi-phase__body) > p { + max-width: 880px; + margin-left: auto; + margin-right: auto; + } + .csi-phase { + display: grid; + grid-template-columns: 42px minmax(0, 1fr); + gap: 1.1rem; + position: relative; + max-width: 1040px; + margin: 2rem auto 0; + text-align: left; + } + .csi-phase + .csi-phase { + margin-top: 1.15rem; + } + .csi-phase__rail { + position: relative; + display: flex; + justify-content: center; + &::after { + content: ""; + position: absolute; + top: 16px; + bottom: -2.3rem; + width: 1px; + background: linear-gradient(180deg, rgba(11, 125, 114, 0.55), rgba(209, 145, 191, 0.3)); + } + } + .csi-phase:last-child .csi-phase__rail::after { + display: none; + } + .csi-phase__marker { + position: relative; + z-index: 1; + display: block; + width: 16px; + height: 16px; + margin-top: 1.55rem; + border: 4px solid #fff; + border-radius: 50%; + @include csi-grad-solid; + box-shadow: 0 0 0 1px rgba(11, 125, 114, 0.28); + } + .csi-phase__card { + padding: 1.55rem 1.65rem; + background: rgba(255, 255, 255, 0.82); + border: 1px solid var(--csi-phase-rule); + border-radius: 10px; + box-shadow: 0 18px 40px -32px rgba(7, 31, 30, 0.5); + } + // Scope to the card so this beats the generic `.csi-prose h3` rule + // (which lives at higher specificity and would otherwise force a 2.8rem + // top margin — the phase title is a descendant of the section's .csi-prose). + .csi-phase__card .csi-phase__title { + margin: 0 0 1rem; + color: $csi-ink; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: clamp(1.05rem, 1.8vw, 1.3rem); + font-weight: 800; + line-height: 1.4; + letter-spacing: -0.02em; + text-transform: none; + } + .csi-phase__body { + color: $csi-ink-soft; + > :last-child { + margin-bottom: 0; + } + } + // ledgers inside a phase need real vertical margin so the card's drop shadow + // clears the surrounding copy instead of falling on the text right below it + .csi-phase__body .csi-compare { + margin-top: 1.6rem; + margin-bottom: 1.6rem; + } + @media (max-width: 575px) { + // The rail column + gap ate ~33px of a phone's width, leaving the card body + // painfully narrow. On mobile give the card the FULL content width (drop the + // grid to a single flow column) and float the decorative rail out into the + // left gutter with absolute positioning, so the marker + line sit just left + // of the card without stealing any of its width. + .csi-phase { + display: block; + position: relative; + } + .csi-phase__rail { + position: absolute; + left: -0.9rem; + top: 0; + bottom: 0; + width: 14px; + } + .csi-phase__marker { + width: 13px; + height: 13px; + margin-top: 1.35rem; + border-width: 3px; + } + .csi-phase__card { + padding: 1rem 0.95rem; + } + } + // --------------------------------------------------------------------------- // QUESTIONS — takeaways row of numbered question cards (csi-questions). // Compact cards: gradient numeral inline with the question, the honest @@ -2984,6 +3597,19 @@ html:has(.cs-sticky-nav) { } // ─────────────────────────────────────────────────────────────────────────── +// csi-carousel mobile swipe-hint arrow nudge (root level — @keyframes must +// not be nested inside a rule) +@keyframes csi-hint-nudge { + 0%, + 55%, + 100% { + transform: translateX(0); + } + 25% { + transform: translateX(5px); + } +} + // PRINT / PDF — make the case study print exactly like the screen. // The site stylesheet now loads with media="all" (see partials/head.html), so // the styles apply when printing. Two extra things are needed for the result to @@ -3120,8 +3746,24 @@ html:has(.cs-sticky-nav) { .csi-fig-terralith__mono, .csi-fig-terralith__stack, .csi-timeline, - .csi-question { + .csi-question, + .csi-phase__card, + .csi-carousel__slide, + .csi-prose img { break-inside: avoid; } + + .csi-carousel__scroller { + display: block !important; + overflow: visible !important; + } + .csi-carousel__slide { + margin: 0 0 1rem !important; + } + .csi-carousel__arrow, + .csi-carousel__dots, + .csi-carousel__hint { + display: none !important; + } } } diff --git a/content/case-studies/cursor.md b/content/case-studies/cursor.md new file mode 100644 index 00000000..85552f03 --- /dev/null +++ b/content/case-studies/cursor.md @@ -0,0 +1,271 @@ +--- +title: "How Cursor Rebuilt Its Terraform Workflow with Masterpoint: 5x Faster Plans, 2x More Deployments" +weight: 1 +description: "5x Faster Plans, 2x More Deployments: At Masterpoint, we focused on Terraform and Infrastructure as Code (IaC) improvements so our client Cursor could focus on making their users extraordinarily productive." + +layout: immersive + +# Hero +eyebrow: "CASE STUDY SUCCESS STORY" +client: "Cursor" +client_logo: /img/case-studies/cursor/cursor-lockup-white.svg +client_logo_height: 40px +hero_title: "How Cursor Rebuilt Its Terraform Workflow with Masterpoint: 5x Faster Plans, 2x More Deployments" +hero_aside_image: /img/case-studies/cursor/cursor-team-photo.webp +hero_aside_alt: "A Cursor engineer leads a team discussion at a chalkboard sketched with background-agent architecture" +hero_photo_inset: true # photo sits in the right 75% (25% plain-pine inseam on the left), left-biased crop + +# At-a-glance stat strip (appears under hero) — headline % / x increase on top, +# the actual before → after numbers underneath. +stat_bar: + - value: "5x" + label: "Faster plan time
10 min → 2 min" + - value: "2.6x" + label: "More infrastructure PRs shipped
194 → 500+ per week" + - value: "91%" + label: "Reduction in blast radius
54 → 5 average cloud resources modified per deployment" + - value: "6x" + label: "More Terraform workspaces, now self-service" + +# Preview / OG +preview_image: /img/case-studies/cursor/cursor-masterpoint.png +og_img: /img/case-studies/cursor/cursor-masterpoint.png + +sitemap: + priority: 0 +--- + +{{< csi-section accent="true" title="Executive Summary" variant="light" align="center" >}} +[Cursor is an applied research AI lab](https://cursor.com/), with products used by [over half of the Fortune 500](https://cursor.com/enterprise). Cursor's AWS cloud infrastructure had outgrown the monolithic Terraform architecture it was built on: plans & deployments took 10+ minutes, diffs were noisy and unreadable, and engineers had stopped trusting the system. Masterpoint audited the infrastructure, decomposed the [terralith](https://masterpoint.io/blog/terralith-monolithic-terraform-architecture/), built an [Infrastructure as Code](https://aws.amazon.com/what-is/iac/) platform with a new scalable architecture, migrated to Spacelift & OpenTofu, and embedded AI agent guardrails so the new architecture sticks, along with a series of platform improvements detailed in the story below. Plans now run **5x faster**, engineers ship **2.6x more** infrastructure changes every week, and with a **91% reduction in blast radius**. + +{{< csi-carousel >}} +image: /img/case-studies/cursor/cursor-terraform-5x-faster-masterpoint.png +alt: Deploy time before and after: average plan + apply took 10+ minutes before, 2 minutes now — 5x faster per Terraform run +--- +image: /img/case-studies/cursor/cursor-2x-more-deploy-masterpoint.png +alt: 2.6x more PRs shipped: from 194 changes per week in October 2025 to 500+ in May 2026, a 157% jump in deployment frequency +--- +image: /img/case-studies/cursor/cursor-terraform-blast-radius-masterpoint.png +alt: 91% reduction in blast radius: a median Terraform run once touched 54 resources, now it averages just 5 +--- +image: /img/case-studies/cursor/cursor-infrastructure-democratized-masterpoint.png +alt: Terraform root module workspaces grew from 21 in October 2025 to 141 in May 2026 — up 571% — as self-service workspaces replaced monolithic antipatterns +{{< /csi-carousel >}} +{{< /csi-section >}} + +{{< csi-section id="the-challenge" eyebrow="The Problem" title="Infrastructure That Couldn't Keep Up" variant="pine" >}} +By late 2025, Cursor's [Infrastructure as Code (IaC)](https://aws.amazon.com/what-is/iac/) setup had become one of the biggest friction points in the engineering org. Cursor manages its [Amazon Web Services (AWS)](https://aws.amazon.com/) infrastructure entirely with [Terraform](https://www.terraform.io/), and that footprint had grown fast. Cursor is no longer just a code editor: the product now extends well beyond the IDE into cloud and background coding agents, automations, agentic workflows, and the training and serving of [Cursor's own frontier models, the Composer family](https://cursor.com/blog/topic/research). All of it runs on AWS at massive scale, serving engineering teams of every size, including [more than half of the Fortune 500](https://cursor.com/enterprise), among them Nvidia, Stripe, Salesforce, PayPal, and Wayfair. Keeping that infrastructure reliable and fast-moving is now core to the business. But like many fast-growing startups, the code behind it had been written quickly and then scaled by copy-paste rather than careful design. + + +Those early Terraform patterns were replicated at machine speed as engineers dogfooded Cursor. They were copied and instantiated hundreds of times without the architectural guardrails needed to keep the system manageable. The result was a classic [terralith](https://masterpoint.io/blog/terralith-monolithic-terraform-architecture/): critical infrastructure was managed as a single [monolithic production Terraform workspace](https://masterpoint.io/blog/terralith-monolithic-terraform-architecture/) with over 7,000 resources in one state file. + +And because that workspace was so large, it was too slow. + +{{< cs-pullquote name="Travis McPeak" title="Security Lead" company="Cursor" >}} +We had one giant workspace that took way too long to plan. It was killing us. +{{< /cs-pullquote >}} + +But the problem wasn't just the lack of speed. It was a lack of engineering confidence in the Infrastructure as Code system. Issues included: + +- The AWS console was used to [manually make changes](https://masterpoint.io/blog/terraform-opentofu-terminology-breakdown/#clickops) during incidents and these fixes were never rolled into Terraform, and so were later inadvertently reverted. +- Managing ECS deployments through Terraform caused constant [drift](https://spacelift.io/blog/drift-detection) in the production terralith, flooding PR diffs with hundreds of unrelated changes. + +Over time, the team stopped trusting the system, let alone reading the diffs. + +{{< cs-pullquote name="Travis McPeak" title="Security Lead" company="Cursor" >}} +We would have engineers click apply on prod workspaces that had 120 AWS ECS service changes. The ECS services would change all the time because of drift, so the team became desensitized to large Terraform plan changes. +{{< /cs-pullquote >}} + +This confusion caused downtime, such as a network firewall change that caused a 10-minute outage that was lost in the noise of Terraform drift. + +The existing platform, Terraform Cloud, introduced some friction too: + +- Workspace management relied on manual ClickOps configuration for each new workspace. +- The SSO experience was inconsistent. +- TFC's resource-under-management (RUM) pricing model made it daunting to scale. +{{< /csi-section >}} + +{{< csi-section id="the-work" eyebrow="The Engagement" title="What Masterpoint Did" variant="light" >}} +Masterpoint came in for a systematic overhaul, working across the architecture, platform, and developer workflows. + +{{< csi-phase title="November 2025 - Audit and High Leverage Fixes" >}} +The engagement opened with [Masterpoint's audit](https://masterpoint.io/services/audit/) of Cursor's infrastructure: + +- cloud environments +- Infrastructure as Code (IaC) & Terraform architecture +- CI/CD pipelines +- networking +- observability + +Within a week, Masterpoint delivered an audit report, a best practices checklist, and a security scan. The findings included IAM duplication accounted for 70% of the production Terraform state and noisy plan diffs caused engineer review fatigue. + +The audit phase targeted the highest-leverage fixes first: IAM resource deduplication, workspace trigger and path filter corrections so plans only ran when relevant code changed, and initial breaking up of the monolithic Terraform workspace to begin isolating Staging and Machine Learning infrastructure. + +Just one week later, Masterpoint had resolved the IAM duplication, with significant results: + +{{< csi-compare before_label="Before" after_label="After" >}} +label: Resource Count +before: 8,480 +after: 4,593 +delta: 45.8% reduction in duplication +--- +label: Mean plan time +before: 488s (8.1 min) +after: 296s (4.9 min) +delta: 39.3% faster +--- +label: P50 plan time (median) +before: 423s (7.1 min) +after: 272s (4.5 min) +delta: 35.7% faster +--- +label: P95 plan time +before: 749s (12.5 min) +after: 414s (6.9 min) +delta: 44.7% faster +{{< /csi-compare >}} + +This was accomplished without touching the workspace structure, and without any downtime. At the speed and scale Cursor operates, downtime simply isn't an option. Every improvement had to land safely while the platform kept growing underneath it: even as Masterpoint deduplicated and trimmed resources, Cursor was continuously adding new ones. There was still more the systems could improve on. +{{< /csi-phase >}} + +{{< csi-phase title="December 2025 - Spacelift Migration Begins" >}} +With the first phase complete, Masterpoint turned to the platform. + +As mentioned above, TFC's ClickOps workspace management, SSO troubles, and resource-under-management pricing were all inhibiting Cursor's velocity and growth. + +Masterpoint recommended a move to [Spacelift](https://spacelift.io/) to address these issues. + +The team kicked off the Spacelift migration in mid-December, starting with the groundwork: + +- naming and tagging standards +- AWS multi-account IAM role architecture +- Okta SSO configuration + +Rather than migrating everything at once, a low-risk pilot Spacelift stack (the equivalent of a TFC workspace) was used to validate the approach. The end-to-end fully automated pilot project was completed within days. + +Masterpoint also began evaluating a full migration from the Terraform runtime to [OpenTofu](https://opentofu.org/). +{{< /csi-phase >}} + +{{< csi-phase title="January to February 2026 - Bulk Migration & Decomposition of the Monolithic Terraform Workspace" >}} +With a plan covering a migration of dozens of TFC workspaces across four phases, the team worked systematically. By late January, the bulk of workspaces had been migrated to Spacelift stacks with no operational downtime for the Cursor engineering organization. + +By February, the migration was done. All workspaces had been moved from TFC to Spacelift and the runtime was converted from Terraform to OpenTofu. Migrating to OpenTofu freed Cursor from licensing constraints and unlocked capabilities unavailable or paywalled in Terraform, such as state encryption, OpenTelemetry support, and provider iteration. + +In parallel, Masterpoint also continued to decompose the Terraform monolith into more product and domain scoped root modules. +{{< /csi-phase >}} + +{{< csi-phase title="February to May 2026 - AI Agent Guardrails And Additional Improvements" >}} +After the migration was completed, Masterpoint continued to improved system usability and speed by: + +- restructuring Route53 DNS API requests into optimized, dedicated Terraform modules to address AWS' hardcap of 5 requests/second (further reducing critical infrastructure plan time from roughly 5 minutes to under 2 minutes) +- continuing to break up large state files into narrower root modules, allowing for faster plans and decreased blast radius +- implementing child module versioning with [OCI registries](https://opentofu.org/docs/cli/oci_registries/module-package/) to enable staged rollouts and safer change control of critical TF resources +- resolving the ECS drift issue by using the ["Task Definition Template Pattern"](https://newsletter.masterpoint.io/p/deploying-your-apps-into-ecs) +- implementing OpenTofu's [OTel tracing](https://opentofu.org/docs/internals/tracing/) to find other performance bottlenecks and set up the Cursor team for longterm visibility into their IaC throughput + +Because Cursor's engineering team uses Cursor to write Terraform, Masterpoint embedded architectural knowledge directly into the codebase as AI agent skills and rules. + +This AI context encodes the decisions that AI models cannot anticipate on their own: + +- when to create a new root module vs. extend an existing one +- how to scope blast radius in a root module +- how to break up a high blast radius monolithic root module +- data source patterns and optimizations +- security practices, such as cloud networking and Terraform/OpenTofu module SHA pinning +- debugging performance bottlenecks, through methods such as logging and OpenTelemetry traces +- …and more + +Every IaC optimization Masterpoint delivered was paired with instructions to prevent anti-patterns from being reintroduced. + +{{< cs-pullquote name="Ravi Rahman" title="Software Engineer" company="Cursor" >}} +The AI agent skills and rules help us build our IaC correctly while at the same time moving faster. +{{< /cs-pullquote >}} +{{< /csi-phase >}} +{{< /csi-section >}} + +{{< csi-section id="the-results" eyebrow="The Results" title="The Results" variant="pine" >}} +### Numbers That Matter + +{{< csi-compare before_label="Before" after_label="After" >}} +label: Plan time (production) +before: 10 min +after: 2 min +delta: 5x faster +--- +label: Infrastructure PRs shipped per week +before: 194 +after: 500+ +delta: 2.6x more frequent, up +157%! +--- +label: Median resources affected per Terraform run +before: 54 +after: 5 +delta: 91% reduction in blast radius +{{< /csi-compare >}} + +### Plan time went from 10+ minutes to 2 minutes + +The production workspace plan time dropped from over 10+ minutes, with spikes to 15 minutes, during the November baseline to around 2 minutes by mid-2026. + +![Deploy time before and after: average plan + apply took 10+ minutes before, 2 minutes now — 5x faster per Terraform run](/img/case-studies/cursor/cursor-terraform-5x-faster-masterpoint.png) + +### Deployment frequency increased + +Weekly merged PRs against infrastructure as code went from approximately 194 per week to over 500 a week, a **157% increase**. They've only accelerated since then. + +![2.6x more PRs shipped: from 194 changes per week in October 2025 to 500+ in May 2026, a 157% jump in deployment frequency](/img/case-studies/cursor/cursor-2x-more-deploy-masterpoint.png) + +### The productivity lift was significant + +This gain wasn't from growing the engineering organization in headcount: a **controlled same-cohort analysis of 34 engineers** active both before and after the Masterpoint engagement found that PR throughput grew by **121%**. + +{{< csi-compare before_label="Pre-engagement" after_label="End of Engagement" >}} +label: Cohort throughput +before: 63 PRs/wk +after: 140 PRs/wk +delta: +121% +--- +label: Per-active-week cadence +before: 5.37 PRs/author/wk +after: 9.19 PRs/author/wk +delta: +71% +{{< /csi-compare >}} + +### Infrastructure became democratized + +Previously, workspace creation required manual ClickOps and admin access. Today, engineers create and manage their own stacks. The number of separately managed modules went from **21 to 100+**. Engineers are now creating their own product/domain-scoped infrastructure, with guardrails, instead of jumbling it in the primary monolithic workspace. + +![Terraform root module workspaces grew from 21 in October 2025 to 141 in May 2026 — up 571% — as self-service workspaces replaced monolithic antipatterns](/img/case-studies/cursor/cursor-infrastructure-democratized-masterpoint.png) +{{< /csi-section >}} + +{{< csi-section eyebrow="The Results" title="What Changed for the Team" variant="light" >}} +{{< cs-pullquote name="Ravi Rahman" title="Software Engineer" company="Cursor" >}} +The Terraform experience has gotten way better. For the first time since I've been here, we just merge and apply. If something's wrong, the system lets us know. It's fast and moves at the speed we need. +{{< /cs-pullquote >}} + +### Engineers know what an IaC Terraform change will touch + +Before the engagement, engineers couldn't predict what a change would actually touch in a workspace. A database might live in a workspace named for networking infrastructure, so even finding where something was managed required tribal knowledge. Dependencies between workspaces were just as opaque: touching one piece of infrastructure could ripple into systems that had nothing to do with the change or surface long-standing unrelated drift. Plans intended for one system would silently queue in unrelated workspaces, waiting for the next person to find them with no context on where they came from or whether they were safe to apply. + +With smaller, focused stacks, plans are scoped to the change at hand. The median Terraform run now touches just 5 resources instead of 54 — **a 91% smaller blast radius** — so engineers know precisely what a plan will affect before they run it. + +![91% reduction in blast radius: a median Terraform run once touched 54 resources, now it averages just 5](/img/case-studies/cursor/cursor-terraform-blast-radius-masterpoint.png) + +### Architectural flexibility enables the business and product delivery + +Before the engagement, the terralith made architectural changes expensive and risky. Touching one piece of infrastructure meant disrupting unconnected components. + +Now, with narrow stacks each owning a focused piece of infrastructure, the Cursor engineering team moves faster: plan and apply deployments complete faster, reviews are lighter & quicker, and a change to one system carries no risk of breaking another. + +New services, providers, and cloud targets can be added as their own stacks, which is a foundation that directly enables expansion into multi-cloud infrastructure as Cursor continues to scale. + +### Cursor's agents are set up for future success + +Since Cursor's engineers rely on Cursor to write their own Terraform, Masterpoint embedded the new architecture into AI agent rules and skills that sit directly within the codebase. + +These rules capture the patterns and decisions that LLMs can't figure out on their own, setting the team up to **consistently ship high-quality infrastructure code**, now and as the system evolves. +{{< /csi-section >}} + +{{< csi-testimonial name="Travis McPeak" title="Security Lead" company="Cursor" variant="pine" photo="/img/testimonials/travis-mcpeak.jpeg" image="/img/bg_our_word.jpg" >}} +Masterpoint didn't just fix our Infrastructure as Code. They gave our entire engineering org the **confidence to move fast**. We went from a system people were afraid to touch Terraform to where nearly every engineer is quickly and assuredly shipping infrastructure changes every week. +{{< /csi-testimonial >}} diff --git a/content/testimonials/travis-mcpeak.md b/content/testimonials/travis-mcpeak.md index 20bb20ea..a37f1fb0 100644 --- a/content/testimonials/travis-mcpeak.md +++ b/content/testimonials/travis-mcpeak.md @@ -6,7 +6,7 @@ company: Cursor logo: /img/logos/cursor-25d-dark-mode.svg logo_full_color: true logo_height: 44 # requires an override bceause the logo dimensions (by default) is too small -# case_study: TODO! +case_study: /case-studies/cursor/ --- "Masterpoint didn't just fix our Infrastructure as Code. They gave our entire engineering org the confidence to move fast. We went from a system people were afraid to touch Terraform to where nearly every engineer is quickly and assuredly shipping infrastructure changes every week." diff --git a/docs/case-studies.md b/docs/case-studies.md index 54402cc6..7a6bee88 100644 --- a/docs/case-studies.md +++ b/docs/case-studies.md @@ -2,12 +2,12 @@ # Case Studies — working guide -This document captures the architecture, conventions, and workflow for building -case study pages on masterpoint.io. **Keep it current** — every time a new -decision is made about layout, shortcodes, visuals, or workflow, update this -file before ending the session. If something is removed from the codebase, -remove its mention from this file too (don't keep "we used to have X" notes — -the codebase is the source of truth, this file describes what IS). +Architecture, conventions, and workflow for building case study pages on +masterpoint.io. **Keep it current and concise** — update it when a layout, +shortcode, visual, or workflow decision changes, and delete mentions of anything +removed from the codebase. Document only what isn't obvious from the code; the +code is the source of truth, this file describes what IS (no "we used to have X" +notes). --- @@ -26,25 +26,20 @@ July 2026 once Power Digital, its last user, was rebuilt on immersive.) | **Modern** | `layouts/case-studies/single.html` | Default for any case study without a `layout:` override | `case-study-modern` | `.case-study-modern` | | **Immersive** | `layouts/case-studies/immersive.html` | MarketSpark, Power Digital (opt-in via `layout: immersive`) | `case-study-modern case-study-immersive` | `.case-study-immersive` | -Routing is via Hugo's `layout:` front matter param. A case study that does -**not** specify `layout:` uses `single.html` (the modern layout). MarketSpark -and Power Digital opt into immersive with `layout: immersive`. - -The **modern** -layout was designed from scratch and is the default for new case studies. The -**immersive** layout keeps the modern hero + stat strip **nearly verbatim** (its -body carries both `case-study-modern` and `case-study-immersive` so the modern -`.cs-*` hero/stat CSS applies; the only divergence is an optional -`client_logo_height` hook on the lockup mark) and replaces only the body with a stack -of rotating light↔dark colour cards (`.csi-*`). Don't merge them; scope every -selector under its prefix. - -**Stylesheet location:** the **modern** and **immersive** CSS — everything under -`.case-study-modern` / `.case-study-immersive` (the `.cs-*` and `.csi-*` rules, the -`$cs-*` vars and the `cs-brand-gradient` mixins) — lives in its own partial, -`assets/css/case-studies.scss`, which is `@import`ed at the **end** of -`custom.scss` so the cascade order is unchanged (compiled output is byte-identical). -The `#caseStudiesPage` list-page grid stays in `custom.scss`. +Routing is via Hugo's `layout:` front matter. No `layout:` → `single.html` +(modern, the default for new case studies, designed from scratch). MarketSpark +and Power Digital opt in with `layout: immersive`. The **immersive** layout keeps +the modern hero + stat strip **nearly verbatim** (its body carries both classes +so the modern `.cs-*` CSS applies; the only divergence is an optional +`client_logo_height` hook) and replaces only the body with a stack of rotating +light↔dark colour cards (`.csi-*`). Don't merge them; scope every selector under +its prefix. + +**Stylesheet:** all `.case-study-modern` / `.case-study-immersive` CSS (`.cs-*` +and `.csi-*` rules, `$cs-*` vars, `cs-brand-gradient` mixins) lives in +`assets/css/case-studies.scss`, `@import`ed at the **end** of `custom.scss` so +cascade order is unchanged. The `#caseStudiesPage` list-page grid stays in +`custom.scss`. --- @@ -122,12 +117,22 @@ Notes: - **No `slug:` field.** Hugo derives the URL from the filename. Prefer renaming the file (e.g. `marketspark.md` → `/case-studies/marketspark/`) over setting `slug:`, so the filename and URL stay in sync. -- **`hero_aside_image` is optional.** When present, the hero gets a full-bleed - background photo with a horizontal scrim (heavy pine on the left where the - title sits, fading to nearly clear on the right where the photo's subject - shows through), and the title sits inside a frosted translucent box - (`.cs-hero__inner--framed`). When absent, the hero is plain pine gradient + - dot grid + soft mint/pink glow with no frosted frame. +- **`hero_aside_image` is optional.** When present, the **default** is a + full-bleed background photo (`.cs-hero__bg img`: `object-fit: cover; + object-position: 70% center`) with a horizontal scrim (heavy pine left, fading + to clear right) and the title in a frosted box (`.cs-hero__inner--framed`); + MarketSpark and Power Digital use this. When absent, the hero is plain pine + gradient + dot grid + soft mint/pink glow, no frosted frame. +- **`hero_photo_inset: true`** (opt-in — currently only Cursor) swaps the + full-bleed default for an **inset-right** treatment: photo fills the right 70% + (`width: 70%`), left ~30% stays plain pine, scrim dropped. Both shaping rules + are scoped to the `.cs-hero--photo-inset` modifier (added by + `single.html`/`immersive.html`): + - **`object-position: 25% center`** biases the crop toward the photo's LEFT; + tune per photo. + - A composited `mask-image` (two gradients + `mask-composite: intersect` / + `-webkit-mask-composite: source-in`) feathers the **left seam** into pine and + softens the **bottom edge** into the stat strip. Top/right flush. --- @@ -168,6 +173,15 @@ All shortcodes are in `layouts/shortcodes/cs-*.html` and styled under node, so the partial descends one level (`(index … 0).Headings`) before listing them. Output each heading's `.Title` with `safeHTML` (it arrives HTML-encoded, e.g. `&`). +- **CSS `min()`/`max()` with mixed units fails to compile.** Hugo's LibSass + evaluates them itself and rejects `min(74%, 880px)` ("Incompatible units"); + interpolation doesn't help. Express the cap differently (a `max-width` + clamping a `%` flex-basis) or hide it inside `calc()`. +- **`%` padding and `%` flex-basis resolve against DIFFERENT boxes.** Padding + `%` uses the containing-block width; a flex child's basis `%` uses the flex + container's CONTENT box (after padding). `csi-carousel` centres slides with + side `padding: 13%` + slide `flex-basis: 100%` (⇒ 74%-wide slides) — mixing + the two bases put the first snap point ~118px off centre. - **SCSS `&__foo` inside nested rules has a trap.** When you write `&__toggle:checked ~ &__content` inside `.case-study-modern .parent { … }`, the second `&` expands to the **full** ancestor chain, producing selectors @@ -204,28 +218,24 @@ mint on mint and disappears. Always keep `:not(.button)` on `.cs-article a`. ## Immersive layout (MarketSpark, Power Digital) -Opt in with `layout: immersive`. The body element carries **both** -`case-study-modern` and `case-study-immersive`: - -- **Top (hero + stat strip): all but identical to modern.** `immersive.html` - copies the `.cs-hero` and `.cs-stat-strip` markup from `single.html`, and the - dual body class means the existing `.case-study-modern .cs-*` rules style them - — so the top renders as the modern layout. The one intentional addition is the - optional **`client_logo_height`** front-matter hook (sets `--cs-client-logo-h` - on the client lockup mark, default 32px; styled in `case-studies.scss`) so a short - client wordmark can be enlarged. Otherwise don't restyle the top here. -- **Body: a stack of contained, rounded CARDS** on the pine page backdrop, the - faces **rotating light ↔ dark** like the marketing pages (services / - who-we-are), each with a subtle gradient "photo" background (glow mesh + faint - dot grid on dark cards). Not full-bleed slides and not one flat article card. - All body CSS is scoped to `.csi-*` under `.case-study-immersive` (search - `// IMMERSIVE CASE STUDY — BODY`) — never bare element selectors — so it can't - touch the modern hero/stats. -- **No yellow.** Per the brand guidelines preference, the body deliberately omits - the brand yellow (`#ECE295`); all accents are teal (`#2ad9c2` / `#55C1B4`) → - pink (`#D191BF`) via the local `csi-grad-*` mixins (the global - `cs-brand-gradient` is NOT used in the body because it leads with yellow). The - modern hero title keeps its standard `text-gradient` (that's the kept top). +Opt in with `layout: immersive`. Body carries **both** `case-study-modern` and +`case-study-immersive`: + +- **Top (hero + stat strip):** `immersive.html` copies the `.cs-hero` / + `.cs-stat-strip` markup from `single.html`; the dual body class styles it via + the existing `.case-study-modern .cs-*` rules. Don't restyle the top here — the + one intentional addition is the optional **`client_logo_height`** hook (sets + `--cs-client-logo-h`, default 32px) to enlarge a short client wordmark. +- **Body: a stack of contained, rounded CARDS** on the pine backdrop, faces + **rotating light ↔ dark** like the marketing pages, each with a subtle gradient + "photo" background (glow mesh + faint dot grid on dark). Not full-bleed slides, + not one flat article card. All body CSS is scoped to `.csi-*` under + `.case-study-immersive` (search `// IMMERSIVE CASE STUDY — BODY`), never bare + element selectors, so it can't touch the modern hero/stats. +- **No yellow.** Per brand preference the body omits brand yellow (`#ECE295`); all + accents are teal (`#2ad9c2` / `#55C1B4`) → pink (`#D191BF`) via the local + `csi-grad-*` mixins (the global `cs-brand-gradient` leads with yellow, so it's + unused in the body). The modern hero title keeps its standard `text-gradient`. ### Card shortcodes (`csi-*`) @@ -238,7 +248,9 @@ _inside_ each block. Each emits a `
` card. | `csi-split` | Text + visual side-by-side; `flip="true"` alternates sides. | `eyebrow`, `title`, `media`, `media_alt`, `media2`/`media2_alt` (second image stacked below the first), `figure` (CSS-drawn figure partial from `figures/.html` instead of an image; subfolders work — `figure="power-digital/terralith"`), `flip`, `contain`, `caption`, `variant`, `ratio` (`50-50` default / `65-35` / `75-25`, text wider; ratios auto-reverse under `flip` since flip puts the media in the first grid track via `order`) | | `csi-steps` | Numbered process cards. Place INSIDE a `csi-section`. | inner blocks split by `---`, each `title:` / `body:` | | `csi-impact` | Outcome cards w/ gradient icon badges. INSIDE a `csi-section`. | inner blocks split by `---`, each `icon:` / `title:` / `body:`; `cols="2"` for a slimmed 2-up grid (pairs with `csi-compare`, capped to the same 980px) | -| `csi-compare` | "Then / now" migration ledger: muted old world → bold gradient new world per metric. Owns a page's hard numbers — pair with a slimmed `csi-impact` so figures aren't stated twice. Mobile stacks each row with per-cell tags. INSIDE a `csi-section`. | `before_label`, `after_label`; inner blocks split by `---`, each `label:` / `before:` / `after:` | +| `csi-compare` | "Then / now" migration ledger: muted old world → bold gradient new world per metric. Owns a page's hard numbers — pair with a slimmed `csi-impact` so figures aren't stated twice. Mobile stacks each row with per-cell tags. INSIDE a `csi-section` (also nests inside a `csi-phase`). | `before_label`, `after_label`; inner blocks split by `---`, each `label:` / `before:` / `after:` / optional `delta:` (renders as a solid-gradient pill **inline** after the new-world value — the "Change" figure) | +| `csi-phase` | A dated chapter in an engagement timeline: a wide **card** hanging off a single gradient rail (`.csi-phase__card` + `.csi-phase__rail`/`__marker`). Each phase is an `
`; place phases one after another INSIDE a `csi-section` — no wrapper element needed. Full block markdown + nested `csi-compare` ledgers work in the body. Used by the Cursor "What Masterpoint Did" timeline. | `title` (HTML ok, typically `"Month YYYY - Headline"`) | +| `csi-carousel` | Scroll-snap slideshow of result charts (used by Cursor "Executive Summary"). Slides carry NO card chrome — the chart exports are self-contained white cards, so the image itself gets the `.csi-prose img` radius + shadow. Active slide snaps to the CENTRE; neighbours peek in, with edge fades + arrows + gradient dots as scroll cues. Clicking a slide opens the chart full size in a native `` lightbox (Esc / backdrop click / × all close it). CSS scroll-snap does the scrolling; the behavior JS lives in `immersive.html` gated on `.HasShortcode "csi-carousel"` — NOT in the shortcode, because the parent csi-section markdown-renders its Inner and goldmark chops long inline scripts into indented code blocks. Arrows hide ≤700px; there a "Swipe for more →" hint (`__hint`) shows under the dots and fades once `data-swiped` is set by the JS — the mobile peek is deliberately ~25-35px wide (11% gutters, 0.7rem gap; the gap all but ate a 7% gutter) and the mobile edge fade stays narrower than the peek so it doesn't white the neighbour out. Designed for the LIGHT face. INSIDE a `csi-section`. | inner blocks split by `---`, each `image:` / `alt:` and optional `stat:` / `title:` / `body:` (caption renders only if one is present; 16:9 fits best — `aspect-ratio` crops others) | | `csi-timeline` | Horizontal parallel-track cutover bars (old system winding down while the new ramps up): percent-positioned bars with `fade: out` / `fade: in` and an optional dashed cutover marker. Typically right after `csi-steps`. INSIDE a `csi-section`. | `marker` (percent 0–100), `marker_label`; inner blocks split by `---`, each `label:` / `note:` / `start:` / `end:` / `fade:` | | `csi-questions` | Takeaways row of compact numbered question cards (gradient numeral inline with the question), plus an optional `outro:` "verdict" panel (leading `**bold**` renders as a block gradient lead line) and optional `cta:` paragraph divided inside the same panel. Sections containing one auto-compact like `csi-list` ones. 3-up, stacks ≤860px. INSIDE a `csi-section`. | inner blocks split by `---`, each `question:` / `body:`; standalone blocks may carry `outro:` or `cta:` (inline markdown works) | | `csi-list` | Compact 2-col icon rows (icon chip + bold title — inline body). Space-saving sibling of `csi-impact` for secondary enumerations (e.g. "under the hood" extras) so they don't mimic the outcome grid. INSIDE a `csi-section`. | same inner format as `csi-impact` (`icon:` / `title:` / `body:`); keep bodies to one short sentence | @@ -278,6 +290,36 @@ reflows responsively: - The pullquote card radius is trimmed to `8px` in portrait mode (the `csi-testimonial` featured band stays full-bleed / square by design). +#### Non-portrait `cs-pullquote` on mobile (`≤640px`) + +The plain (no-photo) pull quote has its own `@media (max-width: 640px)` rule on +the base `.cs-pullquote` (so it applies in **every** section face, not just the +compact `:has(.csi-list)` band). Without it, the desktop card is too big for a +phone: the `4rem` left gutter for the absolute quote glyph wastes width, the +`1.45rem` body overflows, and the `__by` attribution squeezes name + `title, +company` side by side (the role wraps awkwardly). On mobile it: drops the `__mark` +into the text flow (`position: static`, small negative bottom margin so the body +sits right under it — top padding is trimmed to `1.2rem` to match), shrinks the +body to `1.2rem`, and **stacks** the attribution (`__by` → `flex-direction: +column`, name over role). Scoped `:not(.cs-pullquote--portrait)` so the portrait +grid layout above is untouched. + +#### `csi-phase` timeline on mobile (`≤575px`) + +Desktop is a 2-column grid (`rail | card`); on a phone that rail column + gap +cramped the copy. Mobile drops to a **single flow column** (`display: block`) and +absolutely positions the rail in the left gutter (`.csi-phase__rail { position: +absolute; left: -0.9rem }`) so the marker + line sit just left of the full-width +card. The shared section gutter (`.csi-section .csi-container`) also trims to +`0 1rem` at `≤575px` (was `1.4rem`). + +#### `csi-compare` delta pill on mobile (`≤700px`) + +The `__delta` pill shares the narrow flex `__after` cell with the new-world value, +so a long label wrapped into a ragged blob under the `999px` radius. On mobile it +gets its own full-width line (`flex: 0 0 100%`), **centered** text, and a `10px` +radius so multi-line labels read as a tidy chip; short labels stay one line. + **`variant` = the card's face colour** (drives the light↔dark rotation): `light` (white) and `pine` (dark) are the two real faces. `cream`/`mint` alias to light; `pine-deep`/`gradient` alias to dark (kept so older content still renders). Steps, @@ -395,83 +437,58 @@ active-link underline INSIDE the link box — the links row is ## Print / PDF -Case studies print (Ctrl/Cmd+P → Save as PDF) styled to match the screen. - -- **The site stylesheet loads with `media="all"`** (`partials/head.html`), not - `media="screen"`. With `media="screen"` the browser ignored the entire - stylesheet when printing and dumped raw unstyled HTML — that was the original - "no styles in print" bug. -- **A `@media print` block at the end of `case-studies.scss`** (scoped to - `body.case-study-modern` / `body.case-study-immersive`) handles everything print - needs that screen doesn't: - 1. **`print-color-adjust: exact`** on the whole subtree so browsers keep our - backgrounds and gradients instead of dropping them to white for ink saving - (these layouts are almost entirely backgrounds). - 2. **AOS reset — the critical one.** Every `csi-*` card carries `data-aos`, - which starts it at `opacity: 0` until scrolled into view. Nothing scrolls - when printing, so without an override the **entire animated body prints - blank** (empty colour cards — this was the original "things get cut off" - bug). The block forces `[data-aos] { opacity:1; transform:none; - visibility:visible; transition:none }`. Don't remove this. +Case studies print (Ctrl/Cmd+P → Save as PDF) styled to match the screen. Two +foundations plus a `@media print` block do the work: + +- **Site stylesheet loads with `media="all"`** (`partials/head.html`), not + `media="screen"` — with `screen` the browser ignores the stylesheet when + printing and dumps raw unstyled HTML. +- **The `@media print` block** at the end of `case-studies.scss` (scoped to + `body.case-study-modern` / `body.case-study-immersive`) handles the rest: + 1. **`print-color-adjust: exact`** on the subtree so browsers keep our + backgrounds/gradients instead of dropping them to white (these layouts are + almost entirely backgrounds). + 2. **AOS reset (critical, don't remove).** Every `csi-*` card starts at + `opacity: 0` via `data-aos` until scrolled into view; nothing scrolls when + printing, so the whole body would print blank. Forces `[data-aos] { + opacity:1; transform:none; visibility:visible; transition:none }`. 3. **Gradient-clipped text fallback.** `-webkit-background-clip: text` + - transparent fill does not render reliably in Chrome's PDF path: the run - stops wrapping and gets sliced by the hero card's `overflow:hidden`, or the - gradient paints as a solid rectangle. So `.text-gradient` / `.csi-grad` / - `.csi-step__num` / `.csi-testimonial__mark` / `.cs-hero__lockup-x` fall - back to a solid brand colour (`#2ad9c2`) in print — any NEW class using the - grad mixins directly (not via `.csi-grad`) must join this list. - The on-screen multi-stop gradient becomes a single teal in the PDF — an - intentional, reliable trade. - 4. **Hero frosted card** drops `backdrop-filter` (unsupported in print) and - `overflow: hidden` so the title can wrap freely instead of being clipped. - 5. **Hides site chrome** so the PDF is a standalone document: `#sitewideNote`, + transparent fill is unreliable in Chrome's PDF path (run stops wrapping and + is sliced by `overflow:hidden`, or paints as a solid rectangle), so + `.text-gradient` / `.csi-grad` / `.csi-step__num` / `.csi-testimonial__mark` + / `.cs-hero__lockup-x` fall back to solid `#2ad9c2`. Intentional trade. + 4. **Hero frosted card** drops `backdrop-filter` (print-unsupported) and + `overflow: hidden` so the title wraps freely. + 5. **Hides site chrome** for a standalone PDF: `#sitewideNote`, `header[aria-label="header"]`, `#schedule-assessment`, - `footer[role=contentinfo]`, `#preloader`, and the cookie-consent banner - (`#cc-main` / `.cc-window`, orestbida CookieConsent v3 — it's injected into - `` and otherwise overlays the page in the PDF). - 6. **Performance — prevents the PDF/print-preview lag and crash.** Print - rasterizes at ~300+ DPI (~100x the pixels of screen), and several decorative - effects that are cheap on screen explode at that scale: `backdrop-filter` / - `filter: blur()` (blur kernel scales with DPI), the tiled 1px dot-grid - overlays (`background-size:~28px` on full-bleed `inset:0` `::after`s — - millions of dots), and their `mask-image` (extra offscreen buffer per - section). `print-color-adjust:exact` (item 1) forces all of it to actually - paint, so memory blows up and Chrome's renderer dies. The block zeroes + `footer[role=contentinfo]`, `#preloader`, cookie banner (`#cc-main` / + `.cc-window`, injected into ``). + 6. **Performance (prevents print lag/crash).** Print rasterizes at ~300+ DPI, + where `backdrop-filter` / `filter: blur()`, the tiled 1px dot-grid `::after` + overlays, and their `mask-image` explode — `print-color-adjust:exact` forces + them all to paint and Chrome's renderer dies. The block zeroes `backdrop-filter` / `filter` / `mask-image` / `box-shadow` on `*` and drops - `::before`/`::after` `background-image` for print. (Same effects are why - on-screen scrolling can feel slow — they re-composite on the GPU per frame.) - 7. **Page-break control — prevents cards clipping across page boundaries.** - `break-inside: avoid` on the **small, repeating** cards (`.cs-stat`, - `.cs-pullquote`, `.csi-testimonial`, `.csi-impact__card`, `.csi-compare__row`, - `.csi-fig-terralith__mono/__stack`, `.csi-timeline`, `.csi-question`) pushes a whole - card to the next page rather than slicing its top/bottom edge. **Only apply - it to small cards** — putting it on large one-off blocks (the About panel, - article/screenshot cards) backfires: shoving a big block whole to the next - page leaves a huge blank gap on the page it left, which looks worse than the - occasional slice. Those large blocks fit on their own, so let them flow. -- To **keep** a chrome element in the PDF, drop it from that `display: none` list. -- **When you add a new case-study element / shortcode / CSS class, check this - print checklist.** Most additions need NOTHING — the perf and colour-accuracy - rules use universal selectors (`*`, `*::before`, `*::after`), so new elements - inherit them automatically. Only touch the `@media print` block if the new - thing falls into one of these three buckets: - 1. **Gradient-clipped text** (`-webkit-background-clip: text` + transparent - fill)? → add its class to the solid-colour fallback list (item 3 above), or - it prints sliced / as a stray coloured rectangle. - 2. **A small, self-contained card** that would look broken if sliced across a - page break? → add it to the `break-inside: avoid` list (item 7). Do NOT add - large/one-off blocks or full-bleed section bands (see the dense-flow note). - 3. **New site chrome injected into ``** (banner, modal, widget) that - shouldn't appear in the PDF? → add its selector to the `display: none` list - (item 5). - A genuinely new heavy effect (e.g. a new `filter`/`mask` that the universal - resets don't already cover) is the only other reason to edit the block. -- **Verify print changes with a real PDF**, not screen DevTools — Chrome's print - path diverges (AOS, backdrop-filter, background-clip:text all behave - differently). Generate one headless and read it back: - `"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless=new - --disable-gpu --no-pdf-header-footer --print-to-pdf=out.pdf - http://localhost:1313/case-studies/marketspark/` + `::before`/`::after` `background-image`. (Same effects make on-screen scroll + feel slow.) + 7. **Page-break control.** `break-inside: avoid` on **small, repeating** cards + only (`.cs-stat`, `.cs-pullquote`, `.csi-testimonial`, `.csi-impact__card`, + `.csi-compare__row`, `.csi-fig-terralith__mono/__stack`, `.csi-timeline`, + `.csi-question`, `.csi-carousel__slide`) so a card isn't sliced across a page + boundary. NOT on large one-off blocks (About panel, article/screenshot + cards) — shoving a big block whole leaves a worse blank gap; let those flow. + +**Adding a new element?** Most need nothing — the perf/colour rules use universal +selectors (`*`, `::before`, `::after`). Only edit the block if the new thing is: +gradient-clipped text (→ item 3 list), a small self-contained card (→ item 7 +list, never large/full-bleed blocks), new `` chrome to hide (→ item 5 +list), or a scroll container (`overflow-x: auto` can't scroll on paper — lay +items out in flow like `csi-carousel`: scroller `display: block`, slides stacked, +arrows/dots hidden). To **keep** chrome in the PDF, drop it from the item-5 list. + +**Verify with a real headless PDF**, not screen DevTools (Chrome's print path +diverges): `"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" +--headless=new --disable-gpu --no-pdf-header-footer --print-to-pdf=out.pdf +http://localhost:1313/case-studies/marketspark/` --- diff --git a/layouts/case-studies/immersive.html b/layouts/case-studies/immersive.html index 2fbc3cb7..fd3687eb 100644 --- a/layouts/case-studies/immersive.html +++ b/layouts/case-studies/immersive.html @@ -48,10 +48,10 @@ {{/* HERO — verbatim from single.html (modern), do not diverge */}} {{ $bgImg := or .Params.hero_image .Params.hero_aside_image }} -
+
{{ if $bgImg }}
- + {{ .Params.hero_aside_alt | default
{{ end }}
@@ -157,5 +157,87 @@

})(); {{ end }} + {{ if .HasShortcode "csi-carousel" }} + {{/* csi-carousel behavior — lives here (not in the shortcode) because the + shortcode's output is markdown-rendered by its parent csi-section and + goldmark chops long inline scripts into indented code blocks. */}} + + {{ end }} diff --git a/layouts/case-studies/single.html b/layouts/case-studies/single.html index 12ce6b61..719f8e5b 100644 --- a/layouts/case-studies/single.html +++ b/layouts/case-studies/single.html @@ -17,11 +17,11 @@ {{ if .Params.client_logo }}
- {{ .Params.client | default + {{ .Params.client | default `` }} logo
- Masterpoint logo + Masterpoint logo
{{ end }} diff --git a/layouts/shortcodes/csi-carousel.html b/layouts/shortcodes/csi-carousel.html new file mode 100644 index 00000000..0700054b --- /dev/null +++ b/layouts/shortcodes/csi-carousel.html @@ -0,0 +1,79 @@ +{{/* + csi-carousel — scroll-snap slideshow of result charts. Each slide is a chart + card with a caption row (gradient stat + one-line takeaway). Slides are + narrower than the viewport so the next chart peeks in from the right — that + peek (plus the edge fade, arrows and dots) signals there's more to scroll. + Place INSIDE a csi-section (designed for the light face). + Inner: blocks separated by `---`, each block: + image: /img/case-studies/CLIENT/chart.png (16:9 works best) + alt: Descriptive alt text for the chart + stat: 80% (big gradient figure) + title: Faster plans. (bold lead-in, include the period) + body: One-sentence takeaway. Inline markdown works. +*/}} +{{- $slides := slice -}} +{{- range split .Inner "---" -}} + {{- $block := trim . " \t\n" -}} + {{- if $block -}} + {{- $s := dict -}} + {{- range split $block "\n" -}} + {{- $line := trim . " \t" -}} + {{- if hasPrefix $line "image:" -}} + {{- $s = merge $s (dict "image" (trim (strings.TrimPrefix "image:" $line) " ")) -}} + {{- else if hasPrefix $line "alt:" -}} + {{- $s = merge $s (dict "alt" (trim (strings.TrimPrefix "alt:" $line) " ")) -}} + {{- else if hasPrefix $line "stat:" -}} + {{- $s = merge $s (dict "stat" (trim (strings.TrimPrefix "stat:" $line) " ")) -}} + {{- else if hasPrefix $line "title:" -}} + {{- $s = merge $s (dict "title" (trim (strings.TrimPrefix "title:" $line) " ")) -}} + {{- else if hasPrefix $line "body:" -}} + {{- $s = merge $s (dict "body" (trim (strings.TrimPrefix "body:" $line) " ")) -}} + {{- else if and $line ($s.body) -}} + {{- $s = merge $s (dict "body" (printf "%s %s" $s.body $line)) -}} + {{- end -}} + {{- end -}} + {{- $slides = $slides | append $s -}} + {{- end -}} +{{- end -}} +{{- $count := len $slides -}} + +{{/* Behavior JS lives in layouts/case-studies/immersive.html, gated on .HasShortcode "csi-carousel" — an inline script here would pass through the parent csi-section's markdown render, and goldmark chops long scripts into indented code blocks. */}} diff --git a/layouts/shortcodes/csi-compare.html b/layouts/shortcodes/csi-compare.html index f766d2ee..9b6dd492 100644 --- a/layouts/shortcodes/csi-compare.html +++ b/layouts/shortcodes/csi-compare.html @@ -11,6 +11,9 @@ label: Metric name before: Old-world value after: New-world value (inline markdown works in all three) + delta: optional improvement pill after the after value (e.g. "39.3% faster") + neutral: optional "true" → render the after value in plain ink (no gradient) + for counts that aren't improvements (e.g. sample size) */}} {{- $beforeLabel := .Get "before_label" | default "Before" -}} {{- $afterLabel := .Get "after_label" | default "After" -}} @@ -31,6 +34,8 @@ {{- $label := "" -}} {{- $before := "" -}} {{- $after := "" -}} + {{- $delta := "" -}} + {{- $neutral := false -}} {{- range $lines -}} {{- $line := trim . " \t" -}} {{- if hasPrefix $line "label:" -}} @@ -39,13 +44,17 @@ {{- $before = trim (strings.TrimPrefix "before:" $line) " " -}} {{- else if hasPrefix $line "after:" -}} {{- $after = trim (strings.TrimPrefix "after:" $line) " " -}} + {{- else if hasPrefix $line "delta:" -}} + {{- $delta = trim (strings.TrimPrefix "delta:" $line) " " -}} + {{- else if hasPrefix $line "neutral:" -}} + {{- $neutral = eq (lower (trim (strings.TrimPrefix "neutral:" $line) " ")) "true" -}} {{- end -}} {{- end -}}
  • {{ $label | $.Page.RenderString (dict "display" "inline") }}
    {{ $beforeLabel | safeHTML }}{{ $before | $.Page.RenderString (dict "display" "inline") }}
    -
    {{ $afterLabel | safeHTML }}{{ $after | $.Page.RenderString (dict "display" "inline") }}
    +
    {{ $afterLabel | safeHTML }}{{ $after | $.Page.RenderString (dict "display" "inline") }}{{ with $delta }}{{ . | $.Page.RenderString (dict "display" "inline") }}{{ end }}
  • {{- end -}} {{- end -}} diff --git a/layouts/shortcodes/csi-phase.html b/layouts/shortcodes/csi-phase.html new file mode 100644 index 00000000..4abd62f8 --- /dev/null +++ b/layouts/shortcodes/csi-phase.html @@ -0,0 +1,22 @@ +{{/* + csi-phase — a dated chapter in an engagement timeline: a wide card hanging off + a single gradient rail. Each phase is an
    so a sequence reads as one + continuous story while the rail makes a long engagement easy to scan. Full + block markdown works in the body (paragraphs, lists, links, and nested csi-* + shortcodes such as a csi-compare ledger). Place phases one after another + INSIDE a csi-section — no wrapper element is needed. + Args: + title phase heading (HTML allowed) — typically "Month YYYY - Headline" +*/}} +{{- $title := .Get "title" -}} +
    + +
    +{{- with $title }}

    {{ . | safeHTML }}

    {{ end -}} +
    +{{ .Inner | $.Page.RenderString (dict "display" "block") }} +
    +
    +
    diff --git a/static/img/case-studies/cursor/cursor-2x-more-deploy-masterpoint.png b/static/img/case-studies/cursor/cursor-2x-more-deploy-masterpoint.png new file mode 100644 index 00000000..eae15c86 Binary files /dev/null and b/static/img/case-studies/cursor/cursor-2x-more-deploy-masterpoint.png differ diff --git a/static/img/case-studies/cursor/cursor-infrastructure-democratized-masterpoint.png b/static/img/case-studies/cursor/cursor-infrastructure-democratized-masterpoint.png new file mode 100644 index 00000000..261c1dce Binary files /dev/null and b/static/img/case-studies/cursor/cursor-infrastructure-democratized-masterpoint.png differ diff --git a/static/img/case-studies/cursor/cursor-lockup-white.svg b/static/img/case-studies/cursor/cursor-lockup-white.svg new file mode 100644 index 00000000..c0d4466f --- /dev/null +++ b/static/img/case-studies/cursor/cursor-lockup-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/img/case-studies/cursor/cursor-masterpoint.png b/static/img/case-studies/cursor/cursor-masterpoint.png new file mode 100644 index 00000000..48f390cb Binary files /dev/null and b/static/img/case-studies/cursor/cursor-masterpoint.png differ diff --git a/static/img/case-studies/cursor/cursor-team-photo.webp b/static/img/case-studies/cursor/cursor-team-photo.webp new file mode 100644 index 00000000..0fa8d5a8 Binary files /dev/null and b/static/img/case-studies/cursor/cursor-team-photo.webp differ diff --git a/static/img/case-studies/cursor/cursor-terraform-5x-faster-masterpoint.png b/static/img/case-studies/cursor/cursor-terraform-5x-faster-masterpoint.png new file mode 100644 index 00000000..07c34ce5 Binary files /dev/null and b/static/img/case-studies/cursor/cursor-terraform-5x-faster-masterpoint.png differ diff --git a/static/img/case-studies/cursor/cursor-terraform-blast-radius-masterpoint.png b/static/img/case-studies/cursor/cursor-terraform-blast-radius-masterpoint.png new file mode 100644 index 00000000..87d25aee Binary files /dev/null and b/static/img/case-studies/cursor/cursor-terraform-blast-radius-masterpoint.png differ