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
+49 -6
View File
@@ -44,6 +44,7 @@
"knowsAbout": ["change management", "leadership coaching", "organizational change", "manufacturing succession", "knowledge transfer"]
}
</script>
<script>document.documentElement.className += ' has-motion';</script>
</head>
<body>
@@ -90,7 +91,7 @@
<section class="band dark statement" data-bg="dark" id="problem">
<div class="motif" aria-hidden="true"></div>
<div class="wrap narrow">
<div class="wrap narrow reveal">
<p class="eyebrow">The part most consultants skip</p>
<h2 class="statement-line">You can buy the software. You can write the procedures. And still watch the whole thing <em>quietly fall apart.</em></h2>
<p>Because the new way asks people to change — and people don't resist change to be difficult. They resist it because it threatens something: their standing, their habits, the thing that's made them the one everyone relies on. The veteran who won't quite share what he knows. The supervisor who nods in the meeting and does it the old way Monday morning. The blame that starts flying the first time something goes wrong.</p>
@@ -99,7 +100,7 @@
</section>
<section class="band light" data-bg="light">
<div class="wrap narrow">
<div class="wrap narrow reveal">
<p class="eyebrow">One business, two sides</p>
<h2>The human side of <em>ShopWisdom.</em></h2>
<p>Open Communications is the human side of <strong>ShopWisdom</strong>. ShopWisdom builds the systems that keep a shop running when your best people leave. We make sure the people actually use them — and come through the change stronger, not fractured.</p>
@@ -110,10 +111,12 @@
<section class="band light services" data-bg="light" id="work">
<div class="wrap">
<div class="reveal">
<p class="eyebrow">How we work</p>
<h2>The work happens in <em>three places.</em></h2>
<p class="section-intro">Depending on what's actually getting in the way.</p>
<div class="work-list">
</div>
<div class="work-list stagger">
<article class="work-item">
<div class="work-head">
<h3>The people who carry the knowledge</h3>
@@ -145,12 +148,12 @@
</div>
</article>
</div>
<p class="reassure">Most engagements draw on more than one of these. A knowledge holder who won't quite let go is also a team-alignment question; a process redesign that doesn't involve the people doing the work tends not to stick. We design each engagement around what's actually needed — not a fixed program.</p>
<p class="reassure reveal">Most engagements draw on more than one of these. A knowledge holder who won't quite let go is also a team-alignment question; a process redesign that doesn't involve the people doing the work tends not to stick. We design each engagement around what's actually needed — not a fixed program.</p>
</div>
</section>
<section class="band light who" data-bg="light" id="who">
<div class="wrap narrow">
<div class="wrap narrow reveal">
<p class="eyebrow">Who we are</p>
<h2>Rigor <em>and soul.</em></h2>
<p class="section-intro">Our change practice is led by Michael Victory, with Inga Michaelsen as co-facilitator.</p>
@@ -169,8 +172,12 @@
</div>
</section>
<div class="o-divider" aria-hidden="true">
<svg viewBox="0 0 538 538"><path d="M269.06,0C120.46,0,0,120.46,0,269.06s120.46,269.06,269.06,269.06,269.06-120.46,269.06-269.06S417.66,0,269.06,0ZM276.06,356.34c-48.2,0-87.28-39.07-87.28-87.28s39.07-87.28,87.28-87.28,87.28,39.08,87.28,87.28-39.07,87.28-87.28,87.28Z"/></svg>
</div>
<section class="band light contact" data-bg="light" id="contact">
<div class="wrap narrow">
<div class="wrap narrow reveal">
<p class="eyebrow">Start a conversation</p>
<h2>The people part doesn't have to be <em>a mystery.</em></h2>
<p>No pitch, no program to sign up for. Just a straight talk about what's getting in the way and whether we can help.</p>
@@ -352,5 +359,41 @@
Cal("init", { origin: "https://cal.shopwisdom.ca" });
</script>
<div class="float-cta">
<a class="btn" href="https://cal.shopwisdom.ca/michaelvictory/20min-opencommunications-meeting" data-cal-link="michaelvictory/20min-opencommunications-meeting" data-cal-origin="https://cal.shopwisdom.ca" data-cal-config='{"theme":"light"}'>Book a conversation</a>
</div>
<!-- Motion: reveal-on-scroll + stagger + open-o divider + persistent CTA. Gated on html.has-motion (the one line in <head>). -->
<script>
(function () {
var root = document.documentElement;
if (!root.classList.contains('has-motion')) return;
var nodes = [].slice.call(document.querySelectorAll('.reveal, .stagger, .o-divider'));
function showAll() { nodes.forEach(function (n) { n.classList.add('in'); }); }
try {
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } });
}, { rootMargin: '0px 0px -10% 0px', threshold: 0.12 });
nodes.forEach(function (n) { io.observe(n); });
} else { showAll(); }
} catch (err) { showAll(); }
var cta = document.querySelector('.float-cta');
var hero = document.querySelector('.hero');
var contact = document.getElementById('contact');
if (cta && hero) {
var upd = function () {
var pastHero = window.scrollY > hero.offsetHeight - 90;
var atContact = contact && contact.getBoundingClientRect().top < window.innerHeight * 0.92;
cta.classList.toggle('show', pastHero && !atContact);
};
upd();
window.addEventListener('scroll', function () { window.requestAnimationFrame(upd); }, { passive: true });
window.addEventListener('resize', upd);
}
})();
</script>
</body>
</html>
+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; }
}