From 9b2c166b6c8dc2811399e6919d4cba84bb75a0e6 Mon Sep 17 00:00:00 2001 From: Arlind Ukshini Date: Fri, 24 Apr 2026 09:45:24 +0200 Subject: [PATCH] timeline + overview: pacing, labeling, and viewing guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Timeline page: - .page-sub: final sentence wrapped in .page-sub-accent and styled crimson so the rhetorical beat lifts off the block. - Scroll-to-begin hint is now toggled from applyScroll() against state.target, so it dismisses on first commit AND re-appears when the reader scrolls all the way back to the start. onWheel no longer hard-adds .hint-dismissed; the applyScroll toggle drives both ways. Overview page: - Hero scroll icon: size and presence bumped (2px line, 44px tall, 11px chevron, weight 600, color:var(--ink)) so it reads as a confident cue, not a whisper at the bottom of the hero. - Architecture scene gains a title bar pinned with the scene: "The Fenja AI platform in four steps" with a 1/4 → 4/4 counter driven by the scroll-trigger onUpdate. Bar is placed below the site-mark's fixed position so the two don't collide. - Dot-nav: dot size 5px → 10px (1.5px ring) for better click target + visual weight. Buttons for "Words" and "Participate" removed — the corresponding intermediate sections now map to their nearest surviving dot in bifrost.js's scroll-spy (words-scene → stack-scene, bifrost-meaning → bifrost). - Renames: "Hero" → "Fenja introduction", "Architecture" → "Capabilities", "Bifrost" → "Project Bifrost". - scrollTo() adds a per-scene SCENE_ANCHOR_OFFSET — stack-scene lands +2100px into its 5000px pin so the reader arrives on the fully stacked state instead of an empty pre-animation frame. Welcome step (public/entrance.html): - New .welcome-note callout between definitions and CTA advising desktop viewing and gentle scrolling so readers don't fly past animated sections before they've resolved. Co-Authored-By: Claude Opus 4.7 (1M context) --- protected/bifrost.js | 57 +++++++++++++++++++- protected/index.html | 121 ++++++++++++++++++++++++++++++++---------- protected/timeline.js | 15 +++--- public/entrance.html | 41 ++++++++++++++ 4 files changed, 198 insertions(+), 36 deletions(-) diff --git a/protected/bifrost.js b/protected/bifrost.js index ad45c59..d9f0e55 100644 --- a/protected/bifrost.js +++ b/protected/bifrost.js @@ -192,6 +192,18 @@ 'hero', 'stack-scene', 'words-scene', 'bifrost', 'bifrost-meaning', 'bifrost-join', ]; + // Not every scene has a dot in the nav — words-scene and bifrost-meaning + // are intermediate sections with no standalone dot. Map them to the + // nearest surviving upstream dot so the nav stays highlighted through + // those sections instead of going blank. + const sceneToDot = { + 'hero': 'hero', + 'stack-scene': 'stack-scene', + 'words-scene': 'stack-scene', + 'bifrost': 'bifrost', + 'bifrost-meaning': 'bifrost', + 'bifrost-join': 'bifrost-join', + }; let lastActiveScene = null; function updateActiveSceneDot() { if (typeof window.__setActiveDot !== 'function') return; @@ -212,7 +224,7 @@ } if (visibleId !== lastActiveScene) { lastActiveScene = visibleId; - window.__setActiveDot('page-overview', visibleId); + window.__setActiveDot('page-overview', sceneToDot[visibleId] || visibleId); } } lenis.on('scroll', updateActiveSceneDot); @@ -451,6 +463,11 @@ // 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', @@ -461,6 +478,22 @@ 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); + } + }, } }); @@ -1199,6 +1232,25 @@ * — see sceneOrder[] inside init(). * Special value "hero" scrolls to top (0). */ + // Per-scene scroll offsets in pixels. Added to the scene's offsetTop + // when a dot-nav button anchors to it, so the reader lands AFTER the + // scene's initial reveal rather than at an empty frame where the + // scrub hasn't advanced yet. + // + // stack-scene — the pin is 5000px long; Phase A (cards falling in) + // completes at ~0.42 of that (~2100px). Landing at +2100 puts + // the reader on the fully stacked state, just before the grid + // rearrange begins in Phase B. + // + // hero, bifrost, bifrost-join — short reveal tweens; offsetTop is + // already the correct landing spot so offset is 0. + const SCENE_ANCHOR_OFFSET = { + 'hero': 0, + 'stack-scene': 2100, + 'bifrost': 0, + 'bifrost-join': 0, + }; + function scrollTo(sceneId) { if (!scrollerEl) return; // init() hasn't run yet — ignore const target = document.getElementById(sceneId); @@ -1207,7 +1259,8 @@ // "hero" is the first scene and sits at scrollTop 0. Scrolling to // the scene element directly works in most cases but produces a tiny // non-zero offset (padding / border) — hard-code 0 for hero. - const scrollY = sceneId === 'hero' ? 0 : target.offsetTop; + const base = sceneId === 'hero' ? 0 : target.offsetTop; + const scrollY = base + (SCENE_ANCHOR_OFFSET[sceneId] || 0); if (lenisInstance && typeof lenisInstance.scrollTo === 'function') { // Lenis does the smooth animation. `immediate: false` uses the diff --git a/protected/index.html b/protected/index.html index f69c113..3e70b6d 100644 --- a/protected/index.html +++ b/protected/index.html @@ -140,6 +140,12 @@ transition: opacity 520ms var(--ease), transform 520ms var(--ease); } .page-sub em { font-style: italic; font-weight: 700; color: var(--ink); } + /* Final sentence of the front-matter is pulled up in crimson so the + rhetorical question reads as the emphatic beat of the block. Applied + to the whole sentence, not just the on "Washington?", so the + colour change cues the reader's eye before they reach the italic. */ + .page-sub-accent, + .page-sub-accent em { color: var(--crimson); } /* Once the timeline has been advanced, the front matter steps aside */ .page-timeline.is-scrolled .page-title, .page-timeline.is-scrolled .page-sub { @@ -252,19 +258,19 @@ display: flex; align-items: center; justify-content: center; } .dot-btn .dot { - width: 5px; height: 5px; + width: 10px; height: 10px; border-radius: 50%; background: transparent; - box-shadow: inset 0 0 0 1px var(--ink-dim); /* outlined ring, default */ + box-shadow: inset 0 0 0 1.5px var(--ink-dim); /* outlined ring, default */ transition: background var(--dur) var(--ease), box-shadow var(--dur) var(--ease); } .dot-btn:hover .dot { - box-shadow: inset 0 0 0 1px var(--ink); + box-shadow: inset 0 0 0 1.5px var(--ink); } .dot-btn.is-active .dot { background: var(--ink); /* filled ink, active */ - box-shadow: inset 0 0 0 1px var(--ink); + box-shadow: inset 0 0 0 1.5px var(--ink); } /* Label tooltip — rises above the dot on hover or keyboard focus. */ @@ -1002,30 +1008,39 @@ html { .scroll-hint { display: inline-flex; align-items: center; - gap: 0.6rem; + gap: 0.7rem; text-transform: uppercase; letter-spacing: 0.22em; - font-size: var(--step-sm); - color: var(--ink-soft); + /* Bumped from --step-sm for legibility. The hint is the primary + "what next?" cue at the bottom of the hero and needs to read + confidently, not whisper. */ + font-size: calc(var(--step-sm) * 1.15); + font-weight: 600; + /* Switched from --ink-soft to --ink so the icon reads against the + paper background. The hint animation still breathes opacity so + it doesn't shout. */ + color: var(--ink); } - /* Arrow reoriented to point DOWN — a vertical 1px line with a chevron + /* Arrow reoriented to point DOWN — a vertical line with a chevron cap at the bottom. Animation moved from translateX to translateY so - the hint visually "drops" downward, matching its meaning. */ + the hint visually "drops" downward, matching its meaning. Weight + and length both bumped (1px → 2px, 28px → 44px) so the icon has + more visual presence; chevron arms thickened and enlarged to match. */ .scroll-hint .arrow { - width: 1px; height: 28px; background: currentColor; + width: 2px; height: 44px; background: currentColor; position: relative; animation: hint 2.2s ease-in-out infinite; } .scroll-hint .arrow::after { - content: ""; position: absolute; bottom: -1px; left: -3px; - width: 7px; height: 7px; - border-right: 1px solid currentColor; - border-bottom: 1px solid currentColor; + content: ""; position: absolute; bottom: -1px; left: -5px; + width: 11px; height: 11px; + border-right: 2px solid currentColor; + border-bottom: 2px solid currentColor; transform: rotate(45deg); } @keyframes hint { - 0%, 100% { transform: translateY(0); opacity: 0.5; } - 50% { transform: translateY(6px); opacity: 1; } + 0%, 100% { transform: translateY(0); opacity: 0.75; } + 50% { transform: translateY(8px); opacity: 1; } } /* ============================================================ @@ -1055,6 +1070,55 @@ html { max-width: none; } + /* ───────── 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 offset clears the fixed .site-mark (top:28px, width:118px) + so the title sits below the wordmark instead of crashing into it. + Horizontal padding on the left is bumped to push past the wordmark's + right edge; the right side uses the same edge token as the theatre. */ + .stack-title-bar { + position: absolute; + top: clamp(3.75rem, 7vh, 5.25rem); + left: 0; + right: 0; + z-index: 20; + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 1.5rem; + padding-left: clamp(10.5rem, 12vw, 12.5rem); + padding-right: clamp(0.75rem, 2vw, 2.25rem); + 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; + 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 { position: absolute; @@ -2321,7 +2385,7 @@ html {
When AI runs Europe, who runs the AI?
We’ve spent years building data and AI across Denmark and Europe, watching one dependency harden after another. AI is different. The United States has made that clear. China has made that clear. You cannot stand strong in this century on AI you do not control — and for the first time in a generation, Europe has both the reason and the moment to build its own. The window is closing faster than most realise. It is open now. It will not be open long.

- As AI moves into our hospitals, our courts, our defence, our schools — can we afford for the switch to sit in Washington? + As AI moves into our hospitals, our courts, our defence, our schools — can we afford for the switch to sit in Washington?
@@ -2420,6 +2484,15 @@ html { ============================================================ -->
+ +
@@ -2807,23 +2880,15 @@ html { - -
+