add welcome page and change transition to timeline
This commit is contained in:
parent
bb5711c08e
commit
dc545b0776
8 changed files with 226 additions and 21 deletions
|
|
@ -4,11 +4,13 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<title>A Catalog of Sovereignty — 2022–2026</title>
|
<title>A Catalog of Sovereignty — 2022–2026</title>
|
||||||
<link rel="stylesheet" href="fenja/colors_and_type.css" />
|
<link rel="stylesheet" href="/fenja/colors_and_type.css" />
|
||||||
<script src="vendor/d3-array.min.js"></script>
|
<script src="/vendor/d3-array.min.js"></script>
|
||||||
<script src="vendor/d3-geo.min.js"></script>
|
<script src="/vendor/d3-geo.min.js"></script>
|
||||||
<script src="vendor/topojson-client.min.js"></script>
|
<script src="/vendor/topojson-client.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
|
@view-transition { navigation: auto; }
|
||||||
|
|
||||||
:root{
|
:root{
|
||||||
--paper: #faf6ee;
|
--paper: #faf6ee;
|
||||||
--paper-high: #fffcf7;
|
--paper-high: #fffcf7;
|
||||||
|
|
@ -44,6 +46,7 @@
|
||||||
just the paper catching light. */
|
just the paper catching light. */
|
||||||
background:
|
background:
|
||||||
radial-gradient(1200px 800px at 18% 45%, #fffcf7 0%, var(--paper) 55%, #f4efe2 100%);
|
radial-gradient(1200px 800px at 18% 45%, #fffcf7 0%, var(--paper) 55%, #f4efe2 100%);
|
||||||
|
view-transition-name: paper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ───── Page scaffolding ───── */
|
/* ───── Page scaffolding ───── */
|
||||||
|
|
@ -59,7 +62,20 @@
|
||||||
pointer-events: auto;
|
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 overline title — large, sits lower on the front matter so it reads */
|
||||||
.page-title {
|
.page-title {
|
||||||
|
|
@ -717,6 +733,8 @@
|
||||||
</head>
|
</head>
|
||||||
<body data-screen-label="01 Timeline">
|
<body data-screen-label="01 Timeline">
|
||||||
|
|
||||||
|
<img class="site-mark" src="/fenja/fenja-wordmark-black.svg" alt="Fenja" aria-hidden="true" />
|
||||||
|
|
||||||
<!-- ───── Page 1 : TIMELINE ───── -->
|
<!-- ───── Page 1 : TIMELINE ───── -->
|
||||||
<section class="page page-timeline is-active" id="page-timeline" data-screen-label="01 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-title">From the promise of AI to the loss of <em>sovereignty.</em></div>
|
||||||
|
|
@ -856,7 +874,7 @@
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<script src="timeline.js" defer></script>
|
<script src="/timeline.js" defer></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<title>Fenja AI</title>
|
<title>Fenja AI</title>
|
||||||
<style>
|
<style>
|
||||||
|
@view-transition { navigation: auto; }
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--paper: #faf6ee;
|
--paper: #faf6ee;
|
||||||
--paper-sink: #e7e1d0;
|
--paper-sink: #e7e1d0;
|
||||||
|
|
@ -34,6 +36,7 @@
|
||||||
background: radial-gradient(1100px 760px at 22% 42%, #fffcf7 0%, var(--paper) 58%, #f2ecdd 100%);
|
background: radial-gradient(1100px 760px at 22% 42%, #fffcf7 0%, var(--paper) 58%, #f2ecdd 100%);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
view-transition-name: paper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ───── Topographic currents ───── */
|
/* ───── Topographic currents ───── */
|
||||||
|
|
@ -68,6 +71,10 @@
|
||||||
display: block;
|
display: block;
|
||||||
animation: enter 640ms var(--ease) forwards;
|
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 {
|
@keyframes enter {
|
||||||
from { opacity: 0; transform: translateY(6px); }
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
|
@ -144,6 +151,102 @@
|
||||||
box-shadow: inset 0 -2px 0 0 var(--crimson);
|
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 ───── */
|
/* ───── Post-submit acknowledgement ───── */
|
||||||
.ack {
|
.ack {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
|
@ -184,13 +287,18 @@
|
||||||
.code-cell { width: 42px; height: 54px; font-size: 22px; }
|
.code-cell { width: 42px; height: 54px; font-size: 22px; }
|
||||||
.code-row { gap: 7px; }
|
.code-row { gap: 7px; }
|
||||||
.currents { opacity: 0.5; }
|
.currents { opacity: 0.5; }
|
||||||
|
.welcome-title { font-size: 38px; }
|
||||||
|
.welcome-body { font-size: 16.5px; }
|
||||||
|
.welcome-logo { display: none; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="is-pending">
|
||||||
|
|
||||||
<div class="currents" id="currents" aria-hidden="true"></div>
|
<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">
|
<main class="entrance">
|
||||||
<div class="entrance-inner">
|
<div class="entrance-inner">
|
||||||
|
|
||||||
|
|
@ -237,6 +345,31 @@
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</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 — 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">→</span>
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
const steps = {
|
const steps = {
|
||||||
email: document.getElementById('step-email'),
|
email: document.getElementById('step-email'),
|
||||||
code: document.getElementById('step-code'),
|
code: document.getElementById('step-code'),
|
||||||
|
welcome: document.getElementById('step-welcome'),
|
||||||
};
|
};
|
||||||
function showStep(name) {
|
function showStep(name) {
|
||||||
Object.entries(steps).forEach(([k, el]) => {
|
Object.entries(steps).forEach(([k, el]) => {
|
||||||
|
|
@ -171,8 +172,8 @@ async function submitCode() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setAck(codeAck, 'Filed. Opening your archive\u2026', false);
|
setAck(codeAck, '', false);
|
||||||
setTimeout(() => { window.location.href = '/'; }, 500);
|
showStep('welcome');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,3 +200,29 @@ document.getElementById('use-different').addEventListener('click', () => {
|
||||||
showStep('email');
|
showStep('email');
|
||||||
setTimeout(() => emailInput.focus(), 300);
|
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');
|
||||||
|
})();
|
||||||
|
|
|
||||||
19
public/fenja/fenja-wordmark-black.svg
Normal file
19
public/fenja/fenja-wordmark-black.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
3
public/fenja/man_woman_talking_transparent.svg
Normal file
3
public/fenja/man_woman_talking_transparent.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 456 KiB |
3
public/fenja/men_by_computer_transparent.svg
Normal file
3
public/fenja/men_by_computer_transparent.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 457 KiB |
3
public/fenja/six_people_discussion_transparent.svg
Normal file
3
public/fenja/six_people_discussion_transparent.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 523 KiB |
19
server.js
19
server.js
|
|
@ -12,7 +12,6 @@ import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
import authRouter from './src/auth.js';
|
import authRouter from './src/auth.js';
|
||||||
import { requireAuth } from './src/middleware.js';
|
import { requireAuth } from './src/middleware.js';
|
||||||
import { currentSession } from './src/sessions.js';
|
|
||||||
import { initMail } from './src/mail.js';
|
import { initMail } from './src/mail.js';
|
||||||
import './src/db.js'; // side-effect import: opens DB + runs schema
|
import './src/db.js'; // side-effect import: opens DB + runs schema
|
||||||
|
|
||||||
|
|
@ -74,18 +73,18 @@ app.use((req, res, next) => {
|
||||||
app.use('/auth', authRouter);
|
app.use('/auth', authRouter);
|
||||||
|
|
||||||
// ─── Root dispatch ───────────────────────────────────────────
|
// ─── Root dispatch ───────────────────────────────────────────
|
||||||
// GET / → timeline (if authed) | entrance (otherwise)
|
// GET / → always the entrance shell. If authed, entrance.js routes
|
||||||
// GET /entrance → always the entrance (useful for "log in as someone else")
|
// the user to the welcome step client-side (preserving the
|
||||||
// Other paths fall through to the static handlers below.
|
// email/code UI as the no-session fallback).
|
||||||
app.get('/', (req, res, next) => {
|
// GET /timeline → gated timeline page (protected/index.html).
|
||||||
if (currentSession(req)) {
|
app.get('/', (req, res) => {
|
||||||
// Authed: serve the timeline directly from /protected/index.html
|
|
||||||
return res.sendFile(path.join(__dirname, 'protected', 'index.html'));
|
|
||||||
}
|
|
||||||
// Not authed: serve the entrance
|
|
||||||
return res.sendFile(path.join(__dirname, 'public', 'entrance.html'));
|
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.) ────────────────
|
// ─── Public static assets (entrance.js, etc.) ────────────────
|
||||||
// Fallthrough so Express can still try the routes below if nothing matches.
|
// Fallthrough so Express can still try the routes below if nothing matches.
|
||||||
app.use(
|
app.use(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue