deepdive: add Beat-5 'Everything Client-Managed' frame around platform stack

Why: closes the Beat-5 summary with a visible boundary + label so the
"client-managed" point lands visually, not just in copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Hvid 2026-05-19 15:11:05 +02:00
parent 547515061c
commit e3439d6c8f
3 changed files with 45 additions and 1 deletions

View file

@ -226,6 +226,9 @@
<p class="pl-body"> <p class="pl-body">
Fenja brings together all the pieces to solve simple Fenja brings together all the pieces to solve simple
and complex AI use cases across your organisation. and complex AI use cases across your organisation.
Every component hosted in your infrastructure with
full traceability and governance. Secure and sovereign
by design.
</p> </p>
</div> </div>
@ -238,6 +241,13 @@
<div class="pl-canvas-wrap"> <div class="pl-canvas-wrap">
<div class="pl-canvas"> <div class="pl-canvas">
<!-- Beat-5 frame: thin border around all three layers
with a top-right label. Hidden until Beat 5 fires;
platform.js fades opacity to 1. -->
<div class="pl-canvas-frame" aria-hidden="true">
<span class="pl-canvas-frame-label">Everything Client-Managed</span>
</div>
<!-- Foundation — 3 equal cards (corrects the original <!-- Foundation — 3 equal cards (corrects the original
handoff's tall-Knowledge asymmetry: that asymmetry handoff's tall-Knowledge asymmetry: that asymmetry
was for a single Knowledge concept; we've split was for a single Knowledge concept; we've split

View file

@ -318,11 +318,40 @@ body:has(#page-product-deepdive.is-active) .dot-nav-tray { opacity: 0; }
} }
.pl-canvas { .pl-canvas {
position: relative; /* anchors .pl-canvas-frame */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; /* layer-to-layer gap, per design spec */ gap: 20px; /* layer-to-layer gap, per design spec */
} }
/* Beat-5 frame a thin outline around the three settled layer
groups with a label sitting above the top-right corner. Initial
opacity 0 so it's invisible during Beats 14; platform.js fades
it in on Beat 5. The negative inset gives breathing room between
the existing card-group surfaces and the outline. */
.pl-canvas-frame {
position: absolute;
inset: -18px;
border: 1px solid var(--outline-variant, rgba(56,56,49,0.22));
border-radius: calc(var(--radius-lg) + 6px);
pointer-events: none;
opacity: 0;
}
.pl-canvas-frame-label {
position: absolute;
bottom: 100%; /* sits above the top edge */
right: 0;
margin-bottom: 8px;
font-family: var(--font-sans);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--on-surface-variant);
white-space: nowrap;
}
.pl-eyebrow { .pl-eyebrow {
font-family: var(--font-sans); font-family: var(--font-sans);
font-weight: 500; font-weight: 500;
@ -475,4 +504,5 @@ body:has(#page-product-deepdive.is-active) .dot-nav-tray { opacity: 0; }
#platform-layers .pl-canvas-wrap { width: 100%; } #platform-layers .pl-canvas-wrap { width: 100%; }
#platform-layers .pl-group { opacity: 1; } #platform-layers .pl-group { opacity: 1; }
#platform-layers .pl-card { opacity: 1; transform: none; } #platform-layers .pl-card { opacity: 1; transform: none; }
#platform-layers .pl-canvas-frame { opacity: 1; }
} }

View file

@ -125,6 +125,7 @@
const cardsF = groupF ? Array.from(groupF.querySelectorAll('.pl-card')) : []; const cardsF = groupF ? Array.from(groupF.querySelectorAll('.pl-card')) : [];
const cardsT = groupT ? Array.from(groupT.querySelectorAll('.pl-card')) : []; const cardsT = groupT ? Array.from(groupT.querySelectorAll('.pl-card')) : [];
const cardsA = groupA ? Array.from(groupA.querySelectorAll('.pl-card')) : []; const cardsA = groupA ? Array.from(groupA.querySelectorAll('.pl-card')) : [];
const frame = section.querySelector('.pl-canvas-frame');
if (!groupF || !groupT || !groupA || if (!groupF || !groupT || !groupA ||
copies.length !== 5 || copies.length !== 5 ||
@ -145,6 +146,7 @@
gsap.set([groupF, groupT, groupA], { opacity: 0 }); gsap.set([groupF, groupT, groupA], { opacity: 0 });
gsap.set([...cardsF, ...cardsT, ...cardsA], { opacity: 0, y: 24 }); gsap.set([...cardsF, ...cardsT, ...cardsA], { opacity: 0, y: 24 });
gsap.set(copies, { opacity: 0, y: 14 }); gsap.set(copies, { opacity: 0, y: 14 });
if (frame) gsap.set(frame, { opacity: 0 });
const BEAT = 1.0; const BEAT = 1.0;
const tl = gsap.timeline({ const tl = gsap.timeline({
@ -212,10 +214,12 @@
fadeInCopy(3, t4); fadeInCopy(3, t4);
// Beat 5 — closing summary panel. Diagram is fully assembled // Beat 5 — closing summary panel. Diagram is fully assembled
// by now; only the copy stage swaps to the summary text. // by now; the copy stage swaps to the summary text and the
// "Everything Client-Managed" frame fades in around the stack.
const t5 = 4 * BEAT; const t5 = 4 * BEAT;
fadeOutPrev(4, t5); fadeOutPrev(4, t5);
fadeInCopy(4, t5); fadeInCopy(4, t5);
if (frame) tl.to(frame, { opacity: 1, duration: 0.20, ease: 'power2.out' }, t5 + 0.10);
} }
// ─── "The Platform" Part A: The Question ──────────────────── // ─── "The Platform" Part A: The Question ────────────────────