add welcome page and change transition to timeline

This commit is contained in:
Arlind Ukshini 2026-04-22 17:31:45 +02:00
parent bb5711c08e
commit dc545b0776
8 changed files with 226 additions and 21 deletions

View file

@ -4,11 +4,13 @@
<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>
<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;
@ -44,6 +46,7 @@
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 ───── */
@ -59,7 +62,20 @@
pointer-events: auto;
}
/* Masthead and folio removed for a cleaner page — no corner chrome. */
/* ───────── 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 {
@ -717,6 +733,8 @@
</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>
@ -856,7 +874,7 @@
</button>
</nav>
<script src="timeline.js" defer></script>
<script src="/timeline.js" defer></script>
</body>
</html>

View file

@ -5,6 +5,8 @@
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Fenja AI</title>
<style>
@view-transition { navigation: auto; }
:root {
--paper: #faf6ee;
--paper-sink: #e7e1d0;
@ -34,6 +36,7 @@
background: radial-gradient(1100px 760px at 22% 42%, #fffcf7 0%, var(--paper) 58%, #f2ecdd 100%);
display: flex;
align-items: center;
view-transition-name: paper;
}
/* ───── Topographic currents ───── */
@ -68,6 +71,10 @@
display: block;
animation: enter 640ms var(--ease) forwards;
}
/* Suppress every step until entrance.js has checked /auth/me and picked
the right starting step. Prevents a flash of the email form when an
authed user lands on /. */
body.is-pending .step { display: none !important; }
@keyframes enter {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
@ -144,6 +151,102 @@
box-shadow: inset 0 -2px 0 0 var(--crimson);
}
/* ───── Welcome-step wordmark (centered in the right half) ───── */
.welcome-logo {
position: fixed;
top: 50%;
left: 75%;
transform: translate(-50%, -50%);
width: 280px;
height: auto;
opacity: 0;
pointer-events: none;
z-index: 5;
transition: opacity 640ms var(--ease) 120ms;
}
body:has(#step-welcome.is-active) .welcome-logo {
opacity: 0.92;
}
/* ───── Welcome ───── */
.welcome-title {
font-family: "Newsreader", Georgia, "Times New Roman", serif;
font-weight: 400;
font-style: italic;
font-size: 54px;
line-height: 1.05;
letter-spacing: -0.022em;
color: var(--ink);
margin: 0 0 28px 0;
text-wrap: pretty;
}
.welcome-body {
font-family: "Newsreader", Georgia, "Times New Roman", serif;
font-weight: 400;
font-size: 18px;
line-height: 1.55;
color: var(--ink);
max-width: 540px;
margin: 0 0 20px 0;
text-wrap: pretty;
}
.welcome-body em { font-style: italic; font-weight: 700; }
.welcome-cta {
all: unset;
display: inline-flex;
align-items: center;
gap: 14px;
margin-top: 20px;
padding: 16px 24px;
background: #fffcf7;
color: var(--ink);
cursor: pointer;
box-shadow:
0 0 0 0.5px rgba(56,56,49,0.06),
0 18px 32px -18px rgba(56,56,49,0.22),
0 2px 6px -3px rgba(56,56,49,0.08);
transition:
background var(--dur) var(--ease),
box-shadow var(--dur) var(--ease);
animation: welcome-breath 2800ms var(--ease) infinite;
}
.welcome-cta:hover {
background: #fffbf2;
box-shadow:
0 0 0 0.5px rgba(56,56,49,0.10),
0 24px 40px -20px rgba(56,56,49,0.28),
0 3px 8px -4px rgba(56,56,49,0.10);
}
.welcome-cta .c-label {
font-family: "Newsreader", Georgia, serif;
font-size: 19px;
font-weight: 400;
letter-spacing: -0.01em;
color: var(--ink);
line-height: 1;
}
.welcome-cta .c-icon {
flex-shrink: 0;
color: var(--ink-soft);
display: block;
}
.welcome-cta .c-arrow {
font-family: "Newsreader", Georgia, serif;
font-style: italic;
font-size: 21px;
color: var(--crimson);
line-height: 1;
transition: transform var(--dur) var(--ease);
}
.welcome-cta:hover .c-arrow {
transform: translateX(4px);
}
@keyframes welcome-breath {
0%, 100% { transform: translateX(0); }
50% { transform: translateX(5px); }
}
/* ───── Post-submit acknowledgement ───── */
.ack {
margin-top: 16px;
@ -184,13 +287,18 @@
.code-cell { width: 42px; height: 54px; font-size: 22px; }
.code-row { gap: 7px; }
.currents { opacity: 0.5; }
.welcome-title { font-size: 38px; }
.welcome-body { font-size: 16.5px; }
.welcome-logo { display: none; }
}
</style>
</head>
<body>
<body class="is-pending">
<div class="currents" id="currents" aria-hidden="true"></div>
<img class="welcome-logo" src="/fenja/fenja-wordmark-black.svg" alt="Fenja" aria-hidden="true" />
<main class="entrance">
<div class="entrance-inner">
@ -237,6 +345,31 @@
</form>
</section>
<!-- STEP 3 — WELCOME -->
<section class="step" id="step-welcome">
<h1 class="welcome-title">Welcome.</h1>
<p class="welcome-body">
Thank you for joining and for your interest in enabling sovereign AI
in Denmark and Europe. Project Bifrost is a deliberate effort to
advance it &mdash; the conviction that how we build these systems,
and where, will shape the next decades.
</p>
<p class="welcome-body">
What follows is a timeline: twenty-three moments that explain why
this matters now, and what the path looks like.
</p>
<button type="button" class="welcome-cta" id="welcome-continue">
<svg class="c-icon" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.3"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M4 4.5c2.5-.7 5-.7 8 .5C15 4 17.5 3.8 20 4.5v14c-2.5-.7-5-.5-8 .5-3-1-5.5-1.2-8-.5v-14Z"/>
<path d="M12 5v14"/>
</svg>
<span class="c-label">Learn more about Project Bifrost</span>
<span class="c-arrow" aria-hidden="true">&rarr;</span>
</button>
</section>
</div>
</main>

