stack-scene: centered title, per-card counters, earlier anchor

- Stack title bar moves from top-left next to the site-mark to
  centered at ~14vh so the title anchors visually to the cards
  below. Font size bumped to clamp(2rem, 3.6vw, 3rem).
- Counter ("1 / 4 … 4 / 4") relocates from the title bar into
  each .layer-card as a .card-counter element in the top-right of
  each card-box. No longer driven by ScrollTrigger onUpdate —
  each card carries its own number, so stacked + grid phases
  both read correctly without JS. Grid phase shrinks the counter
  so it doesn't compete with the per-cell label.
- SCENE_ANCHOR_OFFSET for stack-scene drops from 1800 back to 0,
  so clicking the "Capabilities" dot lands at the top of the
  pin — the title and first card come in together instead of
  starting mid-stack.

Welcome step: the "desktop experience" aside and its CSS are
removed. Users now see only the two definitions before the CTA.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arlind Ukshini 2026-04-24 10:46:08 +02:00
parent 43129717bf
commit a2cbf57ce2
3 changed files with 49 additions and 106 deletions

View file

@ -463,11 +463,6 @@
// to yPercent, so centering is preserved).
copyLayers.forEach(el => gsap.set(el, { yPercent: -50, opacity: 0, y: 20 }));
// Counter element in the stack-scene title bar. Updated on scroll to
// tick 1/4 → 4/4 as each card lands. Read inside the ScrollTrigger
// onUpdate below. Null-safe so missing markup doesn't break the scene.
const stackCounterEl = document.querySelector('#stack-counter .sc-current');
const stackTl = gsap.timeline({
scrollTrigger: {
trigger: '#stack-scene',
@ -478,22 +473,6 @@
pinSpacing: true,
anticipatePin: 1,
invalidateOnRefresh: true,
// Drive the "N/4" counter in the title bar. Each card in Phase A
// occupies ~0.105 of the timeline (see cards.forEach below), so
// the Nth card is fully landed at roughly N * 0.105. We show the
// *current* card — i.e. the highest-index card that has started
// its fall. After Phase A completes (~0.42) we hold on 4/4.
onUpdate(self) {
if (!stackCounterEl) return;
const p = self.progress;
// Landing midpoints: card i (0-indexed) finishes at roughly
// i * 0.105 + 0.1. Use floor((p - 0.01) / 0.105) + 1 so the
// tick advances slightly after each card starts, then clamp.
const n = Math.min(4, Math.max(1, Math.floor(p / 0.105) + 1));
if (stackCounterEl.textContent !== String(n)) {
stackCounterEl.textContent = String(n);
}
},
}
});
@ -1236,9 +1215,9 @@
// dot-nav button anchors to it, so the reader lands AFTER the scene's
// initial reveal rather than at an empty pre-scrub frame.
//
// stack-scene — pin is 5000px long; Phase A (cards falling in) is
// complete at ~0.42 of that (~2100px). 1800 lands just before the
// 4th card fully settles, so the reveal still has one tick to go.
// stack-scene — offset 0 (top of the pin) so the reader lands right
// when the title appears and the first card starts its fall, and
// sees the full progression through all 4 landings.
//
// bifrost — section is 200vh with a scrubbed reveal that runs from
// top-top to bottom-bottom (100vh scroll range). The sub-headline
@ -1251,9 +1230,8 @@
function getSceneAnchorOffset(sceneId) {
const vh = window.innerHeight;
switch (sceneId) {
case 'stack-scene': return 1800;
case 'bifrost': return Math.round(vh * 0.85);
default: return 0;
case 'bifrost': return Math.round(vh * 0.85);
default: return 0;
}
}

View file

