Add tasteful motion starter set (reviewable, gated, LCP-safe)

Progressive-enhancement motion, all gated on a single html.has-motion line so
it's trivially on/off:
- Section reveal-on-scroll (fade + 10px rise), below the fold only — hero stays
  instant so LCP is untouched. Animates opacity/transform only (CLS 0).
- Staggered reveal on the three "how we work" blocks.
- Grow-on-hover underline for prose links (clearer affordance).
- An open-o divider that settles in just before the contact CTA.
- Persistent "Book a conversation" that returns after the hero scrolls off and
  steps aside at the contact section.
~1KB inline vanilla JS (IntersectionObserver), try/catch + reduced-motion guard
so content is never left hidden. No animation library.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 17:47:02 -07:00
parent 043770f8f4
commit a2a57ce8a1
2 changed files with 96 additions and 9 deletions
+44
View File
@@ -279,3 +279,47 @@ a:focus-visible, .btn:focus-visible { outline: 2px solid var(--amber); outline-o
.lf-err { font-family: 'DM Sans', sans-serif; font-size: 0.85rem; color: #b4452f; margin: 1rem 0 0; }
.lf-err a { color: #b4452f; }
.lf-alt { font-family: 'DM Sans', sans-serif; font-size: 0.82rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-soft); text-align: center; margin: 0.5rem 0 0.8rem; }
/* ── Motion — progressive enhancement ──────────────────────────────────────
Gated on html.has-motion (one inline <head> line is the on/off switch).
Hero is never animated (LCP stays instant). Only opacity/transform animate
(CLS 0). All off under prefers-reduced-motion. JS adds .in; a timeout
fallback guarantees content is never left hidden. */
.has-motion .reveal { opacity: 0; transform: translateY(12px); transition: opacity .6s ease, transform .6s ease; }
.has-motion .reveal.in { opacity: 1; transform: none; }
.has-motion .stagger > * { opacity: 0; transform: translateY(12px); transition: opacity .55s ease, transform .55s ease; }
.has-motion .stagger.in > * { opacity: 1; transform: none; }
.has-motion .stagger.in > *:nth-child(2) { transition-delay: .1s; }
.has-motion .stagger.in > *:nth-child(3) { transition-delay: .2s; }
.has-motion .stagger.in > *:nth-child(4) { transition-delay: .3s; }
/* Underline that grows on hover — replaces the static border on prose links */
.bridge-link a, .person a {
border-bottom: 0;
background-image: linear-gradient(var(--amber-deep), var(--amber-deep));
background-size: 0 1.5px; background-position: 0 100%; background-repeat: no-repeat;
transition: background-size .3s ease, color .2s ease;
}
.bridge-link a:hover, .person a:hover { background-size: 100% 1.5px; }
/* Open-o divider — a quiet brand beat that settles in right before the ask */
.o-divider { text-align: center; margin: 8px 0 -10px; }
.o-divider svg { width: 30px; height: 30px; }
.o-divider path { fill: var(--amber); }
.has-motion .o-divider { opacity: 0; transform: scale(.7); transition: opacity .6s ease, transform .6s cubic-bezier(.2,.8,.2,1); }
.has-motion .o-divider.in { opacity: 1; transform: none; }
/* Persistent CTA — returns after the hero scrolls off, steps aside at the contact */
.float-cta { position: fixed; right: 22px; bottom: 22px; z-index: 80; opacity: 0; transform: translateY(12px); pointer-events: none; transition: opacity .35s ease, transform .35s ease; }
.float-cta.show { opacity: 1; transform: none; pointer-events: auto; }
.float-cta .btn { box-shadow: 0 6px 22px rgba(15, 23, 41, 0.18); }
@media (prefers-reduced-motion: reduce) {
.has-motion .reveal, .has-motion .stagger > * { opacity: 1; transform: none; }
.has-motion .o-divider { opacity: 1; transform: none; }
.float-cta { transition: none; }
}
@media (max-width: 600px) {
.float-cta { right: 12px; left: 12px; bottom: 12px; }
.float-cta .btn { display: block; text-align: center; }
}