customer-presentation/protected/index.html
2026-04-23 14:54:25 +02:00

2713 lines
89 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>A Catalog of Sovereignty — 20222026</title>
<link rel="stylesheet" href="/fenja/colors_and_type.css" />
<script src="/vendor/d3-array.min.js"></script>
<script src="/vendor/d3-geo.min.js"></script>
<script src="/vendor/topojson-client.min.js"></script>
<style>
@view-transition { navigation: auto; }
:root{
--paper: #faf6ee;
--paper-high: #fffcf7;
--paper-mid: #f4efe2;
--paper-low: #ece5d2;
--ink: #383831;
--ink-soft: #5f5e5e;
--ink-dim: #8a887f;
--copper: #6d8c7c; /* copper green */
--ochre: #c29d59;
--terracotta: #b96b58;
--crimson: #8a3a2f; /* deep crimson */
--ease: cubic-bezier(0.2, 0, 0, 1);
--dur: 240ms;
}
*, *::before, *::after { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
height: 100%;
background: var(--paper);
color: var(--ink);
font-family: "Manrope", system-ui, sans-serif;
overflow: hidden;
-webkit-font-smoothing: antialiased;
}
body {
/* Subtle tonal shift across the entire surface — not a gradient on chrome,
just the paper catching light. */
background:
radial-gradient(1200px 800px at 18% 45%, #fffcf7 0%, var(--paper) 55%, #f4efe2 100%);
view-transition-name: paper;
}
/* ───── Page scaffolding ───── */
.page {
position: fixed; inset: 0;
opacity: 0;
pointer-events: none;
transition: opacity 380ms var(--ease);
will-change: opacity;
}
.page.is-active {
opacity: 1;
pointer-events: auto;
}
/* ───────── Site wordmark — top-left masthead ───────── */
.site-mark {
position: fixed;
top: 28px;
left: 36px;
width: 118px;
height: auto;
z-index: 50;
pointer-events: none;
opacity: 0.85;
}
@media (max-width: 720px) {
.site-mark { width: 90px; top: 20px; left: 22px; }
}
/* Page overline title — large, sits lower on the front matter so it reads */
.page-title {
position: absolute;
left: 80px; top: 42vh;
font-family: "Newsreader", Georgia, serif;
font-weight: 400;
font-size: 54px;
letter-spacing: -0.022em;
color: var(--ink);
line-height: 1.08;
z-index: 15;
max-width: 820px;
text-wrap: pretty;
opacity: 1;
transition: opacity 520ms var(--ease), transform 520ms var(--ease);
}
.page-title em {
font-style: italic; font-weight: 700;
}
.page-title + .page-sub {
position: absolute;
left: 80px; top: calc(42vh + 220px);
max-width: 560px;
font-family: "Newsreader", Georgia, serif;
font-style: italic;
font-size: 19px;
line-height: 1.5;
color: var(--ink-soft);
z-index: 15;
opacity: 1;
transition: opacity 520ms var(--ease), transform 520ms var(--ease);
}
/* Once the timeline has been advanced, the front matter steps aside */
.page-timeline.is-scrolled .page-title,
.page-timeline.is-scrolled .page-sub {
opacity: 0;
pointer-events: none;
transform: translateY(-12px);
}
/* ───────── Dot-nav ─────────
5px dots, filled when active, outlined ring otherwise. Labels hidden
by default and appear as a floating tooltip above the dot on hover.
The `.dot-nav-tray` (bottom paper-fade behind the nav) is still
declared but suppressed on #page-overview so the S6 footer reads
as a hard terminus — see the #page-overview.is-active rule further
down. */
.dot-nav-tray {
position: fixed;
left: 0; right: 0; bottom: 0;
height: 110px;
z-index: 35;
pointer-events: none;
background: linear-gradient(to bottom,
rgba(250,246,238,0) 0%,
rgba(250,246,238,0.88) 45%,
rgba(250,246,238,0.98) 100%);
transition: opacity var(--dur) var(--ease);
}
/* Fade the tray away on the Overview page so the S6 footer meets the
bottom of the viewport cleanly without a paper wash over the logos. */
body:has(#page-overview.is-active) .dot-nav-tray { opacity: 0; }
.dot-nav {
position: fixed;
bottom: 36px; left: 50%;
transform: translateX(-50%);
display: flex; gap: 22px;
z-index: 40;
}
.dot-btn {
all: unset;
position: relative;
padding: 10px; /* invisible hit target — the dot itself is 5px */
cursor: pointer;
display: flex; align-items: center; justify-content: center;
}
.dot-btn .dot {
width: 5px; height: 5px;
border-radius: 50%;
background: transparent;
box-shadow: inset 0 0 0 1px 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);
}
.dot-btn.is-active .dot {
background: var(--ink); /* filled ink, active */
box-shadow: inset 0 0 0 1px var(--ink);
}
/* Label tooltip — rises above the dot on hover or keyboard focus. */
.dot-btn .label {
position: absolute;
bottom: calc(100% - 6px); /* sit just above the hit area */
left: 50%;
transform: translate(-50%, 4px);
background: #fffcf7;
color: var(--ink);
font-size: 10.5px;
letter-spacing: 0.22em;
text-transform: uppercase;
font-weight: 500;
font-family: "Manrope", system-ui, sans-serif;
padding: 7px 11px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
box-shadow:
0 0 0 0.5px rgba(56,56,49,0.08),
0 10px 18px -10px rgba(56,56,49,0.2),
0 2px 6px -3px rgba(56,56,49,0.08);
transition: opacity var(--dur) var(--ease),
transform var(--dur) var(--ease);
}
.dot-btn:hover .label,
.dot-btn:focus-visible .label {
opacity: 1;
transform: translate(-50%, 0);
}
/* ───────── Globe ghost ───────── */
.globe-wrap {
position: absolute;
/* 15% larger than the original 58vw ≈ 66.7vw.
Shifted ~20% of its width toward the page center: from left:-8%
to roughly left:+5%. */
left: 5%; top: 0;
width: 66.7vw;
height: 100%;
pointer-events: none;
z-index: 1;
opacity: 0.5;
transition: opacity 280ms var(--ease);
/* Mask top and fade the bottom third so the timeline rests on clean paper */
-webkit-mask-image: linear-gradient(to bottom,
transparent 0%, #000 22%, #000 58%, transparent 72%);
mask-image: linear-gradient(to bottom,
transparent 0%, #000 22%, #000 58%, transparent 72%);
}
.globe-wrap svg {
width: 100%; height: 100%;
display: block;
}
/* ───────── Timeline ───────── */
.timeline-viewport {
position: absolute; inset: 0;
z-index: 5;
}
.timeline-track {
position: absolute;
top: 0; left: 0; height: 100%;
will-change: transform;
transform: translate3d(0,0,0);
display: flex; align-items: center;
padding: 0 120px;
--spine-y: 54%;
}
.spine {
position: absolute;
top: var(--spine-y); left: 0;
height: 1px;
width: 100%;
background: linear-gradient(to right,
transparent 0,
rgba(56,56,49,0.22) 60px,
rgba(56,56,49,0.22) calc(100% - 60px),
transparent);
z-index: 2;
}
.year-tick {
position: absolute;
top: var(--spine-y);
transform: translate(-50%, -50%);
display: flex; flex-direction: column; align-items: center;
gap: 10px;
z-index: 3;
color: var(--ink-dim);
}
.year-tick::before {
content: "";
display: block;
width: 1px; height: 28px;
background: rgba(56,56,49,0.28);
}
.year-tick .y {
font-family: "Newsreader", Georgia, serif;
font-style: italic;
font-size: 22px;
color: var(--ink-soft);
letter-spacing: 0;
font-weight: 400;
}
/* Card */
.evt {
position: absolute;
width: 640px;
padding: 32px 40px 36px;
/* Two-column editorial layout: headline on the left (15% wider),
body paragraph + source stacked on the right. Keeps cards shorter
so they don't clip below the timeline spine. */
display: grid;
grid-template-columns: 1.15fr 0.85fr;
grid-template-areas:
"head body"
"head source";
column-gap: 32px;
row-gap: 14px;
background: var(--paper-high);
/* Tonal surface shifts instead of 1px borders */
box-shadow:
0 0 0 0.5px rgba(56,56,49,0.05),
0 14px 28px -18px rgba(56,56,49,0.18),
0 2px 6px -3px rgba(56,56,49,0.08);
color: var(--ink);
opacity: 0;
/* Pop-in: small scale + downward lift for a more tactile entrance */
transform: translateY(28px) scale(0.96);
transform-origin: center top;
}
.evt.above {
transform: translateY(-28px) scale(0.96);
transform-origin: center bottom;
}
/* Only animate after first paint — prevents the initial card from
getting stuck at opacity 0 while the transition starts pre-layout. */
.evt.can-animate {
transition:
opacity 640ms var(--ease),
transform 640ms cubic-bezier(0.16, 1, 0.3, 1),
box-shadow 320ms var(--ease);
}
.evt.is-near {
opacity: 1;
transform: translateY(0) scale(1);
}
.evt.above { bottom: calc(100% - var(--spine-y) + 48px); }
.evt.below { top: calc(var(--spine-y) + 48px); }
/* Connector from card to spine */
.evt::after {
content: "";
position: absolute;
left: 52px;
width: 1px;
background: rgba(56,56,49,0.28);
}
.evt.above::after { top: 100%; height: 40px; }
.evt.below::after { bottom: 100%; height: 40px; }
/* Node on the spine — tiny dot */
.evt .node {
position: absolute;
left: 46px;
width: 13px; height: 13px;
border-radius: 50%;
background: var(--paper-high);
z-index: 1;
}
.evt.above .node { top: calc(100% + 40px - 6px); }
.evt.below .node { bottom: calc(100% + 40px - 6px); }
.evt .node::after {
content: "";
position: absolute;
inset: 3.5px;
border-radius: 50%;
background: var(--ink-soft);
}
.evt[data-accent="copper"] .node::after { background: var(--copper); }
.evt[data-accent="ochre"] .node::after { background: var(--ochre); }
.evt[data-accent="terracotta"] .node::after { background: var(--terracotta); }
.evt[data-accent="crimson"] .node::after { background: var(--crimson); }
.evt .tag-row {
display: flex; gap: 12px; align-items: center;
margin-bottom: 8px;
}
.evt .date {
font-family: "Newsreader", Georgia, serif;
font-style: italic;
font-size: 13px;
color: var(--ink-soft);
letter-spacing: 0;
}
.evt .kind {
font-size: 9px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink-dim);
font-weight: 600;
}
.evt[data-accent="copper"] .kind { color: var(--copper); }
.evt[data-accent="ochre"] .kind { color: var(--ochre); }
.evt[data-accent="terracotta"] .kind { color: var(--terracotta); }
.evt[data-accent="crimson"] .kind { color: var(--crimson); }
.evt h3 {
grid-area: head;
align-self: start;
font-family: "Newsreader", Georgia, serif;
font-weight: 400;
font-size: 41px;
line-height: 1.12;
letter-spacing: -0.015em;
color: var(--ink);
margin: 0;
text-wrap: pretty;
}
/* Bold-italic emphasis in headlines gets the red accent so each card
carries a consistent touch of colour while staying editorial. */
.evt h3 em {
font-style: italic;
font-weight: 700;
color: var(--crimson);
}
.evt p {
grid-area: body;
margin: 0;
font-size: 16px;
line-height: 1.5;
color: var(--ink-soft);
text-wrap: pretty;
align-self: start;
}
.evt .source {
grid-area: source;
align-self: end;
margin-top: 0;
font-size: 9.5px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--ink-dim);
font-weight: 500;
}
/* ───────── Continue button ─────────
Right-side anchor, vertically centered so readers crossing the
timeline can't miss it. Larger editorial block with a circular
icon on the left and the label on the right. Breath animation
translates horizontally only; `translateY(-50%)` is applied via
`top: 50%` centering and kept stable across keyframes. */
.continue-btn {
all: unset;
position: absolute;
right: 72px;
top: 50%;
display: inline-flex;
align-items: center;
gap: 28px;
padding: 32px 44px;
background: var(--paper-high);
color: var(--ink);
cursor: pointer;
z-index: 30;
opacity: 0;
transform: translate(36px, -50%);
pointer-events: none;
box-shadow:
0 0 0 0.5px rgba(56,56,49,0.08),
0 26px 48px -22px rgba(56,56,49,0.28),
0 3px 10px -4px rgba(56,56,49,0.10);
transition:
opacity 520ms var(--ease),
transform 520ms var(--ease),
box-shadow var(--dur) var(--ease),
background var(--dur) var(--ease);
}
.continue-btn.is-visible {
opacity: 1;
transform: translate(0, -50%);
pointer-events: auto;
animation: continue-breath 2800ms cubic-bezier(0.2, 0, 0, 1) infinite;
}
@keyframes continue-breath {
0%, 100% { transform: translate(0, -50%); }
50% { transform: translate(6px, -50%); }
}
.continue-btn:hover {
background: #fffbf2;
box-shadow:
0 0 0 0.5px rgba(56,56,49,0.12),
0 32px 56px -22px rgba(56,56,49,0.34),
0 4px 12px -5px rgba(56,56,49,0.12);
}
.continue-btn .c-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 72px;
height: 72px;
color: var(--crimson);
flex: 0 0 auto;
transition: transform var(--dur) var(--ease);
}
.continue-btn .c-icon svg {
width: 100%;
height: 100%;
display: block;
}
.continue-btn:hover .c-icon {
transform: translateX(4px);
}
.continue-btn .c-label {
font-family: "Newsreader", Georgia, serif;
font-size: 36px;
font-weight: 400;
letter-spacing: -0.015em;
color: var(--ink);
line-height: 1.08;
max-width: 13ch;
}
.continue-btn .c-label em {
font-style: italic;
font-weight: 700;
color: var(--crimson);
}
/* ───────── Overview page ───────── */
/* Topography layer — concentric ring pattern, parallax-scrolled,
sitting behind the Europe map. Reads as a visual sibling of the
entrance page's "currents" pattern but rotated and repositioned so
it doesn't look like a duplicate. The SVG contents are drawn at
runtime by drawTopography() in bifrost.js.
Transform is driven by JS from Lenis's scroll position (parallax
speed 0.15× — very slow). z-index 0 so it sits behind the map
(z-index 1) and content (z-index 2+). */
.overview-topography {
position: absolute; inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
opacity: 0;
transition: opacity 900ms var(--ease);
}
.overview-topography svg {
position: absolute;
/* Offset to the opposite corner of the entrance-page currents
(which sit top-right). Here we anchor bottom-left and extend
well beyond the viewport so parallax translation never reveals
an edge. */
left: -20vw;
top: -10vh;
width: 140vw;
height: 140vh;
display: block;
/* Rotate 40° so the rings don't read as an exact copy of the
entrance's pattern; the viewer registers this as "related but
distinct". */
transform: rotate(40deg);
transform-origin: 50% 50%;
will-change: transform;
}
.page-overview.is-active .overview-topography {
opacity: 1;
}
/* Globe background behind the overview — same SVG style as the timeline's,
but centered on Europe. It begins at the timeline's size/position so
that when the page is entered, the CSS transition zooms it into place. */
.overview-globe {
position: absolute; inset: 0;
pointer-events: none;
z-index: 1;
overflow: hidden;
/* Soft fade at top + bottom so the paper reads as the surface, not the sphere */
-webkit-mask-image: linear-gradient(to bottom,
transparent 0%, #000 10%, #000 82%, transparent 100%);
mask-image: linear-gradient(to bottom,
transparent 0%, #000 10%, #000 82%, transparent 100%);
}
.overview-globe svg {
position: absolute;
left: 65%; top: 55%;
/* Smaller than before — 92vmax is enough to show Europe at the framing we want,
and it paints fast enough not to block the page fade-in. */
width: 92vmax;
height: 92vmax;
max-width: none;
transform: translate(-50%, -50%) scale(0.78);
transform-origin: 50% 50%;
opacity: 0.22;
transition:
transform 1200ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 900ms var(--ease);
}
/* When the overview page becomes active, zoom onto Europe */
.page-overview.is-active .overview-globe svg {
transform: translate(-50%, -50%) scale(1.35);
opacity: 0.42;
}
.overview {
position: absolute; inset: 0;
overflow: auto;
padding: 160px 80px 180px;
scrollbar-width: thin;
scrollbar-color: rgba(56,56,49,0.18) transparent;
z-index: 5;
}
.overview .col-wrap {
max-width: 1280px; margin: 0 auto;
/* Text on the left; globe occupies the right half of the spread. */
display: grid;
grid-template-columns: minmax(420px, 560px) 1fr;
column-gap: 80px;
row-gap: 24px;
align-items: start;
}
.overview h1 {
grid-column: 1;
font-family: "Newsreader", Georgia, serif;
font-weight: 400;
font-size: 56px;
line-height: 1.05;
letter-spacing: -0.025em;
margin: 0 0 18px 0;
text-wrap: balance;
color: var(--ink);
}
.overview h1 em {
font-style: italic; font-weight: 700;
}
.overview .lede {
grid-column: 1;
font-family: "Newsreader", Georgia, serif;
font-style: italic;
font-size: 20px;
line-height: 1.5;
color: var(--ink-soft);
max-width: 780px;
margin-bottom: 28px;
}
.overview .rule {
grid-column: 1;
height: 1px;
background: rgba(56,56,49,0.18);
margin: 14px 0 8px 0;
}
.overview p {
grid-column: 1;
font-size: 15px;
line-height: 1.7;
color: var(--ink);
margin: 0 0 14px 0;
text-wrap: pretty;
}
.overview p.drop::first-letter {
font-family: "Newsreader", Georgia, serif;
font-weight: 700;
font-size: 58px;
line-height: 0.9;
float: left;
padding: 4px 10px 0 0;
color: var(--ink);
}
.overview .meta-strip {
grid-column: 1;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 28px 40px;
margin-top: 32px;
padding-top: 24px;
border-top: 0;
background:
linear-gradient(to right, rgba(56,56,49,0.18), rgba(56,56,49,0.18)) top / 100% 1px no-repeat;
}
.overview .meta-strip .cell {
display: flex; flex-direction: column; gap: 8px;
}
.overview .meta-strip .k {
font-size: 10.5px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--ink-dim);
font-weight: 600;
}
.overview .meta-strip .v {
font-family: "Newsreader", Georgia, serif;
font-size: 22px;
letter-spacing: -0.01em;
color: var(--ink);
}
.overview .meta-strip .v em { font-style: italic; font-weight: 700; }
/* Short-viewport safety: collapse the page-title block so cards never collide */
@media (max-height: 620px) {
.page-title { font-size: 36px; max-width: 640px; top: 38vh; }
.page-title + .page-sub { font-size: 16px; top: calc(38vh + 160px); }
}
@media (max-height: 500px) {
.page-title { display: none; }
.page-sub { display: none; }
}
.overview::-webkit-scrollbar { width: 6px; }
.overview::-webkit-scrollbar-thumb {
background: rgba(56,56,49,0.18);
border-radius: 3px;
}
/* ============================================================
BIFROST OVERLAY — scenes inside the Overview page
============================================================ */
/* Internal scroller — sits inside #page-overview. Hosts the six
scenes. Scrolls vertically. The Europe map (overview-globe)
stays as fixed-position background behind it. */
#overview-scroll {
position: absolute;
inset: 0;
overflow-y: auto;
overflow-x: hidden;
z-index: 5;
scrollbar-width: thin;
scrollbar-color: rgba(56,56,49,0.18) transparent;
}
#overview-scroll::-webkit-scrollbar { width: 6px; }
#overview-scroll::-webkit-scrollbar-thumb {
background: rgba(56,56,49,0.18);
border-radius: 3px;
}
/* Site-1 overview-globe base rule kept; bifrost.js drives opacity
inline after init, so transition is suppressed by JS. Until JS
runs, CSS handles the 900ms fade-in on page activation. */
/* Hero scene anchored over the Europe map. The map's right-of-
centre framing is preserved from site 1 (overview-globe CSS at
left:65%, top:55%, scale 1.35 when .is-active). The hero text
lives in the left column; the map fills the right ~2/3. */
#page-overview #hero {
min-height: 100vh;
padding-inline: var(--edge);
display: grid;
align-items: center;
position: relative;
/* Asymmetric block padding — the larger padding-bottom shifts the
centered hero content upward, leaving breathing room between the
hero-foot row and the dot-nav at the bottom of the viewport. */
padding-top: clamp(1.5rem, 3vh, 2.5rem);
padding-bottom: clamp(6rem, 16vh, 11rem);
}
#page-overview #hero .hero-wrap {
/* Constrain to the left column so Europe is visible to its right. */
max-width: 62ch;
/* Zeroed: wrap padding-top was adding down-drift inside the centered
container, pushing the hero-foot toward the dot-nav. Vertical
position is now controlled entirely by #hero's asymmetric padding. */
padding-top: 0;
}
/* Make sure scenes don't accidentally inherit `main { position: relative }` */
#overview-scroll > section { position: relative; z-index: 2; }
/* ============================================================
BIFROST SCENES — tokens (scoped to #page-overview only, so
they never leak to the timeline page).
Palette reconciled with site 1's Nordic Editorial system.
============================================================ */
#page-overview {
--ink: #383831; /* site 1 --on-surface (charcoal slate) */
--ink-soft: #5f5e5e; /* site 1 --on-surface-variant */
--ink-mute: #8a887f; /* site 1 --on-surface-muted */
--ink-faint: #ddd6c3; /* site 1 --surface-container-highest */
--paper: #faf6ee; /* site 1 --background */
--paper-2: #f6f2e8; /* site 1 --surface-container-low */
--paper-3: #efeadc; /* site 1 --surface-container */
--accent: #b96b58; /* site 1 --pigment-terracotta */
--ring: #6d8c7c; /* site 1 --pigment-copper */
/* aurora gradient — site-1 Archival Pigments, kept exclusive to Scene 4 */
--aurora-1: #c29d59; /* site 1 --pigment-ochre */
--aurora-2: #b96b58; /* site 1 --pigment-terracotta */
--aurora-3: #5a6d83; /* site 1 --pigment-indigo */
--aurora-4: #8d7a85; /* site 1 --pigment-heather */
--type-body: "Manrope", ui-sans-serif, system-ui, sans-serif;
--type-display: "Newsreader", Georgia, serif;
--step-hero: clamp(2.4rem, 6.2vw, 5.4rem);
--step-xl: clamp(1.8rem, 4.8vw, 4rem);
--step-lg: clamp(1.35rem, 3vw, 2.2rem);
--step-md: clamp(1rem, 1.4vw, 1.15rem);
--step-sm: clamp(0.85rem, 1vw, 0.95rem);
--edge: clamp(1.5rem, 4vw, 4rem);
}
/* ============================================================
TOKENS
============================================================ */
*, *::before, *::after { box-sizing: border-box; }
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
scroll-behavior: auto; /* Lenis handles it */
}
/* Paper-grain noise overlay for tactile warmth */
/* Faint contour lines in the background for the whole page — Nordic-map motif */
/* ============================================================
LAYOUT PRIMITIVES
============================================================ */
.scene {
position: relative;
min-height: 100vh;
padding-inline: var(--edge);
display: grid;
align-items: center;
}
/* ============================================================
SCENE 1 — HERO
============================================================ */
#hero {
min-height: 100vh;
/* Reduced from clamp(7rem, 14vh, 11rem) to pull the hero text up
into the upper half of the viewport. The original value centered
the text too low when measured against the site mark at top. */
padding-top: clamp(3.5rem, 7vh, 5.5rem);
grid-template-columns: 1fr;
align-items: start;
}
.hero-wrap {
display: grid;
grid-template-columns: 1fr;
gap: clamp(2rem, 6vw, 4rem);
align-items: end;
max-width: 1600px;
width: 100%;
margin-inline: auto;
padding-top: clamp(2rem, 6vh, 4rem);
}
/* Hide the hero on first paint while JS is booting so it doesn't
flash in raw form during the page-activation transition. The
Bifrost init fades it in once ScrollTriggers are wired up. */
.js .hero-wrap { opacity: 0; }
.eyebrow {
font-size: var(--step-sm);
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink-mute);
display: inline-flex;
align-items: center;
gap: 0.75rem;
margin-bottom: clamp(1.4rem, 4vh, 2.4rem);
font-weight: 500;
}
.eyebrow::before {
content: "";
width: 28px; height: 1px;
background: var(--ink-mute);
}
.hero-title {
font-family: var(--type-display);
font-weight: 330;
font-size: var(--step-hero);
line-height: 1.02;
letter-spacing: -0.03em;
color: var(--ink);
margin: 0;
max-width: 22ch;
}
.hero-title em {
font-style: italic;
font-weight: 340;
color: var(--accent);
}
.hero-lede {
max-width: 46ch;
font-size: var(--step-lg);
font-weight: 300;
line-height: 1.35;
color: var(--ink-soft);
margin-top: clamp(1.5rem, 4vh, 2.5rem);
letter-spacing: -0.01em;
}
/* Hero foot row — lives INSIDE the left column at the bottom of the
paragraph block. Displays "Supported by Innovationsfonden" on the
left and the scroll-down indicator on the right, sharing a single
baseline. The row sits immediately after the lede paragraph. */
.hero-foot {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 1.5rem;
margin-top: clamp(2rem, 5vh, 3.5rem);
font-size: var(--step-sm);
color: var(--ink-mute);
letter-spacing: 0.04em;
}
.support {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: var(--step-sm);
text-transform: uppercase;
letter-spacing: 0.18em;
}
.support svg { height: 18px; width: auto; }
.scroll-hint {
display: inline-flex;
align-items: center;
gap: 0.6rem;
text-transform: uppercase;
letter-spacing: 0.22em;
font-size: var(--step-sm);
color: var(--ink-soft);
}
/* Arrow reoriented to point DOWN — a vertical 1px line with a chevron
cap at the bottom. Animation moved from translateX to translateY so
the hint visually "drops" downward, matching its meaning. */
.scroll-hint .arrow {
width: 1px; height: 28px; 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;
transform: rotate(45deg);
}
@keyframes hint {
0%, 100% { transform: translateY(0); opacity: 0.5; }
50% { transform: translateY(6px); opacity: 1; }
}
/* ============================================================
SCENE 2 — ARCHITECTURE (pinned, scrubbed)
4 layer-cards that fall in, stack with an offset revealing
each previous layer's bottom strip, then rearrange into a
2x2 grid on the right while explanatory copy appears on
the left.
============================================================ */
#stack-scene { position: relative; }
.stack-pin {
position: relative;
height: 100vh;
padding-inline: var(--edge);
max-width: none;
margin-inline: auto;
display: grid;
place-items: center;
padding-top: clamp(6rem, 11vh, 8.5rem); /* keep clear of brand mark */
}
/* Theatre — holds cards absolutely positioned. GSAP drives positions. */
.layer-theatre {
position: relative;
width: 100%;
height: 100%;
max-width: none;
}
/* LEFT COPY (visible only during grid phase) */
.copy-stage {
position: absolute;
left: 0;
top: 0;
bottom: 0; /* full theatre height so copy-layer can vertically center */
width: 42%;
max-width: 46ch;
z-index: 10;
pointer-events: none;
}
.copy-layer {
position: absolute;
left: 0;
right: 0;
top: 50%; /* vertically center */
will-change: opacity, transform;
}
.js .copy-layer { opacity: 0; }
.copy-layer h2 {
font-family: var(--type-display);
font-weight: 340;
font-size: var(--step-xl);
line-height: 1.04;
letter-spacing: -0.025em;
color: var(--ink);
margin: 0 0 1rem;
}
.copy-layer h2 em { font-style: italic; color: var(--accent); font-weight: 400; }
.copy-layer h2 strong { font-weight: 600; font-style: normal; }
.copy-layer p {
font-size: var(--step-lg);
font-weight: 300;
line-height: 1.35;
color: var(--ink-soft);
margin: 0;
max-width: 38ch;
letter-spacing: -0.005em;
}
.copy-layer .tag {
display: inline-flex;
align-items: center;
gap: 0.6rem;
font-size: var(--step-sm);
text-transform: uppercase;
letter-spacing: 0.22em;
color: var(--ink-mute);
margin-bottom: 1rem;
font-weight: 500;
}
.copy-layer .tag::before {
content: "";
width: 20px; height: 1px;
background: currentColor;
}
/* -------- Layer cards -------- */
.layer-card {
position: absolute;
/* 7.5% margin on each side = 15% total width reduction from the
original edge-to-edge layout. */
left: 7.5%;
right: 7.5%;
top: 50%;
width: auto;
transform: translateY(-50%);
transform-origin: center center;
will-change: transform, opacity;
}
.layer-card .card-eyebrow {
display: block;
font-size: var(--step-sm);
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--ink-soft);
margin: 0 0 0.9rem 0.25rem;
font-weight: 500;
will-change: opacity;
}
.card-box {
position: relative;
border-radius: 22px;
/* Reduced 15% from the original clamp(1.75rem, 3.2vw, 2.8rem) for a
slimmer, quieter card presence. */
padding: clamp(1.5rem, 2.7vw, 2.4rem);
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.9fr);
gap: clamp(0.85rem, 2.1vw, 1.9rem);
align-items: center;
overflow: hidden;
/* contain: paint forces transformed children (with will-change:
transform creating compositing layers) to respect this box's
overflow clipping. Without it, the brain's counter-scale
transform during the morph escapes the box bounds. */
contain: paint;
box-shadow: 0 22px 48px -18px rgba(46,46,40,0.28), 0 8px 22px -8px rgba(46,46,40,0.16);
/* 15% reduction from the original 240px — matches the lateral shrink */
min-height: 204px;
}
.card-content { min-width: 0; }
.card-title {
font-family: var(--type-display);
font-weight: 330;
font-size: clamp(2.08rem, 4.3vw, 3.32rem);
line-height: 1.06;
letter-spacing: -0.018em;
margin: 0 0 1.1rem;
color: #fffcf7;
}
.card-title b { font-weight: 640; font-style: normal; }
.card-title em { font-style: italic; font-weight: 640; }
.card-body {
font-size: clamp(1.2rem, 1.5vw, 1.4rem);
line-height: 1.4;
color: #fffcf7;
margin: 0;
max-width: 38ch;
font-weight: 400;
opacity: 0.88;
will-change: opacity;
}
/* Card illustration — per-layer PNG set via --card-illust custom property. */
.card-brain {
width: 100%;
aspect-ratio: 20 / 17;
background-image: var(--card-illust);
background-size: contain;
background-position: center right;
background-repeat: no-repeat;
opacity: 0.95;
margin-right: clamp(-3.5rem, -3vw, -1.5rem); /* bleed off right edge */
pointer-events: none;
will-change: transform;
}
/* Per-layer colours — muted Nordic mid-tones. */
.layer-card[data-layer="0"] .card-box { background: #7a8c70; } /* sage — AI Model */
.layer-card[data-layer="1"] .card-box { background: #7b9399; } /* slate — Knowledge */
.layer-card[data-layer="2"] .card-box { background: #b07556; } /* clay — Tools */
.layer-card[data-layer="3"] .card-box { background: #8a7a92; } /* plum — Agents */
/* Per-layer illustrations — URL-encode spaces in filenames. */
.layer-card[data-layer="0"] .card-brain { --card-illust: url('/fenja/illustrations/ai.png'); }
.layer-card[data-layer="1"] .card-brain { --card-illust: url('/fenja/illustrations/lightbulb%20-%20knowledge.png'); }
.layer-card[data-layer="2"] .card-brain { --card-illust: url('/fenja/illustrations/blocs%20tools.png'); }
.layer-card[data-layer="3"] .card-brain { --card-illust: url('/fenja/illustrations/agents.png'); }
/* z-stacking — later layers appear on top */
.layer-card[data-layer="0"] { z-index: 1; }
.layer-card[data-layer="1"] { z-index: 2; }
.layer-card[data-layer="2"] { z-index: 3; }
.layer-card[data-layer="3"] { z-index: 4; }
/* -------- GRID PHASE — cards become aligned SQUARES -------- */
/* When the .in-grid class is toggled on .layer-theatre, each card-box
becomes a 20vw square (via .in-grid .card-box), centered in its
full-width parent. Inside, the layout switches: the outside eyebrow
hides; a dedicated grid-label shows at the top; the long title and
body hide; the brain fills the rest centered.
The grid-label is ALWAYS in the DOM (absolutely-positioned inside
card-box with opacity:0 by default) so GSAP can fade it in smoothly
during the morph transition, rather than it snapping on when the
.in-grid class applies. */
.card-grid-label {
position: absolute;
left: clamp(1rem, 1.4vw, 1.4rem);
top: clamp(1rem, 1.4vw, 1.4rem);
font-size: clamp(0.75rem, 0.95vw, 0.95rem);
letter-spacing: 0.22em;
text-transform: uppercase;
color: #fffcf7;
opacity: 0;
font-weight: 500;
text-align: left;
line-height: 1;
pointer-events: none;
z-index: 2;
}
.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). */
max-width: 17vw;
width: 17vw;
aspect-ratio: 1 / 1;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
grid-template-columns: unset;
grid-template-rows: unset;
padding: clamp(0.85rem, 1.2vw, 1.2rem);
gap: 0;
min-height: 0;
border-radius: 16px;
}
/* In grid state the label is visible; position resets inside the
flex column */
.in-grid .card-grid-label {
opacity: 0.88;
position: relative;
left: auto;
top: auto;
margin: 0 0 0.5rem;
}
/* Hide long title + body in grid phase */
.in-grid .card-content { display: none; }
/* Illustration fills remaining space, centered. */
.in-grid .card-brain {
margin: 0;
flex: 1 1 auto;
width: 100%;
aspect-ratio: auto;
background-position: center;
background-size: 90% auto;
opacity: 0.9;
}
/* Hide the outside-box eyebrow during grid phase */
.in-grid .layer-card .card-eyebrow {
opacity: 0 !important;
pointer-events: none;
}
/* ============================================================
SCENE 3 — SLIDE 11 — words fly in
============================================================ */
#words-scene {
position: relative;
height: 260vh;
}
.words-pin {
position: sticky;
top: 0;
height: 100vh;
display: grid;
place-items: center;
padding-inline: var(--edge);
}
.words {
font-family: var(--type-display);
font-weight: 320;
font-size: clamp(2rem, 6vw, 5.2rem);
line-height: 1.06;
letter-spacing: -0.025em;
color: var(--ink);
max-width: 22ch;
margin: 0 auto;
text-align: left;
}
.words .w {
display: inline-block;
will-change: transform, opacity;
margin-right: 0.25em;
}
.js .words .w { opacity: 0; }
.words .w.hi {
font-style: italic;
color: var(--accent);
font-weight: 420;
}
/* ============================================================
SCENE 4 — PROJECT BIFROST REVEAL
============================================================ */
#bifrost {
position: relative;
min-height: 200vh;
}
.bifrost-pin {
position: sticky;
top: 0;
height: 100vh;
display: grid;
place-items: center;
overflow: hidden;
}
.bifrost-stage {
position: relative;
width: 100%;
height: 100%;
display: grid;
place-items: center;
}
/* the arc — bifrost bridge */
.arc-wrap {
position: absolute;
left: 50%;
top: 50%;
width: min(120vw, 1400px);
translate: -50% -50%;
pointer-events: none;
will-change: opacity, transform;
}
.js .arc-wrap { opacity: 0; }
.arc-wrap svg { display: block; width: 100%; height: auto; overflow: visible; }
.bifrost-text {
position: relative;
z-index: 2;
text-align: center;
max-width: 90vw;
padding: 0 var(--edge);
}
.bifrost-eyebrow {
font-size: var(--step-sm);
text-transform: uppercase;
letter-spacing: 0.32em;
color: var(--ink-soft);
margin-bottom: clamp(1rem, 3vh, 1.8rem);
will-change: opacity, transform;
display: inline-flex;
align-items: center;
gap: 0.8rem;
}
.js .bifrost-eyebrow { opacity: 0; }
.bifrost-eyebrow::before,
.bifrost-eyebrow::after {
content: "";
width: 28px; height: 1px;
background: currentColor;
}
.bifrost-name {
font-family: var(--type-display);
font-weight: 320;
font-size: clamp(3rem, 10vw, 9rem);
/* Loosened from 0.95 to 1.12 and padded so the italic "Bifrost"
token's ascenders/descenders clear the .bifrost-pin's
overflow:hidden (which exists to clip the aurora arc). */
line-height: 1.12;
letter-spacing: -0.04em;
color: var(--ink);
margin: 0;
padding: 0.12em 0.08em;
display: flex;
justify-content: center;
gap: 0.15em;
flex-wrap: wrap;
overflow: visible;
}
.bifrost-name .token {
display: inline-block;
will-change: transform, opacity, filter;
}
.js .bifrost-name .token { opacity: 0; }
.bifrost-name .token.accent {
font-style: italic;
background: linear-gradient(100deg, var(--aurora-1) 0%, var(--aurora-2) 32%, var(--aurora-3) 68%, var(--aurora-4) 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.bifrost-sub {
margin-top: clamp(1.4rem, 4vh, 2.2rem);
font-size: var(--step-lg);
font-weight: 300;
color: var(--ink-soft);
max-width: 44ch;
margin-inline: auto;
line-height: 1.35;
will-change: opacity, transform;
}
.js .bifrost-sub { opacity: 0; }
.bifrost-sub em {
font-style: italic;
color: var(--ink);
}
/* Credits */
.credits {
position: relative;
padding: clamp(4rem, 12vh, 8rem) var(--edge) clamp(2rem, 4vh, 3rem);
border-top: 1px solid rgba(46,46,40,0.1);
margin-top: 8vh;
font-size: var(--step-sm);
color: var(--ink-mute);
display: flex;
justify-content: space-between;
gap: 2rem;
flex-wrap: wrap;
letter-spacing: 0.02em;
}
.credits .col a {
color: var(--ink-soft);
text-decoration: none;
border-bottom: 1px solid rgba(46,46,40,0.2);
padding-bottom: 1px;
transition: border-color 0.2s, color 0.2s;
}
.credits .col a:hover { color: var(--accent); border-color: var(--accent); }
/* ============================================================
RESPONSIVE
============================================================ */
@media (max-width: 900px) {
#page-overview { --edge: 1.25rem; }
.meta { display: none; }
.rail { display: none; }
.brand-sub { display: none; } /* too cramped on small screens */
.stack-pin {
padding-top: 5rem;
}
.copy-stage {
width: 100%;
max-width: 100%;
position: relative;
top: auto;
transform: none;
}
.copy-layer h2 { font-size: clamp(1.6rem, 5.5vw, 2.2rem); }
.copy-layer p { font-size: 1rem; }
/* Cards shrink on mobile — single column body + smaller brain */
.layer-card { width: 92%; }
.card-box {
grid-template-columns: 1fr;
min-height: 0;
padding: 1.4rem;
}
.card-title { font-size: clamp(1.25rem, 5vw, 1.7rem); }
.card-body { font-size: 0.92rem; }
.card-brain {
max-width: 180px;
justify-self: end;
margin-right: -1rem;
aspect-ratio: 20 / 14;
}
.hero-foot { margin-top: 2rem; flex-direction: column; gap: 1rem; align-items: flex-start; }
#hero { padding-top: 5rem; padding-bottom: 3rem; min-height: auto; }
}
@media (max-width: 520px) {
.hero-title { font-size: clamp(2.2rem, 10vw, 3.2rem); }
.hero-lede { font-size: 1.05rem; }
.card-eyebrow { font-size: 0.72rem !important; letter-spacing: 0.2em; }
}
/* ============================================================
REDUCED MOTION — degraded version reveals all content
============================================================ */
@media (prefers-reduced-motion: reduce) {
#words-scene, #bifrost { height: auto !important; min-height: 0 !important; }
.stack-pin, .words-pin, .bifrost-pin {
position: relative !important;
height: auto !important;
min-height: auto;
padding-block: 4rem;
display: block !important;
}
.layer-theatre {
position: relative !important;
height: auto !important;
}
.layer-card {
position: relative !important;
left: auto !important; top: auto !important;
transform: none !important;
margin: 0 auto 2rem !important;
width: 100% !important;
max-width: 900px;
}
.card-body { opacity: 1 !important; }
.card-eyebrow { opacity: 1 !important; }
.copy-stage {
position: relative !important;
top: auto !important;
transform: none !important;
width: 100% !important;
max-width: 900px;
margin: 2rem auto 0;
}
.copy-layer { position: static !important; opacity: 1 !important; transform: none !important; margin-bottom: 3rem; }
.brand-sub { opacity: 1 !important; transform: none !important; }
.words .w { opacity: 1 !important; transform: none !important; }
.bifrost-eyebrow, .bifrost-sub, .arc-wrap { opacity: 1 !important; transform: translate(-50%, -50%) !important; }
.arc-wrap { transform: translate(-50%, -50%) !important; }
.bifrost-name .token { opacity: 1 !important; transform: none !important; filter: none !important; }
.scroll-hint .arrow { animation: none; }
}
/* Illustration data URIs — defined once, referenced by both
the treasure-map stops and the summary cards below. */
/* Illustration paths — real SVG files, self-hosted under protected/fenja/illustrations/ */
#page-overview {
--illust-community: url("/fenja/illustrations/community.svg");
--illust-council: url("/fenja/illustrations/council.svg");
--illust-pilot: url("/fenja/illustrations/pilot.svg");
}
/* ============================================================
SCENE 5 — PROJECT BIFROST · WHAT IT MEANS (treasure-map)
A meandering path drawn down the page with one intro stop
and three component stops revealed sequentially as the user
scrolls. Each component pairs body copy with an illustration
that fades in alongside it.
============================================================ */
#bifrost-meaning {
position: relative;
padding: clamp(6rem, 14vh, 12rem) var(--edge) clamp(4rem, 10vh, 8rem);
overflow: hidden;
}
.map-intro {
max-width: 60ch;
margin: 0 auto clamp(5rem, 12vh, 9rem);
text-align: center;
position: relative;
z-index: 2;
}
.map-intro .map-eyebrow {
display: inline-flex;
align-items: center;
gap: 0.75rem;
font-size: var(--step-sm);
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--ink-mute);
margin-bottom: clamp(1.4rem, 3.5vh, 2rem);
font-weight: 500;
}
.map-intro .map-eyebrow::before,
.map-intro .map-eyebrow::after {
content: "";
width: 28px; height: 1px;
background: currentColor;
}
.map-title {
font-family: var(--type-display);
font-weight: 330;
font-size: var(--step-xl);
line-height: 1.04;
letter-spacing: -0.025em;
color: var(--ink);
margin: 0 0 clamp(1rem, 2.5vh, 1.5rem);
}
.map-title em {
font-style: italic;
color: var(--accent);
font-weight: 400;
}
.map-lede {
font-size: var(--step-lg);
font-weight: 300;
line-height: 1.4;
color: var(--ink-soft);
margin: 0 auto;
max-width: 56ch;
letter-spacing: -0.005em;
}
.map-lede em {
font-style: italic;
color: var(--ink);
font-weight: 400;
}
/* The map canvas — relative container holding the path SVG and stops */
.map-canvas {
position: relative;
max-width: 1200px;
margin: 0 auto;
}
/* Wandering path — SVG stretched to canvas dimensions */
.map-path {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
overflow: visible;
}
.map-path .path-bg {
fill: none;
stroke: var(--ink);
stroke-opacity: 0.18;
stroke-width: 1.2;
stroke-dasharray: 4 6;
stroke-linecap: round;
}
.map-path .path-draw {
fill: none;
stroke: var(--accent);
stroke-opacity: 0.7;
stroke-width: 1.6;
stroke-linecap: round;
}
/* A stop — three-column grid: text | dot | image (alternating sides) */
.map-stop {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
gap: clamp(2rem, 5vw, 4.5rem);
margin-bottom: clamp(7rem, 14vh, 11rem);
z-index: 2;
}
.map-stop:last-child { margin-bottom: 0; }
/* Intro stop is single-column, centered, no image */
.map-stop--intro {
grid-template-columns: 1fr;
text-align: center;
margin-bottom: clamp(7rem, 14vh, 11rem);
justify-items: center;
}
.map-stop--intro .stop-content {
max-width: 46ch;
margin: 0 auto;
}
/* Dot anchor — sits on the path */
.dot-anchor {
position: relative;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
z-index: 3;
}
.map-stop[data-side="left"] .dot-anchor { grid-column: 2; }
.map-stop[data-side="right"] .dot-anchor { grid-column: 2; }
.dot {
position: relative;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--paper);
border: 2px solid var(--accent);
/* paper-coloured ring masks the path passing under the dot */
box-shadow: 0 0 0 6px var(--paper);
z-index: 2;
}
/* Soft pulse halo on the dot */
.dot::after {
content: "";
position: absolute;
inset: -10px;
border-radius: 50%;
border: 1px solid var(--accent);
opacity: 0.35;
z-index: -1;
}
.map-stop--intro .dot-anchor {
margin-bottom: clamp(1.5rem, 4vh, 2.5rem);
}
/* Text and image columns — alternating sides */
.map-stop[data-side="left"] .stop-content { grid-column: 1; text-align: left; }
.map-stop[data-side="left"] .stop-image { grid-column: 3; justify-self: start; }
.map-stop[data-side="right"] .stop-image { grid-column: 1; justify-self: end; }
.map-stop[data-side="right"] .stop-content { grid-column: 3; text-align: left; }
.stop-content {
font-family: var(--type-body);
}
.stop-eyebrow {
font-size: var(--step-sm);
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink-mute);
font-weight: 500;
display: block;
margin-bottom: 0.6rem;
}
.stop-title {
font-family: var(--type-display);
font-weight: 340;
font-size: clamp(2rem, 4.2vw, 3.2rem);
line-height: 1.04;
letter-spacing: -0.025em;
color: var(--ink);
margin: 0 0 clamp(0.6rem, 1.5vh, 1rem);
}
.stop-title em {
font-style: italic;
color: var(--accent);
font-weight: 400;
}
.stop-sub {
font-family: var(--type-display);
font-style: italic;
font-size: clamp(1.15rem, 1.8vw, 1.5rem);
font-weight: 300;
color: var(--ink-soft);
margin: 0 0 1.1rem;
letter-spacing: -0.005em;
line-height: 1.3;
}
.stop-body {
font-size: var(--step-md);
line-height: 1.55;
color: var(--ink-soft);
margin: 0;
max-width: 42ch;
font-weight: 400;
}
.stop-intro-text {
font-family: var(--type-display);
font-style: italic;
font-weight: 300;
font-size: clamp(1.4rem, 2.4vw, 2rem);
line-height: 1.35;
color: var(--ink-soft);
margin: 0;
letter-spacing: -0.01em;
}
.stop-intro-text em {
font-style: normal;
color: var(--accent);
font-weight: 400;
}
.stop-image {
width: 100%;
max-width: 380px;
}
.stop-image img {
display: block;
width: 100%;
height: auto;
/* faint blend with the cream paper */
mix-blend-mode: multiply;
}
/* Initial hidden state — only when JS is enabled */
.js .map-stop .dot {
opacity: 0;
transform: scale(0.2);
transform-origin: center;
will-change: opacity, transform;
}
.js .map-stop .stop-content > *,
.js .map-stop .stop-image {
opacity: 0;
transform: translateY(28px);
will-change: opacity, transform;
}
/* ============================================================
SCENE 6 — PROJECT BIFROST · JOIN
A large call-to-action that, on click, crossfades to a
confirmation panel listing what happens next. Below, a
three-column footer row: "Project Bifrost" wordmark (left),
Fenja AI logo (centre), Innovationsfonden mark (right).
============================================================ */
#bifrost-join {
position: relative;
padding: clamp(5rem, 12vh, 10rem) var(--edge) clamp(2rem, 5vh, 3.5rem);
overflow: hidden;
min-height: 90vh;
display: flex;
flex-direction: column;
}
/* Stage holds BOTH the CTA and the confirmation panel, stacked in
the SAME grid cell. The cell auto-sizes to whichever panel is
taller (on mobile the confirmation list is much taller than the
CTA), so neither panel ever overflows into the footer. GSAP's
opacity/y tweens handle the crossfade. */
.join-stage {
position: relative;
flex: 1 1 auto;
max-width: 1100px;
margin: 0 auto;
width: 100%;
display: grid;
place-items: center;
padding: clamp(1rem, 4vh, 3rem) 0;
}
.join-panel {
grid-column: 1;
grid-row: 1;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
/* ---------- CTA state ---------- */
.join-cta .join-eyebrow {
display: inline-flex;
align-items: center;
gap: 0.75rem;
font-size: var(--step-sm);
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--ink-mute);
margin-bottom: clamp(1.4rem, 3.5vh, 2rem);
font-weight: 500;
}
.join-cta .join-eyebrow::before,
.join-cta .join-eyebrow::after {
content: "";
width: 28px; height: 1px;
background: currentColor;
}
.join-cta .join-headline {
font-family: var(--type-display);
font-weight: 320;
font-size: clamp(2.4rem, 6.2vw, 5.4rem);
line-height: 1.04;
letter-spacing: -0.035em;
color: var(--ink);
margin: 0 auto clamp(2.6rem, 6vh, 4rem);
max-width: 20ch;
}
.join-cta .join-headline em {
font-style: italic;
background: linear-gradient(100deg, var(--aurora-1) 0%, var(--aurora-2) 32%, var(--aurora-3) 68%, var(--aurora-4) 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 340;
}
/* The button itself — pill shape with the terracotta accent */
.join-button {
font-family: var(--type-body);
font-size: clamp(1.02rem, 1.35vw, 1.18rem);
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--paper);
background: var(--accent);
border: none;
padding: clamp(1.1rem, 2.2vh, 1.45rem) clamp(2rem, 3.8vw, 2.8rem);
border-radius: 100px;
cursor: pointer;
position: relative;
overflow: hidden;
display: inline-flex;
align-items: center;
gap: 0.85rem;
box-shadow: 0 14px 34px -14px rgba(164, 85, 59, 0.55),
0 4px 12px -4px rgba(46, 46, 40, 0.2);
transition: transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1),
box-shadow 0.25s ease,
background-color 0.25s ease;
}
.join-button:hover {
transform: translateY(-2px);
box-shadow: 0 22px 44px -14px rgba(164, 85, 59, 0.65),
0 8px 18px -4px rgba(46, 46, 40, 0.25);
background: #b55e42;
}
.join-button:active {
transform: translateY(0);
}
.join-button:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 4px;
}
.join-button:disabled {
cursor: default;
}
.join-button .arrow {
display: inline-block;
width: 18px; height: 1.5px;
background: currentColor;
position: relative;
transition: transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.join-button .arrow::after {
content: "";
position: absolute;
right: -1px; top: -4px;
width: 9px; height: 9px;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
transform: rotate(-45deg);
}
.join-button:hover .arrow {
transform: translateX(4px);
}
.join-cta .join-subtext {
margin: clamp(1.6rem, 4vh, 2.4rem) auto 0;
font-size: var(--step-sm);
color: var(--ink-mute);
letter-spacing: 0.04em;
max-width: 40ch;
}
/* ---------- Confirmation state ---------- */
.join-confirmation {
pointer-events: none; /* enabled by JS after fade-in completes */
}
.join-confirmation .confirm-eyebrow {
display: inline-flex;
align-items: center;
gap: 0.75rem;
font-size: var(--step-sm);
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: clamp(1.4rem, 3vh, 1.8rem);
font-weight: 500;
}
.join-confirmation .confirm-eyebrow::before,
.join-confirmation .confirm-eyebrow::after {
content: "";
width: 22px; height: 1px;
background: currentColor;
opacity: 0.7;
}
.join-confirmation .confirm-headline {
font-family: var(--type-display);
font-weight: 330;
font-size: clamp(2rem, 5vw, 3.6rem);
line-height: 1.06;
letter-spacing: -0.03em;
color: var(--ink);
margin: 0 auto;
max-width: 22ch;
}
.join-confirmation .confirm-headline em {
font-style: italic;
color: var(--accent);
font-weight: 380;
}
.confirm-list {
list-style: none;
padding: 0;
margin: clamp(2rem, 5vh, 3.2rem) auto 0;
max-width: 62ch;
text-align: left;
}
.confirm-list li {
padding: clamp(1rem, 2.2vh, 1.4rem) 0 clamp(1rem, 2.2vh, 1.4rem) clamp(2.6rem, 4vw, 3.2rem);
position: relative;
font-size: var(--step-md);
line-height: 1.55;
color: var(--ink-soft);
border-top: 1px solid rgba(46, 46, 40, 0.12);
}
.confirm-list li:first-child { border-top: none; }
.confirm-list li em {
font-family: var(--type-display);
font-style: italic;
font-weight: 400;
color: var(--ink);
}
/* Terracotta circle marker + cream check — staged with a CSS
transition that fires when JS adds `.is-checked` to each <li>. */
.confirm-list li::before {
content: "";
position: absolute;
left: 0;
top: clamp(0.95rem, 2.2vh, 1.35rem);
width: clamp(1.4rem, 2.3vw, 1.7rem);
height: clamp(1.4rem, 2.3vw, 1.7rem);
background: var(--accent);
border-radius: 50%;
opacity: 0;
transform: scale(0.4);
transition: opacity 0.35s ease,
transform 0.4s cubic-bezier(0.3, 1.5, 0.5, 1);
}
.confirm-list li::after {
content: "";
position: absolute;
left: clamp(0.42rem, 0.7vw, 0.55rem);
top: clamp(1.5rem, 3vh, 1.95rem);
width: clamp(0.58rem, 0.9vw, 0.72rem);
height: clamp(0.3rem, 0.5vw, 0.38rem);
border-left: 2px solid var(--paper);
border-bottom: 2px solid var(--paper);
transform: rotate(-45deg) scale(0.4);
transform-origin: center;
opacity: 0;
transition: opacity 0.3s ease 0.15s,
transform 0.3s cubic-bezier(0.3, 1.5, 0.5, 1) 0.15s;
}
.confirm-list li.is-checked::before {
opacity: 1;
transform: scale(1);
}
.confirm-list li.is-checked::after {
opacity: 1;
transform: rotate(-45deg) scale(1);
}
/* ---------- Footer row (three brand marks, equal weight) ---------- */
.join-footer {
margin-top: auto;
padding-top: clamp(2.5rem, 6vh, 4rem);
/* Generous bottom padding — the dot-nav sits 36px from the bottom of
the viewport, plus its own hit area; logos need clear room below
them. Without this, the rightmost footer items can appear to slide
under the nav on short viewports. */
padding-bottom: clamp(5rem, 10vh, 8rem);
border-top: 1px solid rgba(46, 46, 40, 0.1);
display: grid;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
gap: clamp(1rem, 3vw, 3rem);
max-width: 1200px;
margin-left: auto;
margin-right: auto;
width: 100%;
/* Single source of truth for how tall each footer item should be.
All three items are sized against this — the Fenja SVG fills it,
the Innovationsfonden mark scales its icon and text against it,
and the "Project Bifrost" text picks a font-size from it. */
--foot-h: clamp(34px, 4.4vh, 52px);
}
.join-footer > * {
height: var(--foot-h);
display: flex;
align-items: center;
}
/* Left: "Project Bifrost" in Newsreader, italic accent on Bifrost */
.join-footer .foot-project {
justify-self: start;
font-family: var(--type-display);
font-weight: 340;
font-size: calc(var(--foot-h) * 0.54);
letter-spacing: -0.01em;
color: var(--ink);
white-space: nowrap;
line-height: 1;
}
.join-footer .foot-project em {
font-style: italic;
color: var(--accent);
font-weight: 360;
margin-left: 0.2em;
}
/* Centre: the Fenja AI SVG logo.
Same height target (--foot-h) as the "Project Bifrost" wordmark on
the left and the Innovationsfonden mark on the right, so all three
read as equal-weight brand marks. */
.join-footer .foot-fenja {
justify-self: center;
}
.join-footer .foot-fenja img,
.join-footer .foot-fenja svg {
height: 100%;
width: auto;
display: block;
}
/* Right: Innovationsfonden — hybrid of a small SVG slanted "I" mark
plus HTML text. Using HTML text instead of SVG <text> avoids
text-metric overflow clipping at the section's right edge. */
.join-footer .foot-innov {
justify-self: end;
gap: calc(var(--foot-h) * 0.12);
color: #0e3a48;
font-family: var(--type-body);
font-weight: 700;
font-size: calc(var(--foot-h) * 0.46);
letter-spacing: -0.015em;
line-height: 1;
white-space: nowrap;
}
.join-footer .foot-innov .innov-mark {
height: calc(var(--foot-h) * 0.88);
width: auto;
display: block;
fill: currentColor;
}
/* Hidden initial states when JS enabled (scroll-triggered reveals) */
.js .join-cta,
.js .join-confirmation,
.js .join-footer > * {
opacity: 0;
transform: translateY(22px);
will-change: opacity, transform;
}
/* ============================================================
RESPONSIVE — additions for the new sections
(independent of the existing @media block above)
============================================================ */
@media (max-width: 900px) {
/* Treasure map collapses to a centred vertical timeline */
.map-stop,
.map-stop--intro {
grid-template-columns: 1fr;
text-align: center;
margin-bottom: clamp(4.5rem, 10vh, 7rem);
gap: clamp(1.2rem, 3vw, 2rem);
justify-items: center;
}
.map-stop .dot-anchor {
grid-column: 1 !important;
margin: 0 auto;
order: 1;
}
.map-stop .stop-image {
grid-column: 1 !important;
justify-self: center !important;
max-width: 280px;
margin: 0 auto;
order: 2;
/* Paper-coloured backdrop hides the centred rail behind the
illustration on mobile. The illustration uses multiply blend
which then composites against this paper colour (same as the
page background) — visually unchanged, but no rail bleed. */
background: var(--paper);
}
.map-stop .stop-content {
grid-column: 1 !important;
text-align: center !important;
order: 3;
/* Same trick for the text block — keeps the rail from showing
through inter-line spacing. Generous vertical padding so the
rail "ducks under" the whole text region cleanly. */
background: var(--paper);
padding: 0.75rem 0.5rem;
max-width: 90%;
}
.stop-body {
max-width: 42ch;
margin: 0 auto !important;
}
/* Same backdrop for the intro paragraph */
.map-stop--intro .stop-content {
background: var(--paper);
padding: 0.75rem 1rem;
}
/* Dot already has a paper-coloured ring (box-shadow) so the
rail doesn't show through it — no extra work needed there. */
/* Replace the curving SVG path with a clean vertical rail */
.map-path { display: none; }
.map-canvas::before {
content: "";
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 1px;
background: rgba(46, 46, 40, 0.18);
transform: translateX(-50%);
z-index: 0;
}
.map-canvas::after {
/* the "drawn" overlay rail in accent — height set by JS */
content: "";
position: absolute;
left: 50%;
top: 0;
width: 1.5px;
height: var(--rail-progress, 0%);
background: var(--accent);
opacity: 0.7;
transform: translateX(-50%);
z-index: 1;
transition: none;
}
/* Join footer stacks vertically on mobile, each mark centred */
.join-footer {
grid-template-columns: 1fr;
gap: clamp(1.8rem, 4vh, 2.4rem);
justify-items: center;
text-align: center;
}
.join-footer > * {
justify-self: center !important;
}
/* CTA headline + button tighter on phones */
.join-cta .join-headline {
font-size: clamp(2rem, 7vw, 3rem);
}
.confirm-list {
max-width: 100%;
padding: 0 0.25rem;
}
#bifrost-join {
min-height: auto;
padding: clamp(4rem, 10vh, 7rem) var(--edge) clamp(2rem, 4vh, 3rem);
}
.join-stage {
/* Mobile content (esp. confirmation with 4 wrapping bullets) is
taller than on desktop; size for the tallest panel so neither
one overflows into the footer below. */
min-height: clamp(560px, 82vh, 720px);
}
}
@media (max-width: 520px) {
.stop-title { font-size: clamp(1.6rem, 7vw, 2.2rem); }
.stop-sub { font-size: 1.05rem; }
.stop-intro-text { font-size: clamp(1.1rem, 4.5vw, 1.4rem); }
}
/* Reduced motion — show everything statically */
@media (prefers-reduced-motion: reduce) {
.map-stop .dot,
.map-stop .stop-content > *,
.map-stop .stop-image,
.join-cta,
.join-confirmation,
.join-footer > *,
.confirm-list li::before,
.confirm-list li::after {
opacity: 1 !important;
transform: none !important;
}
.map-path .path-draw { stroke-dashoffset: 0 !important; }
.map-canvas::after { height: 100% !important; }
}
/* Bind illustration custom properties to the rendered surfaces.
Each surface uses background-image so the underlying base64 data
stays defined once in :root. */
.stop-illust {
width: 100%;
aspect-ratio: 1 / 1;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
mix-blend-mode: multiply;
}
.stop-illust[data-illust="community"] { background-image: var(--illust-community); }
.stop-illust[data-illust="council"] { background-image: var(--illust-council); }
.stop-illust[data-illust="pilot"] { background-image: var(--illust-pilot); }
</style>
</head>
<body data-screen-label="01 Timeline">
<img class="site-mark" src="/fenja/fenja-wordmark-black.svg" alt="Fenja" aria-hidden="true" />
<!-- ───── Page 1 : TIMELINE ───── -->
<section class="page page-timeline is-active" id="page-timeline" data-screen-label="01 Timeline">
<div class="page-title">From the promise of AI to the loss of <em>sovereignty.</em></div>
<div class="page-sub">Twenty-three headlines, quietly laid across a tinted map. Scroll the wheel — the map turns with you.</div>
<!-- Globe ghost -->
<div class="globe-wrap" id="globe-wrap"></div>
<!-- Continue to the next page -->
<button class="continue-btn" id="continue-btn" type="button">
<span class="c-icon" aria-hidden="true">
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.4">
<circle cx="24" cy="24" r="22.5"/>
<path d="M16 24 H32 M26 17.5 L32.5 24 L26 30.5"
stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
<span class="c-label">How Fenja AI <em>addresses</em> this</span>
</button>
<div class="timeline-viewport" id="tl-viewport">
<div class="timeline-track" id="tl-track">
<div class="spine" id="spine"></div>
<!-- Year ticks and events injected by JS -->
</div>
</div>
</section>
<!-- ───── Page 2 : OVERVIEW ───── -->
<section class="page page-overview" id="page-overview" data-screen-label="02 Overview">
<!-- Topography background — concentric rings, parallax-scrolling behind
the Europe map. Drawn at runtime by bifrost.js's drawTopography()
into the SVG slot. Lives at z-index 0 so it sits behind the map
(z-index 1) and all content (z-index 2+). -->
<div class="overview-topography" id="overview-topography" aria-hidden="true"></div>
<div class="overview-globe" id="overview-globe"></div>
<!-- Internal scroller: the six Project Bifrost scenes live inside this.
Lenis and ScrollTrigger are wired to this element, not the window. -->
<div id="overview-scroll">
<!-- ============================================================
SCENE 1 — HERO
============================================================ -->
<section id="hero" class="scene" aria-labelledby="hero-title">
<div class="hero-wrap">
<div>
<div class="eyebrow" data-reveal>For regulated environments</div>
<h1 id="hero-title" class="hero-title" data-reveal-lines>
Fenja AI — Secure &amp; <em>Sovereign,</em><br/>
hosted where it <em>belongs.</em>
</h1>
<p class="hero-lede" data-reveal>
Fenja AI is a sovereign AI platform, enabling highly advanced AI capabilities hosted within the client's own secure infrastructure.
</p>
<!-- Hero foot: "Supported by Innovationsfonden" on the left and
the scroll-down indicator on the right, both inside the
left column at the bottom of the paragraph block. Shared
baseline via display:flex + align-items:baseline on
.hero-foot. The scroll arrow points DOWN and gently bounces
downward (see @keyframes hint below). -->
<div class="hero-foot">
<div class="support" aria-label="Supported by Innovationsfonden" data-reveal>
<span>Supported by</span>
<!-- Simplified Innovationsfonden wordmark (redrawn — not their official logo, a respectful representation) -->
<svg viewBox="0 0 190 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<g fill="#3c6b6b">
<path d="M4 2 L12 18 L10 2 Z" />
<text x="18" y="15" font-family="Manrope, sans-serif" font-weight="600" font-size="13" letter-spacing="0.2" fill="#3c6b6b">nnovationsfonden</text>
</g>
</svg>
</div>
<div class="scroll-hint" aria-hidden="true" data-reveal>
<span>Scroll</span>
<span class="arrow"></span>
</div>
</div>
</div>
</div>
</section>
<!-- ============================================================
SCENE 2 — ARCHITECTURE (pinned, scrubbed)
============================================================ -->
<section id="stack-scene" aria-label="The Fenja AI architecture">
<div class="stack-pin">
<div class="layer-theatre">
<!-- LEFT SIDE — explanatory copy, visible only during the grid phase. -->
<div class="copy-stage" aria-live="polite">
<div class="copy-layer" data-copy="0">
<span class="tag">One complete platform</span>
<h2>Everything you need <em>in one place.</em></h2>
<p>Fenja AI brings models, knowledge, tools, and agents together in one platform for using and scaling AI across your organisation.</p>
</div>
<div class="copy-layer" data-copy="1">
<span class="tag">Full control</span>
<h2>Your <strong>infrastructure.</strong><br/>Your <em>rules.</em></h2>
<p>Fenja AI is installed in your own client-managed environment, giving you full control over data, security, and governance.</p>
</div>
<div class="copy-layer" data-copy="2">
<span class="tag">Sovereignty</span>
<h2>Built in <strong>Denmark.</strong><br/>Ready for <em>Europe.</em></h2>
<p>Fenja AI is built in Denmark for European organisations that want trusted, sovereign AI on their own terms.</p>
</div>
</div>
<!-- LAYER CARDS — drop in, stack, then rearrange to grid -->
<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-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>
<p class="card-body">A state-of-the-art open-source language model deployed directly in your environment. It gives you powerful AI capabilities with full control over data, performance, and security.</p>
</div>
<div class="card-brain" aria-hidden="true"></div>
</div>
</article>
<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-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>
<p class="card-body">A built-in knowledge layer that helps the platform understand your terminology, processes, and data. It retains what matters, improves over time, and gives the AI the context needed to deliver relevant and accurate results.</p>
</div>
<div class="card-brain" aria-hidden="true"></div>
</div>
</article>
<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-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>
<p class="card-body">The capabilities that let the platform do real work across your environment. From search and retrieval to data access, automation, and analysis, these are the tools the AI uses to solve tasks in practice.</p>
</div>
<div class="card-brain" aria-hidden="true"></div>
</div>
</article>
<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-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>
<p class="card-body">Purpose-built agents designed to handle distinct roles and workflows. Fenja AI includes both ready-made agents and the framework to build new ones, so you can orchestrate AI the same way your organisation already works &mdash; through specialisation and coordination.</p>
</div>
<div class="card-brain" aria-hidden="true"></div>
</div>
</article>
</div>
</div>
</section>
<!-- ============================================================
SCENE 3 — SLIDE 11 — WORDS FLY IN ONE AT A TIME
============================================================ -->
<section id="words-scene" aria-labelledby="words-head">
<h3 id="words-head" class="sr-only" style="position:absolute;left:-9999px;">
This is why we've invited you. To ensure Fenja AI is not just built for you, but with you.
</h3>
<div class="words-pin">
<!-- The words sentence is rebuilt client-side when a first name is
available from /auth/me, so the user sees their own name fly in
as the "Erik." token. Static text below is the no-JS / no-name
fallback, used verbatim if /auth/me returns no firstName.
Spans must be laid out verbatim so the word-fly-in animation
has known DOM targets; the injected name variant swaps them in
place with the same .w structure preserved.
When a first name is present:
"This is why we've invited you, [Name]. To ensure Fenja AI is
not just built for you — but with you."
→ .hi on "[Name]." (the personalization beat)
→ .hi on "with" + "you." (the thesis)
When no first name:
"This is why we've invited you. To ensure Fenja AI is not
just built for you — but with you."
→ .hi on "you." (after invited)
→ .hi on "with" + "you." (the thesis) -->
<p class="words" aria-hidden="true" id="words-sentence">
<span class="w">This</span>
<span class="w">is</span>
<span class="w">why</span>
<span class="w">we&rsquo;ve</span>
<span class="w">invited</span>
<span class="w hi">you.</span>
<span class="w">To</span>
<span class="w">ensure</span>
<span class="w">Fenja</span>
<span class="w">AI</span>
<span class="w">is</span>
<span class="w">not</span>
<span class="w">just</span>
<span class="w">built</span>
<span class="w">for</span>
<span class="w">you</span>
<span class="w">&mdash;</span>
<span class="w">but</span>
<span class="w hi">with</span>
<span class="w hi">you.</span>
</p>
</div>
</section>
<!-- ============================================================
SCENE 4 — PROJECT BIFROST REVEAL
============================================================ -->
<section id="bifrost" aria-labelledby="bifrost-head">
<div class="bifrost-pin">
<div class="bifrost-stage">
<!-- The bridge arc. Uses a restrained aurora gradient — the ONLY
place colour appears in the entire site. Norse mythology:
Bifrost is the bridge between worlds, rendered here as a
single luminous arc spanning the stage. -->
<div class="arc-wrap" aria-hidden="true">
<svg viewBox="0 0 1400 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="auroraGrad" x1="0" x2="1" y1="0" y2="0">
<stop offset="0%" stop-color="#b48755" stop-opacity="0"/>
<stop offset="15%" stop-color="#b48755" stop-opacity="0.95"/>
<stop offset="40%" stop-color="#a4553b" stop-opacity="0.95"/>
<stop offset="65%" stop-color="#5c7b8e" stop-opacity="0.95"/>
<stop offset="85%" stop-color="#6e5a86" stop-opacity="0.95"/>
<stop offset="100%" stop-color="#6e5a86" stop-opacity="0"/>
</linearGradient>
<linearGradient id="auroraGradSoft" x1="0" x2="1" y1="0" y2="0">
<stop offset="0%" stop-color="#b48755" stop-opacity="0"/>
<stop offset="15%" stop-color="#b48755" stop-opacity="0.35"/>
<stop offset="40%" stop-color="#a4553b" stop-opacity="0.35"/>
<stop offset="65%" stop-color="#5c7b8e" stop-opacity="0.35"/>
<stop offset="85%" stop-color="#6e5a86" stop-opacity="0.35"/>
<stop offset="100%" stop-color="#6e5a86" stop-opacity="0"/>
</linearGradient>
<filter id="softGlow" x="-20%" y="-50%" width="140%" height="200%">
<feGaussianBlur stdDeviation="8"/>
</filter>
</defs>
<!-- soft halo -->
<path id="arcHalo" d="M 60 420 Q 700 -40 1340 420"
fill="none"
stroke="url(#auroraGradSoft)"
stroke-width="24"
stroke-linecap="round"
filter="url(#softGlow)"/>
<!-- main arc -->
<path id="arcMain" d="M 60 420 Q 700 -40 1340 420"
fill="none"
stroke="url(#auroraGrad)"
stroke-width="3"
stroke-linecap="round"/>
<!-- secondary thin highlight arc -->
<path id="arcThin" d="M 80 420 Q 700 -20 1320 420"
fill="none"
stroke="url(#auroraGrad)"
stroke-width="1"
stroke-linecap="round"
opacity="0.6"/>
</svg>
</div>
<div class="bifrost-text">
<div class="bifrost-eyebrow">Introducing</div>
<h2 id="bifrost-head" class="bifrost-name">
<span class="token">Project</span>
<span class="token accent">Bifrost</span>
</h2>
<p class="bifrost-sub">
The bridge <em>between</em> an industrial-grade AI platform and the realities of regulated organisations &mdash; built <em>with</em> them, not just for them.
</p>
</div>
</div>
</div>
</section>
<!-- ============================================================
SCENE 5 — PROJECT BIFROST · WHAT IT MEANS
A treasure-map of participation: an intro stop and three
component stops revealed sequentially as the user scrolls
a meandering ink path down the page.
============================================================ -->
<section id="bifrost-meaning" aria-labelledby="bifrost-meaning-head">
<div class="map-intro">
<span class="map-eyebrow">The invitation</span>
<h2 id="bifrost-meaning-head" class="map-title">
What being part of <em>Project Bifrost</em> means
</h2>
<p class="map-lede">
Three ways to <em>shape</em>, to <em>influence</em>, and to <em>build with</em> the platform from the inside &mdash; a journey through what participation actually looks like.
</p>
</div>
<div class="map-canvas">
<!-- Wandering path. Stretched to fill the canvas via
preserveAspectRatio="none". The accent overlay is drawn
as the user scrolls down through the stops. -->
<svg class="map-path" viewBox="0 0 100 200" preserveAspectRatio="none" aria-hidden="true">
<!-- The d attributes are computed at runtime by buildMapPath()
based on the rendered Y positions of the dots, so the path
always passes through them regardless of content height.
The placeholder values below are valid fallback geometry
that ships if JS fails. -->
<path id="mapPathBg" class="path-bg"
vector-effect="non-scaling-stroke"
d="M 50 6 C 72 28, 72 52, 50 70 C 28 88, 28 112, 50 130 C 72 148, 72 172, 50 194"/>
<path id="mapPathDraw" class="path-draw"
vector-effect="non-scaling-stroke"
d="M 50 6 C 72 28, 72 52, 50 70 C 28 88, 28 112, 50 130 C 72 148, 72 172, 50 194"/>
</svg>
<!-- INTRO STOP -->
<article class="map-stop map-stop--intro" data-stop="0">
<div class="dot-anchor"><span class="dot"></span></div>
<div class="stop-content">
<p class="stop-intro-text">
Being part of <em>Project Bifrost</em> means <em>three</em> things &mdash; a community to shape the future with, a council to influence the platform through, and pilot projects that put it in your hands first.
</p>
</div>
</article>
<!-- STOP 1 — Community (text on left, image on right) -->
<article class="map-stop" data-stop="1" data-side="left" aria-labelledby="stop-1-title">
<div class="stop-content">
<span class="stop-eyebrow">Be part of a</span>
<h3 id="stop-1-title" class="stop-title"><em>Community</em></h3>
<p class="stop-sub">Shape the future together</p>
<p class="stop-body">Join a select community of organisations helping define the future of trusted sovereign AI in Denmark and Europe. At a time when Europe needs greater technological independence, this is an opportunity to contribute to an AI platform built on trust, shared ambition, and a common mission.</p>
</div>
<div class="dot-anchor"><span class="dot"></span></div>
<div class="stop-image">
<div class="stop-illust" data-illust="community" role="img" aria-label="Six people in discussion around a table"></div>
</div>
</article>
<!-- STOP 2 — Advisory Council (image on left, text on right) -->
<article class="map-stop" data-stop="2" data-side="right" aria-labelledby="stop-2-title">
<div class="stop-image">
<div class="stop-illust" data-illust="council" role="img" aria-label="A man and a woman in conversation"></div>
</div>
<div class="dot-anchor"><span class="dot"></span></div>
<div class="stop-content">
<span class="stop-eyebrow">Be part of an</span>
<h3 id="stop-2-title" class="stop-title"><em>Advisory Council</em></h3>
<p class="stop-sub">Turn insight into influence</p>
<p class="stop-body">Take part in regular advisory council sessions where your input directly shapes the product and platform roadmap. Gain first-hand insight into cutting-edge AI developments and help influence what is built, which capabilities are prioritised, and how the platform evolves to meet real organisational needs.</p>
</div>
</article>
<!-- STOP 3 — Pilot Projects (text on left, image on right) -->
<article class="map-stop" data-stop="3" data-side="left" aria-labelledby="stop-3-title">
<div class="stop-content">
<span class="stop-eyebrow">Be part of</span>
<h3 id="stop-3-title" class="stop-title"><em>Pilot Projects</em></h3>
<p class="stop-sub">Access the platform before others</p>
<p class="stop-body">A select number of Project Bifrost participants will have the opportunity to join pilot projects and gain early access to the platform at a significantly reduced price, subsidised by the Innovation Fund. This gives your organisation the chance to explore cutting-edge sovereign AI early, realise value at low cost, and help shape the platform through real-world use.</p>
</div>
<div class="dot-anchor"><span class="dot"></span></div>
<div class="stop-image">
<div class="stop-illust" data-illust="pilot" role="img" aria-label="Two people working together at a computer"></div>
</div>
</article>
</div>
</section>
<!-- ============================================================
SCENE 6 — PROJECT BIFROST · JOIN
A large call-to-action ("Join us in shaping the future...")
that, when clicked, crossfades to a confirmation panel listing
what happens next. Below, a three-column footer row with three
equal-weight brand marks: "Project Bifrost" (left-aligned),
Fenja AI (centred), and Innovationsfonden (right-aligned).
============================================================ -->
<section id="bifrost-join" aria-labelledby="bifrost-join-head">
<div class="join-stage">
<!-- CTA state (visible initially) -->
<div class="join-panel join-cta" id="joinCTA">
<div class="join-eyebrow">Ready?</div>
<h2 id="bifrost-join-head" class="join-headline">
Join us in shaping the future of <em>trusted sovereign AI.</em>
</h2>
<button type="button" class="join-button" id="joinBtn" aria-controls="joinConfirm">
Join Project Bifrost
<span class="arrow" aria-hidden="true"></span>
</button>
<p class="join-subtext">Built in Denmark. Supported by the Innovation Fund.</p>
</div>
<!-- Confirmation state (revealed after the CTA is clicked) -->
<div class="join-panel join-confirmation" id="joinConfirm" aria-hidden="true">
<div class="confirm-eyebrow">You're in</div>
<h2 class="confirm-headline">
Thank you for joining <em>Project Bifrost</em>.
</h2>
<ul class="confirm-list" role="list">
<li>The <em>Fenja AI team</em> will reach out to you shortly.</li>
<li>You&rsquo;ll receive an invitation to the <em>project portal</em> soon &mdash; where all project communication, materials, and updates will live.</li>
<li>We&rsquo;re currently setting the date for the <em>first advisory council meeting</em>. You&rsquo;ll be invited as soon as it&rsquo;s confirmed.</li>
<li>We&rsquo;ll be in touch shortly about your participation in the <em>pilot project</em>.</li>
</ul>
</div>
</div>
<div class="join-footer">
<div class="foot-project" aria-label="Project Bifrost">Project <em>Bifrost</em></div>
<div class="foot-fenja" aria-label="Fenja AI">
<!-- Fenja AI wordmark. Sized via .join-footer .foot-fenja img CSS,
which makes it match the height of the other two footer items. -->
<img src="/fenja/fenja-wordmark-black.svg" alt="Fenja AI" />
</div>
<div class="foot-innov" aria-label="Innovationsfonden">
<!-- PLACEHOLDER: redrawn Innovationsfonden mark.
Hybrid: a small SVG holds the characteristic slanted "I"
and an HTML <span> renders the rest of the wordmark with
native text metrics (avoids SVG <text> overflowing its
viewBox at arbitrary widths). Swap this block for the
real PNG/SVG when available — the container already
sizes correctly via --foot-h. -->
<svg class="innov-mark" viewBox="0 0 60 100" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M 22 4 L 55 4 L 38 96 L 5 96 Z"/>
</svg><span class="innov-text">nnovationsfonden</span>
</div>
</div>
</section>
</div><!-- /#overview-scroll -->
</section>
<!-- Dot-nav tray + nav (shared across all pages)
Seven entries, flat. The first targets the Timeline page (P1). The
next five each target a scene inside the Overview page (P2) — clicking
switches to Overview AND scrolls the overview's internal scroller to
that scene. The last (Join) goes to the final scene of Overview.
data-target : page id to activate
data-scroll-to : (optional) element id inside #overview-scroll to
scroll to AFTER the page switch. Scroll runs on the
Overview's internal scroller via Lenis (if booted)
or scroller.scrollTo() as a fallback. -->
<div class="dot-nav-tray"></div>
<nav class="dot-nav">
<button class="dot-btn" data-target="external-welcome" data-href="/" aria-label="Return to welcome page">
<span class="dot"></span>
<span class="label">Welcome</span>
</button>
<button class="dot-btn is-active" data-target="page-timeline">
<span class="dot"></span>
<span class="label">Timeline</span>
</button>
<button class="dot-btn" data-target="page-overview" data-scroll-to="hero">
<span class="dot"></span>
<span class="label">Hero</span>
</button>
<button class="dot-btn" data-target="page-overview" data-scroll-to="stack-scene">
<span class="dot"></span>
<span class="label">Architecture</span>
</button>
<button class="dot-btn" data-target="page-overview" data-scroll-to="words-scene">
<span class="dot"></span>
<span class="label">Words</span>
</button>
<button class="dot-btn" data-target="page-overview" data-scroll-to="bifrost">
<span class="dot"></span>
<span class="label">Bifrost</span>
</button>
<button class="dot-btn" data-target="page-overview" data-scroll-to="bifrost-meaning">
<span class="dot"></span>
<span class="label">Participate</span>
</button>
<button class="dot-btn" data-target="page-overview" data-scroll-to="bifrost-join">
<span class="dot"></span>
<span class="label">Join</span>
</button>
</nav>
<script src="/vendor/lenis.min.js" defer></script>
<script src="/vendor/gsap.min.js" defer></script>
<script src="/vendor/scrolltrigger.min.js" defer></script>
<script src="/bifrost.js" defer></script>
<script src="/timeline.js" defer></script>
</body>
</html>