@ -1072,52 +1072,30 @@ html {
/* ───────── Stack scene title bar ─────────
Sits above the card theatre and rides with the pin so it remains
visible through all four card landings and the grid rearrange. The
.sc-current token is updated from bifrost.js's ScrollTrigger onUpdate
— 1/4 → 4/4 — one tick per landing card.
Vertical top is aligned with the fixed .site-mark (top:28px) so
the title rides beside the wordmark at the very top of the scene,
floating horizontally at the same baseline. Horizontal padding on
the left is sized to clear the wordmark's right edge (~146px). */
visible through all four card landings and the grid rearrange.
Centered horizontally; positioned lower so the title anchors
visually to the cards below rather than floating at the top of
the viewport. Per-card counter (1/4, 2/4, …) now lives inside
each .layer-card rather than being slaved to scroll progress. */
.stack-title-bar {
position: absolute;
top: clamp(1.25rem, 2.8vh, 1.85rem);
top: clamp(5.5rem, 14vh, 10rem);
left: 0;
right: 0;
z-index: 20;
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 1.5rem;
padding-left: clamp(10rem, 12vw, 12rem);
padding-right: clamp(0.75rem, 2vw, 2.25rem);
text-align: center;
padding-inline: clamp(1rem, 4vw, 3rem);
pointer-events: none;
}
.stack-title {
margin: 0;
font-family: "Newsreader", Georgia, serif;
font-weight: 400;
font-size: clamp(1.4rem, 2.6vw, 2.2rem);
letter-spacing: -0.01em;
line-height: 1.2;
font-size: clamp(2rem, 3.6vw, 3rem);
letter-spacing: -0.015em;
line-height: 1.12;
color: var(--ink);
}
.stack-counter {
font-family: "Newsreader", Georgia, serif;
font-weight: 500;
font-size: clamp(1.1rem, 2vw, 1.6rem);
color: var(--ink-soft);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.stack-counter .sc-current {
color: var(--accent);
font-weight: 600;
transition: color 180ms var(--ease);
}
.stack-counter .sc-sep,
.stack-counter .sc-total { color: var(--ink-soft); }
/* LEFT COPY (visible only during grid phase) */
.copy-stage {
@ -1302,6 +1280,32 @@ html {
z-index: 2;
}
/* Per-card counter — lives in the top-right of each card-box and
reads "1 / 4" through "4 / 4". Paper-toned to contrast against
the sage/slate/clay/plum card backgrounds. */
.card-counter {
position: absolute;
top: clamp(1rem, 1.4vw, 1.4rem);
right: clamp(1rem, 1.4vw, 1.4rem);
font-family: "Newsreader", Georgia, serif;
font-weight: 500;
font-size: clamp(0.85rem, 1.05vw, 1.05rem);
letter-spacing: 0.04em;
color: rgba(255, 252, 247, 0.85);
font-variant-numeric: tabular-nums;
line-height: 1;
pointer-events: none;
z-index: 2;
}
.in-grid .card-counter {
/* Keep the counter visible in the grid phase but smaller so it
doesn't compete with the grid-label. */
top: clamp(0.5rem, 0.7vw, 0.8rem);
right: clamp(0.5rem, 0.7vw, 0.8rem);
font-size: clamp(0.6rem, 0.7vw, 0.75rem);
opacity: 0.7;
}
.in-grid .card-box {
/* 15% reduction from the original 20vw — matches the drop-phase shrink.
Also matches cellSize and targetW in bifrost.js (both are vw * 0.17). */
@ -2485,13 +2489,11 @@ html {
<section id="stack-scene" aria-label="The Fenja AI architecture">
<div class="stack-pin">
<!-- Title bar: rides along with the pin so it stays visible while
the reader scrolls through all 4 capability cards. The counter
ticks 1/4 → 4/4 as each layer lands (driven from bifrost.js). -->
the reader scrolls through all 4 capability cards. The per-card
counter now lives inside each .layer-card (see .card-counter
below), so the title here is a standalone centered lockup. -->
<div class="stack-title-bar" aria-hidden="true">
<h2 class="stack-title">The Fenja AI platform in four steps</h2>
<span class="stack-counter" id="stack-counter">
<span class="sc-current">1</span><span class="sc-sep">/</span><span class="sc-total">4</span>
</span>
</div>
<div class="layer-theatre">
@ -2518,6 +2520,7 @@ html {
<article class="layer-card" data-layer="0" aria-label="Layer 1: the AI">
<span class="card-eyebrow">The AI</span>
<div class="card-box">
<span class="card-counter" aria-hidden="true">1 / 4</span>
<span class="card-grid-label" aria-hidden="true">The AI</span>
<div class="card-content">
<h3 class="card-title">An <b>open-source</b> model, running on your <em>own hardware.</em></h3>
@ -2530,6 +2533,7 @@ html {
<article class="layer-card" data-layer="1" aria-label="Layer 2: Knowledge">
<span class="card-eyebrow">The Knowledge</span>
<div class="card-box">
<span class="card-counter" aria-hidden="true">2 / 4</span>
<span class="card-grid-label" aria-hidden="true">The Knowledge</span>
<div class="card-content">
<h3 class="card-title">The business context that makes <em>AI understand your world.</em></h3>
@ -2542,6 +2546,7 @@ html {
<article class="layer-card" data-layer="2" aria-label="Layer 3: Tools">
<span class="card-eyebrow">The Tools</span>
<div class="card-box">
<span class="card-counter" aria-hidden="true">3 / 4</span>
<span class="card-grid-label" aria-hidden="true">The Tools</span>
<div class="card-content">
<h3 class="card-title">How AI <b>acts</b> &mdash; not just what it <em>knows.</em></h3>
@ -2554,6 +2559,7 @@ html {
<article class="layer-card" data-layer="3" aria-label="Layer 4: Agents">
<span class="card-eyebrow">The Agents</span>
<div class="card-box">
<span class="card-counter" aria-hidden="true">4 / 4</span>
<span class="card-grid-label" aria-hidden="true">The Agents</span>
<div class="card-content">
<h3 class="card-title">Specialized AI agents <b>working together</b> around <em>real tasks.</em></h3>

View file

@ -211,39 +211,6 @@
color: var(--ink);
}
/* Practical viewing-experience note. Lives between the definitions
and the CTA so readers see it before committing to "Learn more".
Left-bordered callout in the editorial voice — same Newsreader as
the surrounding copy, tempered colour so it reads as guidance not
body text. */
.welcome-note {
max-width: 620px;
margin: 22px 0 8px 0;
padding: 14px 18px;
border-left: 3px solid var(--crimson);
background: rgba(138, 58, 47, 0.04);
font-family: "Newsreader", Georgia, "Times New Roman", serif;
font-weight: 400;
font-size: 17px;
line-height: 1.5;
color: var(--ink);
}
.welcome-note strong {
font-weight: 600;
color: var(--ink);
}
.welcome-note em {
font-style: italic;
font-weight: 700;
color: var(--crimson);
}
.welcome-note p {
margin: 0;
}
.welcome-note p + p {
margin-top: 8px;
}
.welcome-cta {
all: unset;
display: inline-flex;
@ -378,14 +345,6 @@
<h3 class="welcome-term"><em>Project Bifrost</em></h3>
<p class="welcome-def">The initiative created to ensure that Fenja AI is built not just for organisations like yours, but <em>with</em> you.</p>
</div>
<aside class="welcome-note" aria-label="Viewing recommendation">
<p>
This site is a <strong>desktop experience</strong>. It showcases many of the capabilities of our products, and is therefore not optimised for mobile — we recommend viewing it on a <em>desktop screen</em>.
</p>
<p>
Be mindful of the scrolling experience: <em>scroll gently</em> so you don&rsquo;t accidentally pass over a section before it finishes animating.
</p>
</aside>
<button type="button" class="welcome-cta" id="welcome-continue">
<svg class="c-icon" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.3"