From b257d92636f0fd38f1dfedf1cb13637354736af1 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Fri, 15 May 2026 17:33:29 -0700 Subject: [PATCH] Testimonials: center, scale by length, smoother fade Center-align the testimonial card (text + attribution row), then size the quote based on character count so short, punchy quotes get the spotlight and long ones tighten down: is-xshort (<=90 chars) 3rem cap is-short (<=180 chars) 2.25rem cap is-medium (<=320 chars) 1.55rem cap (the previous baseline) is-long (<=520 chars) 1.20rem cap is-xlong (>520 chars) 1.05rem cap Counts use [...str] so grapheme-cluster emoji (skin-tone, ZWJ) count as a single visual character instead of inflating the length. Crossfade: drop the secondary translateY (it stuttered against centered content), bump duration 600ms -> 1100ms, swap easing to cubic-bezier(0.65, 0, 0.35, 1) for a softer in/out, and add will-change:opacity so the browser keeps the layer on the GPU. --- assets/css/styles.css | 52 ++++++++++++++++++++++++++++++++----------- assets/js/app.js | 9 +++++++- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/assets/css/styles.css b/assets/css/styles.css index d7a6597..c5cb84e 100644 --- a/assets/css/styles.css +++ b/assets/css/styles.css @@ -493,16 +493,15 @@ body::before { grid-area: stack; opacity: 0; visibility: hidden; - transform: translateY(8px); - transition: opacity 600ms var(--ease-strong), transform 600ms var(--ease-strong), visibility 0s linear 600ms; + transition: opacity 1100ms cubic-bezier(0.65, 0, 0.35, 1), visibility 0s linear 1100ms; pointer-events: none; + will-change: opacity; } .carousel__track > .is-active { opacity: 1; visibility: visible; - transform: none; pointer-events: auto; - transition: opacity 600ms var(--ease-strong), transform 600ms var(--ease-strong), visibility 0s; + transition: opacity 1100ms cubic-bezier(0.65, 0, 0.35, 1), visibility 0s; } .carousel--stacked .carousel__track { display: flex; @@ -545,9 +544,11 @@ body::before { /* ───── testimonial card ───── */ .card--testimonial { - padding: clamp(1.75rem, 3vw, 2.75rem); + padding: clamp(1.75rem, 3vw, 3rem) clamp(1.5rem, 4vw, 3.5rem); display: flex; flex-direction: column; - gap: 1.5rem; + align-items: center; + text-align: center; + gap: 1.25rem; min-height: clamp(220px, 24vw, 300px); } .card--testimonial::after { display: none; } @@ -559,25 +560,50 @@ body::before { font-style: italic; font-variation-settings: "opsz" 144, "SOFT" 100; font-weight: 400; - align-self: start; user-select: none; } .testimonial__quote { font-family: var(--display); - font-size: clamp(1.15rem, 0.95rem + 0.7vw, 1.45rem); - line-height: 1.45; + font-size: var(--quote-fs, clamp(1.2rem, 0.95rem + 0.85vw, 1.55rem)); + line-height: var(--quote-lh, 1.45); font-weight: 400; font-variation-settings: "opsz" 60, "SOFT" 100; color: var(--ink); margin: 0; letter-spacing: -0.005em; - max-width: 60ch; + max-width: 64ch; + text-wrap: balance; } + +/* length-driven scale: shorter quotes get the spotlight, longer ones tighten */ +.card--testimonial.is-xshort { + --quote-fs: clamp(1.85rem, 1.2rem + 2.4vw, 3rem); + --quote-lh: 1.20; +} +.card--testimonial.is-short { + --quote-fs: clamp(1.5rem, 1.05rem + 1.7vw, 2.25rem); + --quote-lh: 1.30; +} +.card--testimonial.is-medium { + --quote-fs: clamp(1.2rem, 0.95rem + 0.85vw, 1.55rem); + --quote-lh: 1.45; +} +.card--testimonial.is-long { + --quote-fs: clamp(1.05rem, 0.95rem + 0.35vw, 1.20rem); + --quote-lh: 1.55; +} +.card--testimonial.is-xlong { + --quote-fs: clamp(0.95rem, 0.92rem + 0.18vw, 1.05rem); + --quote-lh: 1.6; +} + .testimonial__by { - display: flex; align-items: center; gap: 0.85rem; + display: flex; align-items: center; justify-content: center; gap: 0.85rem; margin-top: auto; - padding-top: 0.5rem; + padding-top: 0.85rem; border-top: 1px solid var(--rule); + width: 100%; + max-width: 32rem; } .testimonial__avatar { flex: 0 0 auto; @@ -590,7 +616,7 @@ body::before { } .testimonial__avatar img { width: 100%; height: 100%; object-fit: cover; } .testimonial__avatar img.is-favicon { width: 60%; height: 60%; object-fit: contain; } -.testimonial__attr { display: flex; flex-direction: column; gap: 0.1rem; line-height: 1.3; min-width: 0; } +.testimonial__attr { display: flex; flex-direction: column; gap: 0.1rem; line-height: 1.3; min-width: 0; text-align: left; } .testimonial__name { font-family: var(--body); font-weight: 600; diff --git a/assets/js/app.js b/assets/js/app.js index 831ab85..5d0bca0 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -117,8 +117,15 @@ const isFavicon = !(it.image || it.icon); const initial = (it.name || it.org || "·").trim().charAt(0).toUpperCase(); const meta = [it.role, it.org].filter(Boolean).join(", "); + const len = [...String(it.quote || "")].length; + const lengthClass = + len <= 90 ? " is-xshort" : + len <= 180 ? " is-short" : + len <= 320 ? " is-medium" : + len <= 520 ? " is-long" : + " is-xlong"; return ` - <${tag} class="card card--testimonial reveal" ${attrs}> + <${tag} class="card card--testimonial reveal${lengthClass}" ${attrs}>
${esc(it.quote)}