View file

@ -48,6 +48,7 @@
const steps = {
email: document.getElementById('step-email'),
code: document.getElementById('step-code'),
welcome: document.getElementById('step-welcome'),
};
function showStep(name) {
Object.entries(steps).forEach(([k, el]) => {
@ -171,8 +172,8 @@ async function submitCode() {
});
if (res.ok) {
setAck(codeAck, 'Filed. Opening your archive\u2026', false);
setTimeout(() => { window.location.href = '/'; }, 500);
setAck(codeAck, '', false);
showStep('welcome');
return;
}
@ -199,3 +200,29 @@ document.getElementById('use-different').addEventListener('click', () => {
showStep('email');
setTimeout(() => emailInput.focus(), 300);
});
/* ───── Step 3: welcome → timeline ───── */
document.getElementById('welcome-continue').addEventListener('click', () => {
// Cross-document View Transitions animate this nav automatically on
// supported browsers (Chrome/Safari). Firefox falls back to a plain nav.
window.location.href = '/timeline';
});
/* ───── On-load routing ───── */
// `/` always serves this entrance shell. Decide which step to show based
// on whether the visitor already has a valid session.
(async function routeOnLoad() {
let authed = false;
try {
const res = await fetch('/auth/me', { credentials: 'same-origin' });
authed = res.ok;
} catch { /* offline — fall through to email */ }
if (authed) {
showStep('welcome');
} else {
showStep('email');
setTimeout(() => emailInput.focus(), 300);
}
document.body.classList.remove('is-pending');
})();

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 456 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 457 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 523 KiB

View file

@ -12,7 +12,6 @@ import { fileURLToPath } from 'node:url';
import authRouter from './src/auth.js';
import { requireAuth } from './src/middleware.js';
import { currentSession } from './src/sessions.js';
import { initMail } from './src/mail.js';
import './src/db.js'; // side-effect import: opens DB + runs schema
@ -74,18 +73,18 @@ app.use((req, res, next) => {
app.use('/auth', authRouter);
// ─── Root dispatch ───────────────────────────────────────────
// GET / → timeline (if authed) | entrance (otherwise)
// GET /entrance → always the entrance (useful for "log in as someone else")
// Other paths fall through to the static handlers below.
app.get('/', (req, res, next) => {
if (currentSession(req)) {
// Authed: serve the timeline directly from /protected/index.html
return res.sendFile(path.join(__dirname, 'protected', 'index.html'));
}
// Not authed: serve the entrance
// GET / → always the entrance shell. If authed, entrance.js routes
// the user to the welcome step client-side (preserving the
// email/code UI as the no-session fallback).
// GET /timeline → gated timeline page (protected/index.html).
app.get('/', (req, res) => {
return res.sendFile(path.join(__dirname, 'public', 'entrance.html'));
});
app.get('/timeline', requireAuth, (req, res) => {
return res.sendFile(path.join(__dirname, 'protected', 'index.html'));
});
// ─── Public static assets (entrance.js, etc.) ────────────────
// Fallthrough so Express can still try the routes below if nothing matches.
app.use(