Six scroll-bound scenes (hero, architecture stack, words fly-in,
aurora arc, treasure-map, join CTA) now live inside page-overview,
above the existing 23-headline timeline. The Europe map stays as a
static background that fades with scroll.
- protected/index.html: rewrote #page-overview only; timeline and
archive sections unchanged. Site-2 palette re-mapped to site-1
Nordic Editorial tokens, Fraunces to Newsreader, tokens scoped
to #page-overview.
- protected/timeline.js: dot-nav boots window.__bifrost.init()
on first Overview activation. Added .js class on documentElement.
- protected/bifrost.js (new): Lenis + ScrollTrigger wired to the
overview's internal scroller via scrollerProxy; drives Europe
map opacity on scroll.
- protected/vendor/{lenis,gsap,scrolltrigger}.min.js (new):
extracted from site-2's inlined vendor blobs; CSP-compliant.
- protected/fenja/illustrations/{community,council,pilot}.svg
(new): treasure-map stop images.
No changes to src/, server.js, deploy/, or public/. CSP stays
strict (script-src 'self'); zero inline scripts added. Auth gate
and session model untouched.
9.6 KiB
project-bifrost — merge notes
Site 2 (the 6-scene editorial scroll) has been merged into the Timeline's
Overview page. Timeline and Archive pages are untouched. Auth, CSP, and
all of src/, server.js, deploy/ are untouched.
Deploy — what to rsync where
From the project root on your laptop, copy the contents of this protected/
folder into project-bifrost/protected/ on the VPS, preserving structure:
protected/
├── index.html ← REPLACED (timeline + 6 Bifrost scenes)
├── timeline.js ← REPLACED (adds `.js` class + bifrost lazy-boot)
├── bifrost.js ← NEW
├── vendor/
│ ├── lenis.min.js ← NEW
│ ├── gsap.min.js ← NEW
│ └── scrolltrigger.min.js ← NEW
│ (d3-array, d3-geo, topojson-client, countries-110m are UNCHANGED; do not overwrite)
└── fenja/
└── illustrations/ ← NEW FOLDER
├── community.svg
├── council.svg
└── pilot.svg
After rsync, run the usual:
sudo chown -R fenja:fenja /opt/fenja
sudo systemctl restart fenja
sudo journalctl -u fenja -n 20
What changed, concretely
-
protected/index.html— rewrote#page-overviewonly.- Europe map (
.overview-globe) stays as absolute-positioned background. - Added
<div id="overview-scroll">— the new internal scroller. All six Bifrost scenes live inside it. Lenis + ScrollTrigger are wired to this element, neverwindow. - Removed the four editorial paragraphs + meta-strip ("Notes on a quiet inheritance" / entries / period / editor) — replaced by the Bifrost scenes in scroll order.
- Site-1 palette applied via
#page-overview { --ink: #383831; --paper: #faf6ee; ...; --aurora-*: site-1 Archival Pigments; }. Tokens are scoped to#page-overviewonly — they do not leak to the timeline or archive pages. - All
Frauncesreferences replaced withNewsreader. - Site-2's top-left brand mark, top-right meta chip, and right-edge progress rail removed (would clash with site 1's site-mark + dot-nav).
- Footer centre logo: swapped inlined Fenja SVG for
<img src="/fenja/fenja-wordmark-black.svg">. - Innovationsfonden remains the redrawn placeholder — swap when the real asset arrives.
- Illustration references: the big PNG data URIs in site 2's CSS were
replaced with three
url("/fenja/illustrations/*.svg")references pointing to the new illustration files. - Timeline section (
#page-timeline) and Archive section (#page-archive) are byte-identical to the previous version.
- Europe map (
-
protected/timeline.js— two changes only.- Added
document.documentElement.classList.add('js')at the top so site-2's.js .some-element { opacity: 0 }hide-before-reveal rules work. Harmless to timeline (nothing there uses.js). - Dot-nav click handler wrapped into
activatePage(targetId). WhentargetId === 'page-overview', it callswindow.__bifrost.init()after a 60ms delay (lets the page-activation transition start).init()is idempotent — subsequent activations just trigger aScrollTrigger.refresh(). - The "Read the editor's note" button's existing behaviour
(
document.querySelector('.dot-btn[data-target="page-overview"]').click()) routes throughactivatePagenow, so clicking the button both activates the overview and boots the bifrost scenes.
- Added
-
protected/bifrost.js(NEW) — single-file CSP-compliant module.- Exposes
window.__bifrost.init(). No auto-exec. - Creates a
new Lenis({ wrapper: scroller, content: scroller.firstElementChild, ... })scoped to#overview-scroll. - Registers a
ScrollTrigger.scrollerProxy(scroller, ...)and setsScrollTrigger.defaults({ scroller })so every ScrollTrigger the site-2 code registers targets the overview's internal scroll. - Europe-map opacity is driven from Lenis's scroll event:
scrollTop = 0→ 0.42 opacity (site-1's original "active" value)- between
0.2 × vhand0.8 × vh→ ramps to 0 - scrolling back up → fades back in
- The map does not rotate with scroll — it's static.
- Reduced-motion: short-circuits to "content visible, map at 0.42, no animations" and bails before registering any ScrollTriggers.
- All site-2 scene logic (HERO reveal, pinned 4-card stack, word fly-ins, aurora arc draw-in, treasure-map path draw-in + per-stop reveals, Join CTA + confirmation crossfade) transplanted verbatim.
- Exposes
-
protected/vendor/{lenis,gsap,scrolltrigger}.min.js(NEW) — the three libraries extracted verbatim from site 2's inlined vendor blobs. Served withdeferso they load and execute beforebifrost.jsandtimeline.js. (Script order at the bottom ofindex.html: lenis → gsap → scrolltrigger → bifrost → timeline.) -
protected/fenja/illustrations/{community,council,pilot}.svg(NEW) — the three illustrations you uploaded, renamed for site-2's slot names. ~1.7 MB each (SVG wrappers around embedded PNGs).
What I did NOT touch
src/(auth, db, mail, middleware, sessions) — byte-identical.server.js— byte-identical. The request-routing order, CSP headers, andrequireAuthgate are preserved exactly.deploy/— byte-identical.public/entrance.html,public/entrance.js— byte-identical.protected/fenja/colors_and_type.css— byte-identical. (The Bifrost scenes' tokens live inindex.html's own<style>block, scoped to#page-overview.)protected/fenja/fonts/*— byte-identical.protected/archive.html,protected/archive.js— byte-identical.protected/vendor/{d3-array,d3-geo,topojson-client,countries-110m}*— byte-identical. The new vendor libs are added alongside these.
Security posture
CSP is script-src 'self' per server.js. The merged page contains zero
inline <script> tags — verified. All JS is in separate .js files served
from /vendor/... or /.
Auth gate is unchanged: requireAuth runs before express.static(protected),
so the merged page is fully gated by the session cookie. The protected
directory's structure didn't change.
What to spot-check after deploy (from CHECKLIST.md section B + D)
Primary smoke test:
- Login flow still works:
/→ email → code → welcome → click "Learn more" button → timeline loads as before. - Timeline page unchanged: 23 headlines, globe rotates with wheel scroll, "Read the editor's note" button appears when scrolled to the end.
- Click "Read the editor's note" → overview activates. Europe map fades in on the right. Left column shows the new site-2 hero headline: "Secure & Sovereign AI, hosted where it belongs."
- Scroll inside the overview page — Europe map fades out between ~20 % and ~80 % of the first viewport scroll.
- Scroll back up — Europe map fades back in.
- Keep scrolling past the hero — the 4 architecture layer cards fall in, stack, then rearrange into a 2×2 grid (pinned scrub).
- Words fly in one by one.
- Project Bifrost aurora arc draws across, "Project Bifrost" headline appears.
- Treasure-map with 3 stops: Community / Advisory Council / Pilot Projects. Path draws in as you scroll. Each stop's illustration fades in on reach.
- Join CTA visible. Clicking "Join Project Bifrost" crossfades to the confirmation panel with 4 list items.
- Three-column footer at the very bottom: "Project Bifrost" (left), Fenja AI logo (centre), Innovationsfonden placeholder (right).
- Click dot-nav "Archive" — the 23-row archive table still works.
- Click dot-nav "Timeline" — back to the horizontal-scroll timeline.
- DevTools console: no CSP violations, no 404s. Check that
/bifrost.js,/vendor/lenis.min.js,/vendor/gsap.min.js,/vendor/scrolltrigger.min.js, and all three illustration SVGs return 200. - Response headers on
/timelinestill include the full six security headers (CSP withscript-src 'self', etc.).curl -I https://project-bifrost.fenja.ai/timelineshould look identical to before.
Known limitations
- Innovationsfonden logo is still a hand-drawn placeholder. Swap when
the real asset is ready — replace the SVG + span block in the
.foot-innovdiv inindex.html(search forfoot-innov). - Illustration file sizes are large (~1.7 MB each — SVG wrappers
around embedded PNGs). The gated page is slow to first-paint the
treasure-map if bandwidth is constrained. Consider exporting flat
PNGs later and swapping the
url(...)in the#page-overview { --illust-*: ... }block inindex.html. - Scene 2 (architecture stack) uses
pinwithpinType: 'transform'to work inside the nested scroller. This is the correct setting for overflow-auto containers but means the pinned card'spositionduring pinning is transform-based. If layout looks off on Firefox or Safari specifically, the first thing to investigate is the pin behaviour. - Resize behaviour: we consolidated site-2's two separate resize
listeners into one debounced
ScrollTrigger.refresh()220ms after the last resize event. If you see ScrollTrigger positions drifting on fast/rapid resize, the de-duper can be tuned inbifrost.js(setTimeout(scheduleRefresh, 220)). - Performance: inlining vendor libs in site 2 was ~135 KB. They're now
served as three separate cacheable files (~135 KB uncompressed, ~50 KB
gzipped). Nginx
gzipshould already cover them.