product-pages: add deepdive subpage and platform assets

Adds a new standalone /deepdive page with its own platform.css/platform.js,
wires it into the dot-nav as an external entry, and updates the dot-nav
docblock to reflect the new seven-entry layout. Also drops in BUSINESS.md
and reference material under architecture boxes/ and examples/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Hvid 2026-05-07 09:28:23 +02:00
parent 9f742928d5
commit 547515061c
22 changed files with 2263 additions and 9 deletions

210
BUSINESS.md Normal file
View file

@ -0,0 +1,210 @@
# Fenja AI — Business Description
*A synthesis of the company and products described on the project-bifrost.fenja.ai web experience. Drawn entirely from on-site copy: the entrance, timeline, overview, and product deepdive pages.*
---
## 1. At a glance
**Fenja AI** is a Danish company **and** an AI platform — one organisation, one product, one name. The platform is **client-managed**: it installs into the customer's own environment (on-prem or in their controlled cloud), runs on their hardware, and never sends data to a foreign provider. It is positioned as **"trusted, sovereign AI built in Denmark, for Europe"** and is backed by **Innovationsfonden** (the Danish national Innovation Fund).
The platform brings **language models, organisational knowledge, tools, and agents** together in a single architecture so that a customer can not just run an LLM, but actually get work done with AI under their own governance.
A parallel initiative, **Project Bifrost**, exists alongside the platform. It is the engagement programme through which a curated group of Danish and European organisations help shape Fenja's product roadmap and gain early access to the platform at a subsidised price.
---
## 2. The strategic thesis
Fenja AI's pitch is built on a sustained argument that **European digital sovereignty is no longer a procurement preference — it is a security imperative**. The site presents this argument as a 13-event timeline running from September 2024 through Q1 2026, drawn from public reporting on Danish and European institutions, the U.S. CLOUD Act, the Trump tariffs, and the migration of the International Criminal Court off Microsoft.
The thesis can be summarised in five claims:
1. **Concentration risk is now critical.** Three American firms run roughly 70% of Europe's cloud and almost all of its AI capacity (per the September 2024 Draghi Report).
2. **The "kill switch" is real, not theoretical.** In May 2025, Microsoft disabled the email of the ICC chief prosecutor in compliance with a U.S. executive order. In June 2025, Microsoft testified under oath to the French Senate that it could not guarantee European sovereignty even for data stored on European soil with European staff and European keys, because of the U.S. CLOUD Act.
3. **European governments now treat dependence as a vulnerability.** Copenhagen's Microsoft bill rose 72% in five years (313→538M DKK). Copenhagen and Aarhus announced full Microsoft exits. The Danish Minister of Emergency Preparedness has urged every Danish company to "create exit plans for cloud services". The official Danish threat assessment in January 2026 listed the U.S. alongside Russia and China for the first time in history.
4. **Tariffs and trade weaponisation make U.S. dependency a foreign-policy risk.** The January 2026 tariffs on Denmark and seven European nations were tied to demands over Greenland. The DSA, DMA, and AI Act have been reframed by the U.S. administration as discriminatory trade barriers.
5. **Regulation alone will not close the gap.** Europe is "writing rules for infrastructure it does not own" — U.S. hyperscalers add €10B/quarter of European capacity, more than Gaia-X spent in a decade. **Sovereign capacity has to be built, not legislated.**
Fenja AI positions itself as the practical answer to that gap for regulated and security-sensitive Danish and European organisations: not a research project, not an EU policy lever, but a working AI platform a customer can install and run today.
The site's framing question — *"When AI runs Europe, who runs the AI?"* — captures the pitch in one line.
---
## 3. The company
**Fenja AI** is the company name and the product name. The site is explicit that this is intentional: *"Fenja AI is both our company and our platform — one mission, one name."*
Stated identity:
- **Origin:** Built in Denmark.
- **Audience:** Danish and European organisations that need full control over their own AI.
- **Backer:** Innovationsfonden (the Danish Innovation Fund). The phrase "Backed by Innovationsfonden" appears on the welcome screen, the hero, and the join footer; the pilot subsidy explicitly references the same fund.
- **Stance:** Trusted, sovereign, client-managed. Throughout the site, "client-managed" is the operative phrase — Fenja is not a SaaS, not a hyperscaler resale, not a hosted product. It runs in the customer's environment.
The site does not name founders, headcount, or revenue figures.
---
## 4. The platform — architecture in four layers
The site introduces the platform as **"The Fenja AI platform in four steps"** and reinforces three parallel framings:
- **One complete platform:** *"Fenja AI brings models, knowledge, tools, and agents together in one platform for using and scaling AI across your organisation."*
- **Full control:** *"Fenja AI is installed in your own client-managed environment, giving you full control over data, security, and governance."*
- **Sovereignty:** *"Fenja AI is built in Denmark for European organisations that want trusted, sovereign AI on their own terms."*
The architecture itself is then described both at the timeline-overview level (four layers) and in the deepdive (a more detailed five-beat build with specific cards inside each layer). The deepdive version is the canonical, more-precise statement.
### Layer 1 — Foundation: the language model
A **state-of-the-art, open-source language model**, deployed **on-prem** in the customer's environment. No data leaves the customer's perimeter. The site frames this as the necessary starting point but explicitly *not yet Fenja* on its own — *"Installing an open-source language model isn't enough."*
### Layer 2 — Foundation: knowledge
What turns a generic open-source model into *Fenja*. Two sub-components are called out as cards:
- **Wiki** — Company and domain knowledge captured in a wiki the customer's team can read and edit. Three scopes: **organisational, departmental, personal**.
- **Routines & memory** — How Fenja works inside the organisation: **stand-ups, recurring tasks, working memory**. The framing positions Fenja as "a coworker who knows how things get done", not a chatbot.
### Layer 3 — Tools: how Fenja acts
Four tool families are listed:
- **Document retrieval** — Find and cite. (RAG.)
- **Structured data (e.g. SQL)** — Query and extract. Natural language → SQL.
- **System actions** — Read and write across the customer's systems. APIs and integrations.
- **Custom tools** — Defined by the customer for their specific work.
The framing: *"How knowledge becomes work. Fenja uses tools to find documents, query data, take action across your systems. Some are obvious; others depend on what your work needs."*
### Layer 4 — Agents: how Fenja scales
Four agent constructs:
- **Supervisor** — Plan and dispatch. Orchestration.
- **Specialists** — Focused expertise. Subagents.
- **Skills** — Reusable capability, portable across specialists.
- **Workflows** — Composed by the customer. Governed end-to-end.
The framing: *"Real work isn't one task. Fenja becomes a team — a supervisor and specialists, each focused, each governed, all dispatched by workflows you've designed."*
### The summary claim
Once the four layers are assembled, the deepdive closes with: *"Everything you need and with full control. Fenja brings together all the pieces to solve simple and complex AI use cases across your organisation."*
This is the core differentiation argument: not "we sell you an LLM" and not "we sell you a chatbot", but a complete on-prem stack from foundation model up through governed multi-agent workflows.
---
## 5. The products — four capabilities
Fenja is sold as four named capability tiers that build on each other. The deployment story: a customer starts with **Core** and adds **Dev**, **Analyze**, and/or **Agentic** as their needs grow. The dark-tile **Agentic** tier is the full bundle.
### Fenja Core
**Tier:** Foundational.
**Includes:** Essential LLM capabilities with Fenja Semantic — a safe and custom chatbot that understands the customer's organisation.
**Audience:** Any customer starting their sovereign-AI journey. The baseline.
### Fenja Dev
**Tier:** Developer toolset.
**Includes:** Fenja Core, plus an AI-supported development platform.
**Promise:** *"Code faster and better with your own secure AI-supported development platform."*
**Audience:** Engineering teams who want a coding-assistant capability without sending source code to an external provider.
### Fenja Analyze
**Tier:** Strategic intel.
**Includes:** Fenja Core, plus an analytics and insights capability.
**Promise:** *"Bring real insights to your people. You ask for an insight, and your agents will find, analyze, and present the relevant data."*
**Audience:** Analysts, decision-makers, knowledge workers — anyone whose value comes from synthesising data across the organisation.
### Fenja Agentic
**Tier:** Automation. The complete framework.
**Includes:** Core + Dev + Analyze.
**Promise:** *"The complete framework. Fully governed and controlled agents collaborate to solve your most important processes."*
**Audience:** Customers ready to operationalise AI as governed multi-agent workflows across critical processes — not just assist humans, but run end-to-end work under human oversight.
---
## 6. Project Bifrost — the engagement programme
**Project Bifrost** is positioned as a parallel initiative, not a product:
> *"Project Bifrost is the bridge between an industrial-grade AI platform and the realities of regulated organisations — built with them, not just for them."*
The Norse-mythology naming is deliberate: Bifrost is the bridge between worlds. In Fenja's framing, the two worlds are (a) a fast-moving, industrial-grade AI platform and (b) the slower-moving operational, legal, and security realities of regulated European organisations. Project Bifrost is the structured way the two are reconciled in product design.
Members of Project Bifrost get **three things** — and the site is explicit that these are inseparable, not a menu:
### 6.1 Community — shape the future together
> *"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."*
A peer network of like-minded Danish and European organisations engaged in the same sovereignty problem. The framing emphasises **trust, shared ambition, and a common mission** rather than transactional vendor/customer dynamics.
### 6.2 Advisory Council — turn insight into influence
> *"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."*
A formal feedback channel into the product. Members influence which capabilities are prioritised, how governance is built in, how the platform fits into regulated environments. This is the mechanism that makes the "*built with you, not just for you*" claim concrete.
### 6.3 Pilot Projects — access the platform before others
> *"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."*
Early commercial access. Crucially, the **pricing is subsidised by Innovationsfonden**, which materially lowers the cost of the pilot for the participant and is part of why participation is positioned as "selectivity" rather than a sales motion.
### 6.4 What happens after a customer joins
The post-join confirmation panel commits to a specific sequence:
1. The Fenja AI team reaches out shortly.
2. The participant receives an invitation to the **project portal**, where all communication, materials, and updates live.
3. A date is being set for the **first advisory council meeting**; the participant will be invited.
4. Pilot-project participation is discussed individually shortly after.
---
## 7. The invitation framing
The web experience itself is invite-gated — visitors land on an email entry, the email is checked against an invite list, and only invited contacts get past the welcome step. The welcome copy makes this explicit:
> *"This is a personal invitation because we believe your perspective can make a meaningful contribution to an important mission: building trusted, sovereign AI for Denmark and Europe. In this short web experience, we will explain why this matters, what Fenja AI is, and how you, through Project Bifrost, can help shape its future."*
Two terms are then defined for the visitor:
- **Fenja AI** — *"The company and platform for sovereign and safe AI."*
- **Project Bifrost** — *"The initiative created to ensure that Fenja AI is built not just for organisations like yours, but with you."*
This invite-only positioning reinforces the selectivity narrative around Project Bifrost: it is not a sign-up form, it is a curated cohort.
---
## 8. Differentiation — what Fenja is not
The deepdive's framing question makes the negative space explicit:
> *"Renting a few AI capabilities from American companies isn't enough. Installing an open-source language model isn't enough. You need a platform you control — with the tools, the knowledge, and the framework to make AI actually do the work your organization needs done."*
This positions Fenja against three implicit alternatives:
1. **U.S. AI APIs (OpenAI, Anthropic, etc.)** — rejected on sovereignty grounds. The CLOUD Act narrative on the timeline is the supporting argument: even European-soil U.S.-vendor data is reachable by U.S. authorities.
2. **DIY open-source LLM deployment** — acknowledged as necessary but insufficient. A model alone is "the starting point — but not yet Fenja". Without the knowledge layer, tool integrations, and agent framework, the customer has running infrastructure but no actual capability to do work.
3. **EU-policy-led sovereignty (Gaia-X, regulation)** — implicitly framed as too slow on the timeline ("Europe writes rules for infrastructure it does not own"). Fenja's argument is that sovereign capacity has to be **built and shipped**, not merely legislated.
Fenja's positive claim is the combination: **on-prem foundation model + organisational knowledge + tool integrations + governed agent framework**, all from one vendor, all in one client-managed deployment, all under the customer's control.
---
## 9. Backing and provenance
- **Innovationsfonden** (Innovation Fund Denmark) — the consistent backer reference across every page of the site. It appears in the entrance footer, the platform hero, the join footer, and is named explicitly in the Pilot Projects pricing description.
- **Built in Denmark** — surfaced in the hero, the architecture rotating taglines, and the join footer ("Built in Denmark. Supported by the Innovation Fund.").
- **For Europe** — the consistent geographic scope. Customer language is always "Danish and European organisations", and the strategic thesis is European, not just Danish.
---
## 10. Summary in one paragraph
**Fenja AI is a Danish-built, client-managed AI platform for Danish and European organisations that need full control over their own AI. The platform combines an on-prem open-source language model, an organisational knowledge layer (wiki, routines, working memory), a tools layer (document retrieval, structured-data query, system actions, custom tools), and an agent framework (supervisor, specialists, skills, workflows) into a single architecture deployed inside the customer's environment. It is sold as four progressively-bundled capabilities — Fenja Core, Fenja Dev, Fenja Analyze, and Fenja Agentic. A parallel initiative, Project Bifrost, brings a curated group of regulated organisations into the product's design through a community, an advisory council, and subsidised pilot projects funded by Innovationsfonden. The pitch is grounded in a concrete strategic argument: U.S. cloud and AI dependency is now a security and trade-policy risk for Europe, regulation alone will not close the gap, and sovereign AI capacity has to be built in-region — which is what Fenja AI is.**

View file

@ -0,0 +1,268 @@
# Handoff: Fenja architecture diagram
## Overview
A **right-side architecture diagram** for a section of the Fenja AI website. The left side of the section will hold explanatory prose (the developer adds this in their codebase). The right side is this stacked, three-layer diagram showing the Fenja system at a glance: **Foundation → Tools → Agents**.
The diagram is the visual anchor of the section. As the user reads down the left column, each paragraph cues the next layer of the diagram to enter — creating a guided "build" of the architecture in concert with the prose.
---
## About the design files
The files in `reference/` are a **design reference** — a working HTML/React prototype that shows the intended look, layout, and entry animation. They are **not production code to copy verbatim**.
- `reference/preview.html` — open this in a browser to see the diagram rendered.
- `reference/approach-fenja.jsx` — the React component (inline JSX, transpiled by Babel in the prototype). Use it as a structural and styling reference.
- `reference/colors_and_type.css` — Fenja design tokens (CSS variables) and base type styles. Use these tokens — do not invent new ones.
- `reference/fonts/` — Manrope (sans) and Newsreader (serif) self-hosted TTFs. The diagram **requires both families** — Newsreader for italic descriptors, Manrope for everything else.
Recreate the design in your existing environment (Next.js, React, Vue, Astro, whatever the Fenja site uses), wired up to the site's real design tokens and motion primitives.
## Fidelity
**High-fidelity.** Colors, typography, spacing, and corner radii are final. Recreate pixel-perfectly using the established Fenja design tokens.
---
## What this diagram is
A vertically stacked diagram with three layers, top to bottom:
```
┌──────────────────────────────────────────────────────────┐
│ FOUNDATION Sovereign by design │
│ ┌──────────────────────┐ ┌──────────────────────────┐ │
│ │ Language model │ │ Knowledge (taller) │ │
│ │ italic descriptor │ │ italic descriptor │ │
│ │ mono · tag │ │ mono · tag │ │
│ │ │ │ italic descriptor │ │
│ │ │ │ mono · tag │ │
│ └──────────────────────┘ └──────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ TOOLS How Fenja acts │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ 4 boxes, equal width and height │
│ └────┘ └────┘ └────┘ └────┘ │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ AGENTS How Fenja scales │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ 4 boxes, equal width and height │
│ └────┘ └────┘ └────┘ └────┘ │
└──────────────────────────────────────────────────────────┘
```
### Critical asymmetries
- **Foundation has 2 boxes** (narrower row) — the irreducible base.
- **Tools and Agents each have 4 boxes** (full row) — they extend the base.
- Within Foundation, the **Knowledge box is intentionally taller** than Language model (5 content rows vs 2). Do not equalize them. The asymmetry is the point.
- Within Tools and Agents, all 4 boxes are **equal height** (stretch). Do not let content drift the heights.
### No connecting lines
The first pass has **no lines between layers**. Stacking + vertical order communicates "lower layers depend on what's above." If that's unclear after live testing with copy, a second pass can add subtle connectors.
---
## Layer content (exact)
Use this content verbatim. Do not paraphrase.
### Layer 1 — Foundation
- Eyebrow (top-left of layer): `Foundation` (rendered uppercase via `text-transform: uppercase`)
- Descriptor (top-right of layer, serif italic): `Sovereign by design`
**Box 1 — Language model**
- Bold name: `Language model`
- Italic role: `State-of-the-art, open-source`
- Mono tag: `On-prem`
**Box 2 — Knowledge** (taller — 5 content rows, no min-height equalization)
- Bold name: `Knowledge`
- Italic: `Organizational memory at every level`
- Mono: `Organizational · Departmental · Personal`
- Italic: `Facts, context, and how things get done`
- Mono: `Wiki · Routines · Working memory`
### Layer 2 — Tools
- Eyebrow: `Tools`
- Descriptor: `How Fenja acts`
| Bold name | Italic role | Mono tag |
|---|---|---|
| Document retrieval | Find and cite | RAG |
| Structured data | Query and extract | NL → SQL |
| System actions | Read and write | APIs · integrations |
| Custom tools | Your specific work | Defined by you |
### Layer 3 — Agents
- Eyebrow: `Agents`
- Descriptor: `How Fenja scales`
| Bold name | Italic role | Mono tag |
|---|---|---|
| Supervisor | Plan and dispatch | Orchestration |
| Specialists | Focused expertise | Subagents |
| Skills | Reusable capability | Portable across specialists |
| Workflows | Composed by you | Governed end-to-end |
---
## Layout & sizing
The diagram is meant to occupy the right column of a two-column section (roughly 50% of the viewport width on desktop, ~600760px wide). It is fully fluid — it scales to its container.
| Element | Spec |
|---|---|
| Outer container | `padding: 56px 56px 64px` on the standalone prototype. In the site, drop the outer padding and let the section's own grid drive margins. |
| Layer (group) | `background: var(--surface-container)` · `border-radius: 20px` · `padding: 24px 24px 28px` |
| Layer-to-layer gap | `20px` (vertical flex gap) |
| Layer header | flex row, baseline-aligned, eyebrow left + italic descriptor right, `margin-bottom: 18px` |
| Eyebrow label | Manrope · 11px · weight 600 · `letter-spacing: 0.14em` · uppercase · `color: var(--on-surface-variant)` |
| Italic descriptor | Newsreader italic · 14px · `color: var(--on-surface-muted)` |
| Card grid | `display: grid` · `gap: 14px` · 2 cols (Foundation) or 4 cols (Tools, Agents) |
| Card grid alignment | **Foundation: `align-items: start`** (preserves Knowledge's taller height) · **Tools & Agents: `align-items: stretch`** (equal heights within row) |
| Card | `background: var(--surface-container-lowest)` · `border-radius: 12px` · `padding: 18px 20px` · column flex · `gap: 6px` · `min-height: 76px` · `box-shadow: 0 1px 0 rgba(56,56,49,0.04), 0 8px 20px -16px rgba(56,56,49,0.18)` |
| Card name | Manrope · 15px · weight 600 · `letter-spacing: -0.005em` · `color: var(--on-surface)` |
| Italic row | Newsreader italic · 13px · `line-height: 1.35` · `color: var(--on-surface-variant)` |
| Mono row | Mono stack · 10.5px · `letter-spacing: 0.04em` · `line-height: 1.4` · `color: var(--on-surface-muted)` |
**No borders anywhere.** Containment is expressed entirely through the tonal step from `--surface-container` (group) to `--surface-container-lowest` (card). The card's tiny ambient shadow is the only depth cue.
---
## Design tokens (use existing Fenja tokens; do not invent)
These come from `colors_and_type.css`. Map to whatever variable system the codebase uses.
```
--surface #faf6ee /* page canvas */
--surface-container #efeadc /* group / layer background */
--surface-container-lowest #fffcf7 /* card background */
--on-surface #383831 /* card name, primary text */
--on-surface-variant #5f5e5e /* italic role, eyebrow */
--on-surface-muted #8a887f /* mono tag, descriptor */
--font-sans Manrope, Inter, system-ui (weights 400, 500, 600 used)
--font-serif Newsreader, Source Serif Pro, Georgia (italic, 400)
--font-mono JetBrains Mono, IBM Plex Mono, ui-monospace
--radius-md 12px (cards)
--radius-lg 20px (groups)
```
---
## Entry animation
The user requested: **"each element jumps in together with text I'll add in Claude Code."**
### Intent
As the section scrolls into view, the diagram builds layer-by-layer in step with the prose on the left. Each paragraph the reader passes "summons" the corresponding layer.
### Mechanism (recommended)
Use whatever scroll-trigger primitive the site already uses (Framer Motion `whileInView`, GSAP ScrollTrigger, IntersectionObserver, etc.). Do not introduce a new motion library just for this.
### Animation values — match Fenja system motion
The Fenja design system has **one easing** and **page entrances are fade + 4px translate-up, never scale**. Use those.
```
duration: 240ms (use --duration-med)
easing: cubic-bezier(0.2, 0.0, 0, 1) (use --ease-standard)
from: opacity: 0; transform: translateY(8px)
to: opacity: 1; transform: translateY(0)
```
Note the design system says 4px translate-up, but for cards inside a build sequence 8px reads better. Stay within 410px.
### Choreography
Three trigger points, each tied to a paragraph on the left:
| Trigger | Reveals | Stagger |
|---|---|---|
| Paragraph 1 in view | Foundation layer (group + 2 cards) | Layer fades, then cards stagger 60ms each |
| Paragraph 2 in view | Tools layer (group + 4 cards) | Same — group, then cards 60ms apart |
| Paragraph 3 in view | Agents layer (group + 4 cards) | Same |
Within a single layer, the **group container** (with eyebrow + descriptor) appears first, then its cards stagger in left-to-right.
### Reduced motion
Honor `prefers-reduced-motion: reduce` — fade in over 120ms, no translate.
### What to wire on the developer side
The component should expose a controllable visibility state per layer, e.g.:
```tsx
<FenjaArchitecture
visibleLayers={{ foundation: true, tools: paragraph2InView, agents: paragraph3InView }}
/>
```
…or accept three IntersectionObserver refs (one per paragraph) and drive its own visibility internally. Either is fine — pick whichever suits the codebase's conventions.
---
## Behavior when the section is statically rendered (no JS)
If the page renders without JS (SSR, RSS preview, screenshot tool, print), all three layers must be visible. Animation is a progressive enhancement — the static state is the final state, and the animation should ramp from that final state's opposite, not block on JS to render content. (Translation: render at full opacity by default and let the animation library set initial opacity:0 only when it's ready.)
---
## State
No app state. Pure presentational component. All content is static; nothing is fetched.
If the prose paragraphs and the diagram need to coordinate, expose a single `currentLayer: 'foundation' | 'tools' | 'agents' | null` prop or use shared IntersectionObserver hooks — implementer's choice.
---
## Assets
None. The diagram is text-on-paper — no icons, no images, no SVG illustrations.
---
## Files in this bundle
```
design_handoff_architecture_diagram/
├── README.md ← you are here
└── reference/
├── preview.html ← open this to see the diagram render
├── approach-fenja.jsx ← the React reference component
├── colors_and_type.css ← Fenja design tokens (variables + base styles)
└── fonts/ ← Manrope + Newsreader TTFs (self-hosted)
```
---
## Implementation checklist
- [ ] Component scaffolded in the codebase's preferred framework (React, Vue, etc.)
- [ ] All three layers render with the exact copy above
- [ ] Foundation: Knowledge box visibly taller than Language model (do not equalize)
- [ ] Tools: 4 cards flush-equal in height (`align-items: stretch`)
- [ ] Agents: 4 cards flush-equal in height (`align-items: stretch`)
- [ ] Eyebrow + italic descriptor in the header of every layer
- [ ] Mono tags use the site's monospace stack (do not substitute regular text)
- [ ] Bullet separator `·` (interpunct U+00B7) used in mono tags — not `•` or `-`
- [ ] No borders anywhere (containment = tonal step only)
- [ ] No connecting lines between layers
- [ ] No icons inside boxes
- [ ] Entry animation: fade + 8px translate-up, 240ms, `cubic-bezier(0.2, 0, 0, 1)`
- [ ] Per-layer trigger tied to corresponding left-column paragraph entering viewport
- [ ] Cards within a layer stagger 60ms left-to-right
- [ ] `prefers-reduced-motion: reduce` honored
- [ ] Static render is fully visible without JS

View file

@ -0,0 +1,196 @@
/* eslint-disable */
// Fenja architecture right-side diagram for the section page.
// Sibling of Approach 1 (Editorial Stack). Same paper-on-paper
// language: tonal nested cards, no borders, eyebrow + italic
// descriptor in the group header. No connectors between layers.
//
// Three layers, top bottom: FOUNDATION TOOLS AGENTS.
// Foundation has 2 boxes (Knowledge intentionally taller).
// Tools and Agents each have 4 boxes.
const fenjaArchData = {
layers: [
{
id: "foundation",
label: "Foundation",
caption: "Sovereign by design",
columns: 2,
align: "start", // intentional asymmetry Knowledge is taller
cards: [
{
id: "model",
name: "Language model",
rows: [
{ kind: "italic", text: "State-of-the-art, open-source" },
{ kind: "mono", text: "On-prem" },
],
},
{
id: "knowledge",
name: "Knowledge",
rows: [
{ kind: "italic", text: "Organizational memory at every level" },
{ kind: "mono", text: "Organizational · Departmental · Personal" },
{ kind: "italic", text: "Facts, context, and how things get done" },
{ kind: "mono", text: "Wiki · Routines · Working memory" },
],
},
],
},
{
id: "tools",
label: "Tools",
caption: "How Fenja acts",
columns: 4,
cards: [
{ id: "retrieval", name: "Document retrieval",
rows: [{ kind: "italic", text: "Find and cite" }, { kind: "mono", text: "RAG" }] },
{ id: "structured", name: "Structured data",
rows: [{ kind: "italic", text: "Query and extract" }, { kind: "mono", text: "NL → SQL" }] },
{ id: "actions", name: "System actions",
rows: [{ kind: "italic", text: "Read and write" }, { kind: "mono", text: "APIs · integrations" }] },
{ id: "custom", name: "Custom tools",
rows: [{ kind: "italic", text: "Your specific work" }, { kind: "mono", text: "Defined by you" }] },
],
},
{
id: "agents",
label: "Agents",
caption: "How Fenja scales",
columns: 4,
cards: [
{ id: "supervisor", name: "Supervisor",
rows: [{ kind: "italic", text: "Plan and dispatch" }, { kind: "mono", text: "Orchestration" }] },
{ id: "specialists", name: "Specialists",
rows: [{ kind: "italic", text: "Focused expertise" }, { kind: "mono", text: "Subagents" }] },
{ id: "skills", name: "Skills",
rows: [{ kind: "italic", text: "Reusable capability" }, { kind: "mono", text: "Portable across specialists" }] },
{ id: "workflows", name: "Workflows",
rows: [{ kind: "italic", text: "Composed by you" }, { kind: "mono", text: "Governed end-to-end" }] },
],
},
],
};
const fenjaStyles = {
artboard: {
width: "100%",
minHeight: "100%",
background: "var(--surface)",
padding: "56px 56px 64px",
fontFamily: "var(--font-sans)",
color: "var(--on-surface)",
boxSizing: "border-box",
},
stage: {
display: "flex",
flexDirection: "column",
gap: "20px",
},
group: {
background: "var(--surface-container)",
borderRadius: "20px",
padding: "24px 24px 28px",
},
groupHeader: {
display: "flex",
alignItems: "baseline",
justifyContent: "space-between",
marginBottom: "18px",
paddingLeft: "4px",
},
groupLabel: {
fontFamily: "var(--font-sans)",
fontSize: "11px",
fontWeight: 600,
letterSpacing: "0.14em",
textTransform: "uppercase",
color: "var(--on-surface-variant)",
},
groupCaption: {
fontFamily: "var(--font-serif)",
fontStyle: "italic",
fontSize: "14px",
color: "var(--on-surface-muted)",
},
cards: {
display: "grid",
gap: "14px",
},
cardsStretch: { alignItems: "stretch" },
cardsStart: { alignItems: "start" },
card: {
background: "var(--surface-container-lowest)",
borderRadius: "12px",
padding: "18px 20px",
display: "flex",
flexDirection: "column",
gap: "6px",
minHeight: "76px",
justifyContent: "flex-start",
boxShadow: "0 1px 0 rgba(56,56,49,0.04), 0 8px 20px -16px rgba(56,56,49,0.18)",
},
cardName: {
fontFamily: "var(--font-sans)",
fontSize: "15px",
fontWeight: 600,
letterSpacing: "-0.005em",
color: "var(--on-surface)",
marginBottom: "2px",
},
rowItalic: {
fontFamily: "var(--font-serif)",
fontStyle: "italic",
fontSize: "13px",
color: "var(--on-surface-variant)",
lineHeight: 1.35,
},
rowMono: {
fontFamily: "var(--font-mono)",
fontSize: "10.5px",
letterSpacing: "0.04em",
color: "var(--on-surface-muted)",
lineHeight: 1.4,
},
};
function FenjaArchitecture({ data = fenjaArchData }) {
return (
<div style={fenjaStyles.artboard}>
<div style={fenjaStyles.stage}>
{data.layers.map((layer) => (
<section key={layer.id} style={fenjaStyles.group}>
<div style={fenjaStyles.groupHeader}>
<div style={fenjaStyles.groupLabel}>{layer.label}</div>
<div style={fenjaStyles.groupCaption}>{layer.caption}</div>
</div>
<div
style={{
...fenjaStyles.cards,
...(layer.align === "start" ? fenjaStyles.cardsStart : fenjaStyles.cardsStretch),
gridTemplateColumns: `repeat(${layer.columns}, 1fr)`,
}}
>
{layer.cards.map((c) => (
<div key={c.id} style={fenjaStyles.card}>
<div style={fenjaStyles.cardName}>{c.name}</div>
{c.rows.map((r, i) => (
<div
key={i}
style={r.kind === "mono" ? fenjaStyles.rowMono : fenjaStyles.rowItalic}
>
{r.text}
</div>
))}
</div>
))}
</div>
</section>
))}
</div>
</div>
);
}
window.FenjaArchitecture = FenjaArchitecture;
window.FENJA_ARCH_DATA = fenjaArchData;

View file

@ -0,0 +1,346 @@
/* =============================================================
Fenja AI Nordic Editorial Design System
"The Digital Archivist"
============================================================= */
/* ---------- Fonts ------------------------------------------ */
@font-face {
font-family: "Manrope";
font-weight: 200;
font-style: normal;
font-display: swap;
src: url("./fonts/Manrope-ExtraLight.ttf") format("truetype");
}
@font-face {
font-family: "Manrope";
font-weight: 300;
font-style: normal;
font-display: swap;
src: url("./fonts/Manrope-Light.ttf") format("truetype");
}
@font-face {
font-family: "Manrope";
font-weight: 400;
font-style: normal;
font-display: swap;
src: url("./fonts/Manrope-Regular.ttf") format("truetype");
}
@font-face {
font-family: "Manrope";
font-weight: 500;
font-style: normal;
font-display: swap;
src: url("./fonts/Manrope-Medium.ttf") format("truetype");
}
@font-face {
font-family: "Manrope";
font-weight: 600;
font-style: normal;
font-display: swap;
src: url("./fonts/Manrope-SemiBold.ttf") format("truetype");
}
@font-face {
font-family: "Manrope";
font-weight: 700;
font-style: normal;
font-display: swap;
src: url("./fonts/Manrope-Bold.ttf") format("truetype");
}
@font-face {
font-family: "Manrope";
font-weight: 800;
font-style: normal;
font-display: swap;
src: url("./fonts/Manrope-ExtraBold.ttf") format("truetype");
}
@font-face {
font-family: "Newsreader";
font-weight: 400;
font-style: normal;
font-display: swap;
src: url("./fonts/Newsreader-Regular.ttf") format("truetype");
}
@font-face {
font-family: "Newsreader";
font-weight: 400;
font-style: italic;
font-display: swap;
src: url("./fonts/Newsreader-Italic.ttf") format("truetype");
}
@font-face {
font-family: "Newsreader";
font-weight: 700;
font-style: normal;
font-display: swap;
src: url("./fonts/Newsreader-Bold.ttf") format("truetype");
}
@font-face {
font-family: "Newsreader";
font-weight: 700;
font-style: italic;
font-display: swap;
src: url("./fonts/Newsreader-BoldItalic.ttf") format("truetype");
}
@font-face {
font-family: "Newsreader";
font-weight: 800;
font-style: normal;
font-display: swap;
src: url("./fonts/Newsreader-ExtraBold.ttf") format("truetype");
}
/* ---------- Tokens ----------------------------------------- */
:root {
/* --- Core neutrals (unbleached paper, clay, slate) --- */
--background: #faf6ee; /* base canvas — warm paper */
--surface: #faf6ee;
--surface-container-lowest: #fffcf7; /* most-elevated — unbleached paper, never pure white */
--surface-container-low: #f6f2e8;
--surface-container: #efeadc;
--surface-container-high: #e7e1d0;
--surface-container-highest: #ddd6c3;
--surface-variant: #ddd6c3;
--on-surface: #383831; /* charcoal slate */
--on-surface-variant: #5f5e5e;
--on-surface-muted: #8a887f;
--primary: #5f5e5e;
--on-primary: #fffcf7;
--secondary: #785f53; /* hand-rubbed wood */
--secondary-dim: #6b5348;
--on-secondary: #ffffff;
--secondary-fixed-dim: #9a8679;
--outline: #babab0;
--outline-variant: #babab0; /* used at 15% for ghost borders */
/* --- Archival Pigment accent palette (flat, matte inks) --- */
--pigment-terracotta: #b96b58; /* warnings, critical */
--pigment-copper: #6d8c7c; /* success, growth */
--pigment-ochre: #c29d59; /* cautions, tertiary */
--pigment-indigo: #5a6d83; /* info, neutral data */
--pigment-heather: #8d7a85; /* categorical, supportive */
/* --- Semantic state mappings --- */
--color-success: var(--pigment-copper);
--color-warning: var(--pigment-ochre);
--color-danger: var(--pigment-terracotta);
--color-info: var(--pigment-indigo);
/* --- Type families --- */
--font-serif: "Newsreader", "Source Serif Pro", Georgia, "Times New Roman", serif;
--font-sans: "Manrope", "Inter", -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
--font-mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
/* --- Type scale (clamped for responsive) --- */
--text-display-xl: clamp(3.5rem, 6vw, 5.5rem); /* 5688 */
--text-display-lg: clamp(3rem, 5vw, 4.5rem); /* 4872 */
--text-display-md: clamp(2.5rem, 4vw, 3.5rem); /* 4056 */
--text-headline-lg: 2.25rem; /* 36 */
--text-headline-md: 1.75rem; /* 28 */
--text-headline-sm: 1.375rem; /* 22 */
--text-title-lg: 1.125rem; /* 18 */
--text-title-md: 1rem; /* 16 */
--text-body-lg: 1.0625rem; /* 17 */
--text-body-md: 1rem; /* 16 */
--text-body-sm: 0.875rem; /* 14 */
--text-label-md: 0.8125rem; /* 13 */
--text-label-sm: 0.75rem; /* 12 */
/* Letter-spacing */
--tracking-tight: -0.02em;
--tracking-snug: -0.01em;
--tracking-normal: 0;
--tracking-wide: 0.04em;
--tracking-wider: 0.08em;
/* Line-heights */
--leading-tight: 1.1;
--leading-snug: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.6;
--leading-loose: 1.75;
/* --- Spacing scale (editorial, generous) --- */
--space-1: 0.25rem; /* 4 */
--space-2: 0.5rem; /* 8 */
--space-3: 0.75rem; /* 12 */
--space-4: 1rem; /* 16 */
--space-5: 1.5rem; /* 24 */
--space-6: 2rem; /* 32 — list separator default */
--space-7: 2.5rem; /* 40 */
--space-8: 2.75rem; /* 44 — hero-card padding */
--space-10: 4rem; /* 64 */
--space-12: 5rem; /* 80 */
--space-16: 6rem; /* 96 */
--space-20: 7rem; /* 112 — desktop lateral margin */
--space-24: 8rem; /* 128 */
/* --- Radii --- */
--radius-none: 0;
--radius-sm: 0.375rem; /* 6 */
--radius-md: 0.75rem; /* 12 — primary */
--radius-lg: 1.25rem; /* 20 */
--radius-full: 9999px;
/* --- Elevation (atmospheric, warm) --- */
--shadow-none: none;
--shadow-ambient: 0 12px 32px -12px rgba(56, 56, 49, 0.06);
--shadow-float: 0 24px 48px -16px rgba(56, 56, 49, 0.05), 0 4px 12px -4px rgba(56, 56, 49, 0.04);
--shadow-modal: 0 40px 64px -24px rgba(56, 56, 49, 0.08), 0 8px 16px -6px rgba(56, 56, 49, 0.04);
/* --- Ghost border (WCAG fallback only) --- */
--ghost-border-color: rgba(186, 186, 176, 0.15);
--ghost-border: 1px solid var(--ghost-border-color);
/* --- Glass --- */
--glass-blur: blur(16px);
--glass-surface: rgba(255, 252, 247, 0.8);
/* --- Motion --- */
--ease-standard: cubic-bezier(0.2, 0.0, 0, 1);
--ease-entrance: cubic-bezier(0, 0, 0, 1);
--ease-exit: cubic-bezier(0.3, 0, 1, 1);
--duration-fast: 140ms;
--duration-med: 240ms;
--duration-slow: 420ms;
/* --- Layout --- */
--content-max: 72rem; /* 1152 */
--reading-max: 42rem; /* 672 */
}
/* ---------- Base semantic styles --------------------------- */
html {
font-family: var(--font-sans);
color: var(--on-surface);
background: var(--background);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body {
margin: 0;
font-size: var(--text-body-md);
line-height: var(--leading-relaxed);
color: var(--on-surface);
background: var(--background);
}
/* Display — serif, tight, left-aligned editorial intent */
.display-xl,
.display-lg,
.display-md {
font-family: var(--font-serif);
font-weight: 400;
letter-spacing: var(--tracking-tight);
line-height: var(--leading-tight);
color: var(--on-surface);
margin: 0 0 var(--space-5) 0;
}
.display-xl { font-size: var(--text-display-xl); }
.display-lg { font-size: var(--text-display-lg); }
.display-md { font-size: var(--text-display-md); }
/* Headlines — serif, authoritative */
h1, .headline-lg,
h2, .headline-md,
h3, .headline-sm {
font-family: var(--font-serif);
font-weight: 400;
color: var(--on-surface);
letter-spacing: var(--tracking-snug);
line-height: var(--leading-snug);
margin: 0 0 var(--space-4) 0;
}
h1, .headline-lg { font-size: var(--text-headline-lg); }
h2, .headline-md { font-size: var(--text-headline-md); }
h3, .headline-sm { font-size: var(--text-headline-sm); }
/* Titles — sans, precise structural labels */
h4, .title-lg,
h5, .title-md {
font-family: var(--font-sans);
font-weight: 600;
color: var(--on-surface);
letter-spacing: var(--tracking-normal);
line-height: var(--leading-snug);
margin: 0 0 var(--space-3) 0;
}
h4, .title-lg { font-size: var(--text-title-lg); }
h5, .title-md { font-size: var(--text-title-md); }
/* Body */
p, .body-md {
font-family: var(--font-sans);
font-weight: 400;
font-size: var(--text-body-md);
line-height: var(--leading-relaxed);
color: var(--on-surface);
margin: 0 0 var(--space-4) 0;
text-wrap: pretty;
}
.body-lg {
font-size: var(--text-body-lg);
line-height: var(--leading-relaxed);
}
.body-sm {
font-size: var(--text-body-sm);
line-height: var(--leading-normal);
color: var(--on-surface-variant);
}
/* Labels — muted, small caps optional */
.label-md,
.label-sm {
font-family: var(--font-sans);
font-weight: 500;
color: var(--on-surface-variant);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
}
.label-md { font-size: var(--text-label-md); }
.label-sm { font-size: var(--text-label-sm); }
/* Editorial lead — serif italic, subtle */
.lead {
font-family: var(--font-serif);
font-style: italic;
font-size: var(--text-body-lg);
color: var(--on-surface-variant);
line-height: var(--leading-relaxed);
}
/* Inline code / mono */
code, kbd, samp, pre, .mono {
font-family: var(--font-mono);
font-size: 0.92em;
color: var(--on-surface);
}
/* Links — editorial, no underline until hover */
a {
color: var(--secondary);
text-decoration: none;
border-bottom: 1px solid rgba(120, 95, 83, 0.3);
transition: border-color var(--duration-fast) var(--ease-standard),
color var(--duration-fast) var(--ease-standard);
}
a:hover {
color: var(--secondary-dim);
border-bottom-color: currentColor;
}
/* Selection — warm, not blue */
::selection {
background: rgba(120, 95, 83, 0.18);
color: var(--on-surface);
}
/* Utility: ghost border fallback */
.ghost-border { border: var(--ghost-border); }
.ghost-border-bottom { border-bottom: var(--ghost-border); }

View file

@ -0,0 +1,41 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Fenja architecture diagram — reference</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="stylesheet" href="colors_and_type.css" />
<style>
html, body { height: 100%; margin: 0; background: var(--surface); }
body { font-family: var(--font-sans); }
.wrap {
max-width: 760px;
margin: 40px auto;
padding: 24px;
}
.note {
font-family: var(--font-serif);
font-style: italic;
font-size: 13px;
color: var(--on-surface-muted);
text-align: center;
margin-bottom: 24px;
}
</style>
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
</head>
<body>
<div class="wrap">
<div class="note">Reference rendering — implement in your codebase, do not ship this HTML.</div>
<div id="root"></div>
</div>
<script type="text/babel" src="approach-fenja.jsx"></script>
<script type="text/babel" data-presets="react">
const Fenja = window.FenjaArchitecture;
ReactDOM.createRoot(document.getElementById('root')).render(<Fenja />);
</script>
</body>
</html>

BIN
examples/capabilities.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

386
protected/deepdive.html Normal file
View file

@ -0,0 +1,386 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Fenja AI Platform — Product Deepdive</title>
<link rel="stylesheet" href="/fenja/colors_and_type.css" />
<link rel="stylesheet" href="/platform.css" />
<style>
/* ───── Page scaffolding ─────
Standalone subpage — no SPA page-switching. The deepdive section
is the only "page" here; its internal scroller
(#product-deepdive-scroll) handles all scrolling, so the body
itself stays clipped (matches the timeline page's pattern). */
*, *::before, *::after { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
height: 100%;
background: var(--background);
color: var(--on-surface);
font-family: var(--font-sans);
overflow: hidden;
-webkit-font-smoothing: antialiased;
}
body {
/* Same warm radial wash as the timeline page so the two surfaces
feel like the same paper. */
background:
radial-gradient(1200px 800px at 18% 45%,
var(--surface-container-lowest) 0%,
var(--background) 55%,
var(--surface-container-low) 100%);
}
/* Reproduce the .page / .is-active contract platform.js expects.
There's only one page here, so it's active from frame 0. */
.page {
position: fixed; inset: 0;
opacity: 0;
pointer-events: none;
transition: opacity 380ms cubic-bezier(0.2, 0, 0, 1);
}
.page.is-active {
opacity: 1;
pointer-events: auto;
}
/* Site wordmark — top-left, links back to the entrance. Mirrors the
masthead pattern from /timeline so the two pages feel paired. */
.site-mark,
.site-mark:link,
.site-mark:visited,
.site-mark:hover,
.site-mark:active,
.site-mark:focus,
.site-mark:focus-visible {
text-decoration: none;
border-bottom: 0;
}
.site-mark {
position: fixed;
top: 28px;
left: 36px;
width: 118px;
height: auto;
z-index: 50;
opacity: 0.85;
display: block;
cursor: pointer;
transition: opacity 200ms ease;
}
.site-mark img {
display: block;
width: 100%;
height: auto;
pointer-events: none;
}
.site-mark:hover,
.site-mark:focus-visible { opacity: 1; }
.site-mark:focus-visible {
outline: 2px solid #785f53;
outline-offset: 6px;
}
@media (max-width: 720px) {
.site-mark { width: 90px; top: 20px; left: 22px; }
}
</style>
</head>
<body data-screen-label="03 Product Deepdive">
<a class="site-mark" href="/timeline" aria-label="Back to the timeline">
<img src="/fenja/fenja-wordmark-black.svg" alt="Fenja" />
</a>
<!-- ───── PRODUCT DEEPDIVE — standalone page ─────
Self-contained page reached at /deepdive. Uses the same internal
scroller (#product-deepdive-scroll) the timeline's deepdive used,
so Lenis + ScrollTrigger.scrollerProxy can be wired without
touching the window scroll. All behaviour lives in /platform.js,
all styles in /platform.css. Pre-set `is-active` so platform.js's
init observer fires immediately on load. -->
<section class="page page-product-deepdive is-active" id="page-product-deepdive" data-screen-label="03 Product Deepdive">
<div id="product-deepdive-scroll">
<!-- ============================================================
"The Question" intro — first section of the Deepdive page.
Full-viewport framing statement, fades in on scroll like
the cards stagger.
============================================================ -->
<section id="platform-question" aria-labelledby="platform-question-head">
<div class="pq-wrap">
<!-- Two-part typographic rhythm: italic serif for the
parallel "isn't enough" rejection (problem), upright
serif for the affirmative resolution. Reads as one
flowing statement, not headline + sub. -->
<h2 id="platform-question-head" class="pq-title">
Renting a few AI capabilities from American companies isn't enough.<br>
Installing an open-source language model isn't enough.
</h2>
<p class="pq-body">
You need a <em>platform you control</em> &mdash; with the
tools, the knowledge, and the framework to make AI
actually do the work your organization needs done.
</p>
</div>
</section>
<!-- ============================================================
"The Layers" entry section — pinned scrubbed five-beat
build. Two columns inside the pin: a copy stage on the left
whose five text panels swap per beat, and an architecture
canvas on the right that assembles progressively (Foundation
in Beat 1+2, Tools in Beat 3, Agents in Beat 4; Beat 5 is
the closing summary against the fully assembled diagram).
A static title sits above the body from the moment the pin
engages.
Layout invariant: the canvas reserves its full final
assembled height from frame 0. Each .pl-group occupies its
final vertical slot from the start (opacity 0 until its
beat). Cards inside reveal in place — settled elements
never move.
All behaviour in /platform.js (initLayers), all styles in
/platform.css (.pl-* selectors).
============================================================ -->
<section id="platform-layers" aria-labelledby="platform-layers-head">
<h2 id="platform-layers-head" class="sr-only" style="position:absolute;left:-9999px;">The Fenja architecture, layer by layer</h2>
<div class="pl-pin">
<!-- HEADER — section title + subtitle. Statically rendered
from the moment the pin engages; visible before any
beat fires. Stays in place while the beats play below. -->
<header class="pl-pin-header">
<p class="pl-pin-title">Fenja AI Platform Architecture</p>
<p class="pl-pin-subtitle">Simply Explained</p>
</header>
<!-- BODY — two-column build. Copy stage (left) and the
diagram canvas (right) live here. -->
<div class="pl-pin-body">
<!-- LEFT — text panel stage. Five beats; each panel is
absolutely positioned, vertically centred within the
stage so shorter panels (Beats 1, 3) read as deliberate
rather than top-anchored fragments. -->
<div class="pl-copy-stage" aria-live="polite">
<!-- Beat 1 — Foundation begins -->
<div class="pl-copy-step" data-beat="1">
<p class="pl-eyebrow">The foundation</p>
<h3 class="pl-headline"><em>A model in your environment.</em></h3>
<p class="pl-body">
A state-of-the-art open-source language model, running
entirely on your hardware. No data leaves your
perimeter. The starting point &mdash; but not yet
Fenja.
</p>
</div>
<!-- Beat 2 — Knowledge added -->
<div class="pl-copy-step" data-beat="2">
<p class="pl-eyebrow">The foundation</p>
<h3 class="pl-headline"><em>Knowledge.</em></h3>
<p class="pl-body">
What makes the model <em>Fenja</em> &mdash; an
understanding of your organization, captured in a wiki
your team can read and edit. Plus the routines and
working memory that turn Fenja into a coworker who
knows how things get done.
</p>
</div>
<!-- Beat 3 — Tools layer arrives -->
<div class="pl-copy-step" data-beat="3">
<p class="pl-eyebrow">What Fenja can do</p>
<h3 class="pl-headline"><em>Tools.</em></h3>
<p class="pl-body">
How knowledge becomes work. Fenja uses tools to find
documents, query data, take action across your
systems. Some are obvious; others depend on what your
work needs.
</p>
</div>
<!-- Beat 4 — Agents layer arrives -->
<div class="pl-copy-step" data-beat="4">
<p class="pl-eyebrow">When one becomes a team</p>
<h3 class="pl-headline"><em>Agents.</em></h3>
<p class="pl-body">
Real work isn't one task. Fenja becomes a team
&mdash; a supervisor and specialists, each focused,
each governed, all dispatched by workflows you've
designed.
</p>
</div>
<!-- Beat 5 — final summary. Diagram is fully assembled
by now; this panel speaks for the whole stack. -->
<div class="pl-copy-step" data-beat="5">
<p class="pl-eyebrow">The full picture</p>
<h3 class="pl-headline"><em>Everything you need and with full control.</em></h3>
<p class="pl-body">
Fenja brings together all the pieces to solve simple
and complex AI use cases across your organisation.
</p>
</div>
</div>
<!-- RIGHT — diagram canvas. Three layer slots, each at its
final vertical position from the start. .pl-canvas-wrap
holds the column; .pl-canvas is the flex container that
stacks the three layers with a 20px gap (per spec). -->
<div class="pl-canvas-wrap">
<div class="pl-canvas">
<!-- Foundation — 3 equal cards (corrects the original
handoff's tall-Knowledge asymmetry: that asymmetry
was for a single Knowledge concept; we've split
Knowledge into Wiki + Routines & memory, so all
three Foundation cards balance). -->
<section class="pl-group" data-layer="foundation" aria-hidden="true">
<header class="pl-group-head">
<span class="pl-group-label">Foundation</span>
<span class="pl-group-caption">Sovereign by design</span>
</header>
<div class="pl-cards pl-cards--3 pl-cards--stretch">
<article class="pl-card" data-card="lm">
<h4 class="pl-card-name">Language model</h4>
<p class="pl-card-italic">State-of-the-art, open-source</p>
<p class="pl-card-mono">On-prem</p>
</article>
<article class="pl-card" data-card="wiki">
<h4 class="pl-card-name">Wiki</h4>
<p class="pl-card-italic">Company and domain knowledge</p>
<p class="pl-card-mono">Organizational &middot; Departmental &middot; Personal</p>
</article>
<article class="pl-card" data-card="routines">
<h4 class="pl-card-name">Routines &amp; memory</h4>
<p class="pl-card-italic">How Fenja works inside it</p>
<p class="pl-card-mono">Stand-ups &middot; Recurring tasks &middot; Working memory</p>
</article>
</div>
</section>
<!-- Tools -->
<section class="pl-group" data-layer="tools" aria-hidden="true">
<header class="pl-group-head">
<span class="pl-group-label">Tools</span>
<span class="pl-group-caption">How Fenja acts</span>
</header>
<div class="pl-cards pl-cards--4 pl-cards--stretch">
<article class="pl-card">
<h4 class="pl-card-name">Document retrieval</h4>
<p class="pl-card-italic">Find and cite</p>
<p class="pl-card-mono">RAG</p>
</article>
<article class="pl-card">
<h4 class="pl-card-name">Structured data (ie SQL)</h4>
<p class="pl-card-italic">Query and extract</p>
<p class="pl-card-mono">NL &rarr; SQL</p>
</article>
<article class="pl-card">
<h4 class="pl-card-name">System actions</h4>
<p class="pl-card-italic">Read and write</p>
<p class="pl-card-mono">APIs &middot; integrations</p>
</article>
<article class="pl-card">
<h4 class="pl-card-name">Custom tools</h4>
<p class="pl-card-italic">Your specific work</p>
<p class="pl-card-mono">Defined by you</p>
</article>
</div>
</section>
<!-- Agents -->
<section class="pl-group" data-layer="agents" aria-hidden="true">
<header class="pl-group-head">
<span class="pl-group-label">Agents</span>
<span class="pl-group-caption">How Fenja scales</span>
</header>
<div class="pl-cards pl-cards--4 pl-cards--stretch">
<article class="pl-card">
<h4 class="pl-card-name">Supervisor</h4>
<p class="pl-card-italic">Plan and dispatch</p>
<p class="pl-card-mono">Orchestration</p>
</article>
<article class="pl-card">
<h4 class="pl-card-name">Specialists</h4>
<p class="pl-card-italic">Focused expertise</p>
<p class="pl-card-mono">Subagents</p>
</article>
<article class="pl-card">
<h4 class="pl-card-name">Skills</h4>
<p class="pl-card-italic">Reusable capability</p>
<p class="pl-card-mono">Portable across specialists</p>
</article>
<article class="pl-card">
<h4 class="pl-card-name">Workflows</h4>
<p class="pl-card-italic">Composed by you</p>
<p class="pl-card-mono">Governed end-to-end</p>
</article>
</div>
</section>
</div>
</div>
</div><!-- /.pl-pin-body -->
</div>
</section>
<!-- "Choose your Capability" — 4 product cards. Final section. -->
<section id="platform-cards" aria-labelledby="platform-cards-head">
<header class="platform-cards-head">
<p class="platform-eyebrow">Deployment options</p>
<h2 id="platform-cards-head" class="platform-title">Choose your <em>Capability.</em></h2>
</header>
<div class="platform-card-grid" role="list">
<article class="platform-card" role="listitem">
<h3 class="platform-card-name">Fenja <em>Core.</em></h3>
<p class="platform-card-tier">Foundational</p>
<p class="platform-card-body">Essential LLM capabilities with Fenja Semantic. Your safe and custom chatbot that understands your organization.</p>
</article>
<article class="platform-card" role="listitem">
<h3 class="platform-card-name">Fenja <em>Dev.</em></h3>
<p class="platform-card-tier">Developer toolset</p>
<p class="platform-card-body">Code faster and better with your own secure AI-supported development platform.</p>
<p class="platform-card-includes">+ Core</p>
</article>
<article class="platform-card" role="listitem">
<h3 class="platform-card-name">Fenja <em>Analyze.</em></h3>
<p class="platform-card-tier">Strategic intel</p>
<p class="platform-card-body">Bring real insights to your people. You ask for an insight, and your agents will find, analyze, and present the relevant data.</p>
<p class="platform-card-includes">+ Core</p>
</article>
<article class="platform-card is-dark" role="listitem">
<h3 class="platform-card-name">Fenja <em>Agentic.</em></h3>
<p class="platform-card-tier">Automation</p>
<p class="platform-card-body">The complete framework. Fully governed and controlled agents collaborate to solve your most important processes.</p>
<p class="platform-card-includes">+ Core. Dev. Analyze.</p>
</article>
</div>
</section>
</div><!-- /#product-deepdive-scroll -->
</section>
<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="/platform.js" defer></script>
</body>
</html>

View file

@ -2929,16 +2929,23 @@ html {
</section> </section>
<!-- Dot-nav tray + nav (shared across all pages) <!-- Dot-nav tray + nav (shared across all pages)
Seven entries, flat. The first targets the Timeline page (P1). The Seven entries, flat:
next five each target a scene inside the Overview page (P2) — clicking 1. Welcome — external; routes to /
switches to Overview AND scrolls the overview's internal scroller to 2. Timeline — Timeline page (P1)
that scene. The last (Join) goes to the final scene of Overview. 3. Fenja intro — Overview page (P2), scene #hero
4. Capabilities — Overview page (P2), scene #stack-scene
5. Project Bifrost — Overview page (P2), scene #bifrost
6. Join — Overview page (P2), scene #bifrost-join
7. Product Deepdive — external; routes to /deepdive (its own
standalone subpage; not part of this app).
data-target : page id to activate data-target : page id to activate, OR `external-*` for a
data-scroll-to : (optional) element id inside #overview-scroll to cross-page navigation (uses data-href).
scroll to AFTER the page switch. Scroll runs on the data-scroll-to : (optional, Overview only) element id inside
Overview's internal scroller via Lenis (if booted) #overview-scroll to scroll to AFTER the page
or scroller.scrollTo() as a fallback. --> 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> <div class="dot-nav-tray"></div>
<nav class="dot-nav"> <nav class="dot-nav">
<button class="dot-btn" data-target="external-welcome" data-href="/" aria-label="Return to welcome page"> <button class="dot-btn" data-target="external-welcome" data-href="/" aria-label="Return to welcome page">
@ -2965,6 +2972,10 @@ html {
<span class="dot"></span> <span class="dot"></span>
<span class="label">Join</span> <span class="label">Join</span>
</button> </button>
<button class="dot-btn" data-target="external-deepdive" data-href="/deepdive" aria-label="Open the product deepdive page">
<span class="dot"></span>
<span class="label">Product Deepdive</span>
</button>
</nav> </nav>
<script src="/vendor/lenis.min.js" defer></script> <script src="/vendor/lenis.min.js" defer></script>

478
protected/platform.css Normal file
View file

@ -0,0 +1,478 @@
/* =============================================================
Fenja AI "Product Deepdive" page
A self-contained top-level page (#page-product-deepdive) reached
via its own dot. Hosts:
"The Question" full-viewport framing statement (fade-in)
"The Layers" pinned scrubbed five-beat architecture build
"Choose your Capability" 4 product cards (final section)
============================================================= */
/* Page scaffold
#page-product-deepdive is `position: fixed; inset: 0;` via the
shared `.page` rule. Its child #product-deepdive-scroll is the
internal scroller Lenis + ScrollTrigger.scrollerProxy attach
here so the scrub on Section B works without touching the
window scroll. Mirrors #overview-scroll's shape. */
#product-deepdive-scroll {
position: absolute;
inset: 0;
overflow-y: auto;
overflow-x: hidden;
background: var(--background);
scrollbar-width: thin;
scrollbar-color: rgba(56,56,49,0.18) transparent;
}
#product-deepdive-scroll::-webkit-scrollbar { width: 6px; }
#product-deepdive-scroll::-webkit-scrollbar-thumb {
background: rgba(56,56,49,0.18);
border-radius: 3px;
}
#product-deepdive-scroll > section { position: relative; z-index: 2; }
/* The dot-nav-tray paper-fade reads as a soft footer on the timeline
page but would interrupt the architecture pin's scrub on the
deepdive page. Hide it while this page is active, mirroring the
#page-overview rule. */
body:has(#page-product-deepdive.is-active) .dot-nav-tray { opacity: 0; }
/* Section A: Choose your Capability final section
This is the last section of the Deepdive page. min-height + flex
centring make the cards land vertically centred in the viewport
when the user scrolls to the page end. */
#platform-cards {
position: relative;
width: 100%;
min-height: 100vh;
background: var(--background);
color: var(--on-surface);
padding: clamp(2rem, 6vh, 5rem) clamp(2rem, 5vw, 7rem);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: clamp(2.5rem, 6vh, 4rem);
box-sizing: border-box;
}
.platform-cards-head {
text-align: center;
max-width: var(--content-max);
}
.platform-eyebrow {
font-family: var(--font-sans);
font-weight: 500;
font-size: var(--text-label-md);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--on-surface-variant);
margin: 0 0 var(--space-5) 0;
}
.platform-title {
font-family: var(--font-serif);
font-weight: 400;
font-size: clamp(2.5rem, 5vw, 3.75rem);
letter-spacing: -0.022em;
line-height: 1.1;
color: var(--on-surface);
margin: 0;
}
.platform-title em { font-style: italic; font-weight: 400; }
.platform-card-grid {
width: 100%;
max-width: var(--content-max);
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: var(--space-5);
align-items: stretch;
}
.platform-card {
display: flex;
flex-direction: column;
background: var(--surface-container-lowest);
color: var(--on-surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-ambient);
padding: var(--space-7) var(--space-6);
min-height: 320px;
}
.platform-card.is-dark {
background: var(--secondary);
color: #fffcf7;
}
.platform-card-name {
font-family: var(--font-serif);
font-weight: 400;
font-size: clamp(1.75rem, 2.4vw, 2.125rem);
line-height: 1.05;
margin: 0 0 var(--space-3) 0;
color: inherit;
}
.platform-card-name em {
font-style: italic;
font-weight: 400;
display: block;
}
.platform-card-tier {
font-family: var(--font-sans);
font-weight: 600;
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--on-surface-variant);
margin: 0 0 var(--space-5) 0;
}
.platform-card.is-dark .platform-card-tier {
color: rgba(255, 252, 247, 0.65);
}
.platform-card-body {
font-family: var(--font-sans);
font-size: var(--text-body-md);
line-height: var(--leading-relaxed);
color: inherit;
margin: 0 0 var(--space-5) 0;
}
.platform-card-includes {
margin: auto 0 0 0;
font-family: var(--font-serif);
font-style: italic;
font-size: var(--text-body-md);
color: var(--on-surface-variant);
}
.platform-card.is-dark .platform-card-includes {
color: rgba(255, 252, 247, 0.78);
}
/* Narrow desktop fallback 4 cards become a 2×2 grid before content
gets too cramped. Mobile UAs are routed to a separate page entirely
(see PROJECT.md), so this is the only narrow case to handle. */
@media (max-width: 960px) {
.platform-card-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* =============================================================
"The Question" intro section first section of the Deepdive
page. A full-viewport framing statement; fades in on scroll
like the cards stagger. Plain (not pinned, not scrubbed).
============================================================= */
#platform-question {
position: relative;
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: clamp(4rem, 8vh, 8rem) clamp(2rem, 5vw, 7rem);
background: var(--background);
box-sizing: border-box;
}
.pq-wrap {
width: 100%;
max-width: var(--reading-max);
}
/* Title italic serif for the parallel "isn't enough" rejection.
Sized below the previous hero scale so the longer flowing text
reads as a sustained statement rather than a billboard. */
.pq-title {
font-family: var(--font-serif);
font-style: italic;
font-weight: 400;
font-size: clamp(1.75rem, 3.2vw, 2.5rem);
letter-spacing: var(--tracking-snug);
line-height: 1.25;
color: var(--on-surface);
margin: 0 0 var(--space-6) 0;
opacity: 0; /* GSAP animates this; CSS prevents pre-init flash */
}
.pq-title em { font-style: italic; font-weight: 400; }
/* Body upright serif for the affirmative resolution. Same size
as the title so the two parts read as paired voices, not as
title-and-fineprint. Italic <em> emphasises the key claim
("platform you control"). */
.pq-body {
font-family: var(--font-serif);
font-weight: 400;
font-size: clamp(1.5rem, 2.6vw, 2rem);
letter-spacing: var(--tracking-snug);
line-height: 1.35;
color: var(--on-surface);
margin: 0;
opacity: 0; /* GSAP animates this; CSS prevents pre-init flash */
}
.pq-body em { font-style: italic; font-weight: 400; }
/* =============================================================
"The Layers" entry section pinned scrubbed five-beat build.
A .pl-pin two-row layout: a static title header and a
two-column body (copy stage + diagram canvas). Pinned for
+=500% so five beats can play out at 100%-viewport-per-beat
(Beats 14 assemble the diagram; Beat 5 is the closing
summary copy panel against the fully assembled diagram). The
card and layer-wrapper visuals are recreated 1:1 from the
design handoff (architecture boxes/.../README.md) pixel
sizes, letter-spacing, gaps, and radii are taken verbatim.
============================================================= */
/* No fixed height: ScrollTrigger inserts a pin-spacer (~600vh for
the +=500% pin) inside this section. */
#platform-layers {
position: relative;
width: 100%;
background: var(--background);
color: var(--on-surface);
}
/* The pin: a vertical flex stack header (title), body
(two-column copy + canvas), footer (closing caption). The body
takes remaining height and centres its two columns. */
.pl-pin {
position: relative;
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
/* Top padding sets where the header sits. The body below fills
the remaining height and centres the diagram, so a larger
top padding both lowers the header and tightens the gap to
the boxes (gap shrinks by ~half the padding bump). */
padding: clamp(5rem, 12vh, 9rem) clamp(2rem, 5vw, 7rem) clamp(1rem, 2vh, 2rem);
box-sizing: border-box;
}
.pl-pin-header {
flex: 0 0 auto;
text-align: center;
margin-bottom: clamp(1rem, 2.5vh, 2rem);
}
.pl-pin-title {
font-family: var(--font-serif);
font-weight: 400;
font-size: clamp(1.625rem, 2.6vw, 2.125rem);
letter-spacing: var(--tracking-snug);
line-height: 1.15;
color: var(--on-surface);
margin: 0 0 var(--space-2) 0;
}
.pl-pin-subtitle {
font-family: var(--font-serif);
font-style: italic;
font-weight: 400;
font-size: clamp(0.95rem, 1.4vw, 1.125rem);
color: var(--on-surface-variant);
margin: 0;
}
.pl-pin-body {
flex: 1 1 auto;
min-height: 0;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: clamp(2rem, 5vw, 6rem);
}
/* Copy stage flex item that takes remaining width up to the
reading max. Each .pl-copy-step is absolutely positioned within
the stage; vertical centring keeps shorter beats (Beats 1, 3)
reading as deliberate panels rather than top-anchored fragments. */
.pl-copy-stage {
position: relative;
flex: 1 1 0;
min-width: 0;
max-width: var(--reading-max);
min-height: 320px;
}
.pl-copy-step {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
opacity: 0; /* GSAP animates this; CSS prevents pre-init flash */
}
/* Diagram canvas three layer slots in their final vertical
positions from the start, regardless of which beats have fired.
This is the "settled-elements-never-move" guarantee: each layer
reveals in place rather than growing from zero, so neither
Foundation nor Tools is ever pushed to make room. */
.pl-canvas-wrap {
flex: 0 1 auto;
width: clamp(440px, 50vw, 700px);
min-width: 0;
}
.pl-canvas {
display: flex;
flex-direction: column;
gap: 20px; /* layer-to-layer gap, per design spec */
}
.pl-eyebrow {
font-family: var(--font-sans);
font-weight: 500;
font-size: var(--text-label-md);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--on-surface-variant);
margin: 0 0 var(--space-5) 0;
}
.pl-headline {
font-family: var(--font-serif);
font-weight: 400;
font-size: clamp(1.875rem, 3.4vw, 2.75rem);
letter-spacing: -0.02em;
line-height: 1.1;
color: var(--on-surface);
margin: 0 0 var(--space-6) 0;
}
.pl-headline em { font-style: italic; font-weight: 400; }
.pl-body {
font-family: var(--font-sans);
font-size: var(--text-body-md);
line-height: var(--leading-relaxed);
color: var(--on-surface);
margin: 0;
}
.pl-body em { font-style: italic; }
/* Architecture-layer visual (right column)
Pixel values below come straight from the design handoff
README. Do not relax them to tokens the spec is explicit. */
.pl-group {
background: var(--surface-container);
border-radius: var(--radius-lg); /* 20px */
padding: 24px 24px 28px;
}
.pl-group-head {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 18px;
padding-left: 4px;
gap: var(--space-4);
}
.pl-group-label {
font-family: var(--font-sans);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--on-surface-variant);
}
.pl-group-caption {
font-family: var(--font-serif);
font-style: italic;
font-size: 14px;
color: var(--on-surface-muted);
}
.pl-cards {
display: grid;
gap: 14px;
}
.pl-cards--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.pl-cards--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.pl-cards--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.pl-cards--start { align-items: start; }
.pl-cards--stretch { align-items: stretch; }
.pl-card {
background: var(--surface-container-lowest);
border-radius: var(--radius-md); /* 12px */
padding: 18px 20px;
display: flex;
flex-direction: column;
gap: 6px;
min-height: 76px;
box-shadow: 0 1px 0 rgba(56,56,49,0.04), 0 8px 20px -16px rgba(56,56,49,0.18);
}
.pl-card-name {
font-family: var(--font-sans);
font-size: 15px;
font-weight: 600;
letter-spacing: -0.005em;
color: var(--on-surface);
margin: 0 0 2px 0;
}
.pl-card-italic {
font-family: var(--font-serif);
font-style: italic;
font-size: 13px;
line-height: 1.35;
color: var(--on-surface-variant);
margin: 0;
}
.pl-card-mono {
font-family: var(--font-mono);
font-size: 10.5px;
letter-spacing: 0.04em;
line-height: 1.4;
color: var(--on-surface-muted);
margin: 0;
}
/* Narrow desktop: drop Tools/Agents to a 2-col grid before content
gets too cramped. Same breakpoint .platform-card-grid uses.
Mobile UAs go to a separate page entirely (PROJECT.md), so this
is the only narrow case to handle. */
@media (max-width: 960px) {
.pl-cards--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* Reduced motion release the pin entirely, stack header, all
five text panels, and the fully assembled diagram vertically.
platform.js mirrors this gate. The .pq-* opacity:1 lines belong
to #platform-question (above) but the GSAP fade-in init also
applies there, so they're paired here for the same reason. */
@media (prefers-reduced-motion: reduce) {
.pq-title, .pq-body { opacity: 1; }
#platform-layers .pl-pin {
height: auto;
gap: var(--space-10);
padding: var(--space-12) clamp(2rem, 5vw, 7rem);
}
#platform-layers .pl-pin-body {
flex-direction: column;
gap: var(--space-10);
}
#platform-layers .pl-copy-stage {
position: relative;
min-height: 0;
max-width: none;
display: flex;
flex-direction: column;
gap: var(--space-7);
}
#platform-layers .pl-copy-step {
position: relative;
opacity: 1;
}
#platform-layers .pl-canvas-wrap { width: 100%; }
#platform-layers .pl-group { opacity: 1; }
#platform-layers .pl-card { opacity: 1; transform: none; }
}

318
protected/platform.js Normal file
View file

@ -0,0 +1,318 @@
// ─────────────────────────────────────────────────────────────
// protected/platform.js — Product Deepdive page
//
// Owns #page-product-deepdive: a self-contained top-level page
// reached via the "Product Deepdive" dot. Sections (in order):
// #platform-question — full-viewport framing statement (fade-in)
// #platform-layers — pinned scrubbed four-beat architecture build
// #platform-cards — "Choose your Capability" deployment options
// (final section; centred when at scroll end)
//
// This page has its OWN internal scroller (#product-deepdive-scroll)
// with its OWN Lenis instance and its OWN ScrollTrigger.scrollerProxy
// — fully isolated from bifrost.js's setup on #overview-scroll. Every
// ScrollTrigger created here passes `scroller` explicitly so it never
// inherits ScrollTrigger.defaults from bifrost.
//
// Self-defers init until #page-product-deepdive gains `is-active`,
// so vendor libs are loaded and the scroller has real dimensions.
//
// CSP: 'script-src self'. No inline scripts anywhere.
// ─────────────────────────────────────────────────────────────
(function () {
'use strict';
let initialized = false;
let scrollerEl = null;
let lenisInstance = null;
function init() {
if (initialized) return;
if (typeof window.gsap === 'undefined' ||
typeof window.ScrollTrigger === 'undefined' ||
typeof window.Lenis === 'undefined') {
console.warn('[deepdive] gsap/ScrollTrigger/Lenis missing; skipping init.');
return;
}
const scroller = document.getElementById('product-deepdive-scroll');
if (!scroller) {
console.warn('[deepdive] #product-deepdive-scroll not found; skipping init.');
return;
}
initialized = true;
scrollerEl = scroller;
const gsap = window.gsap;
const ScrollTrigger = window.ScrollTrigger;
const Lenis = window.Lenis;
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
gsap.registerPlugin(ScrollTrigger);
// Lenis on the deepdive's internal scroller (NOT the window).
// Mirrors bifrost.js's pattern so wheel/touch input drives this
// scroller smoothly while the architecture scrub stays buttery.
if (!reduceMotion) {
const lenis = new Lenis({
wrapper: scroller,
content: scroller.firstElementChild,
duration: 1.15,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
smoothWheel: true,
wheelMultiplier: 1,
touchMultiplier: 1.5,
});
// Tell ScrollTrigger how to read scroll on this scroller. pinType
// 'transform' is required because the scroller is itself an
// overflow-scroll element rather than the window.
ScrollTrigger.scrollerProxy(scroller, {
scrollTop(value) {
if (arguments.length) {
scroller.scrollTop = value;
}
return scroller.scrollTop;
},
getBoundingClientRect() {
return { top: 0, left: 0, width: window.innerWidth, height: window.innerHeight };
},
pinType: 'transform',
});
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
lenisInstance = lenis;
}
initQuestion(gsap, ScrollTrigger, scroller, reduceMotion);
initLayers(gsap, ScrollTrigger, scroller, reduceMotion);
initCards(gsap, ScrollTrigger, scroller, reduceMotion);
// Refresh now that the page is laid out and triggers exist.
if (!reduceMotion) ScrollTrigger.refresh();
}
// ─── "The Layers" entry section ─────────────────────────────
// Pinned scrubbed five-beat build.
//
// Beat 1 — Foundation wrapper + Language model card.
// Beat 2 — Wiki + Routines & memory cards (stagger).
// Beat 3 — Tools wrapper + 4 cards (stagger).
// Beat 4 — Agents wrapper + 4 cards (stagger).
// Beat 5 — Closing summary copy panel; diagram is fully
// assembled and unchanged.
//
// Layout invariant: the canvas reserves its full assembled
// height from the start, with each .pl-group at its final
// vertical slot. Reveals are pure opacity/translate — no card
// ever moves once it has settled, because the Foundation grid
// is 3-col throughout (slots for Wiki and Routines exist from
// frame 0, just invisible) and Tools/Agents wrappers occupy
// their layout space (opacity 0) from frame 0. The .pl-pin
// header (title + subtitle) is statically rendered — visible
// before any beat fires, untouched by the timeline.
function initLayers(gsap, ScrollTrigger, scroller, reduceMotion) {
const section = document.getElementById('platform-layers');
if (!section) return;
const copies = Array.from(section.querySelectorAll('.pl-copy-step'));
const groupF = section.querySelector('[data-layer="foundation"]');
const groupT = section.querySelector('[data-layer="tools"]');
const groupA = section.querySelector('[data-layer="agents"]');
const cardsF = groupF ? Array.from(groupF.querySelectorAll('.pl-card')) : [];
const cardsT = groupT ? Array.from(groupT.querySelectorAll('.pl-card')) : [];
const cardsA = groupA ? Array.from(groupA.querySelectorAll('.pl-card')) : [];
if (!groupF || !groupT || !groupA ||
copies.length !== 5 ||
cardsF.length !== 3 || cardsT.length !== 4 || cardsA.length !== 4) {
console.warn('[deepdive] platform-layers DOM mismatch — expected 5 copy steps, 3 layer groups, 3+4+4 cards.');
return;
}
if (reduceMotion) {
// CSS @media handles the unfold; nothing for JS to do.
return;
}
// Initial states. All three layer wrappers occupy their final
// grid slots from frame 0; only opacity is animated for the
// wrappers themselves. Cards animate y+opacity within their
// pre-allocated grid cells. Text panels fade + 14px translate.
gsap.set([groupF, groupT, groupA], { opacity: 0 });
gsap.set([...cardsF, ...cardsT, ...cardsA], { opacity: 0, y: 24 });
gsap.set(copies, { opacity: 0, y: 14 });
const BEAT = 1.0;
const tl = gsap.timeline({
scrollTrigger: {
trigger: '#platform-layers',
scroller,
start: 'top top',
end: '+=500%',
pin: '.pl-pin',
pinType: 'transform',
scrub: 0.5,
},
});
// Helper — previous-copy fade-out. Skipped on Beat 1.
function fadeOutPrev(i, t) {
if (i === 0) return;
tl.to(copies[i - 1], { opacity: 0, y: -12, duration: 0.06, ease: 'power2.in' }, t);
}
// Helper — new-copy fade-in.
function fadeInCopy(i, t) {
tl.to(copies[i], { opacity: 1, y: 0, duration: 0.10, ease: 'power2.out' }, t + 0.26);
}
// Beat 1 — Foundation wrapper appears, Language model card lands.
const t1 = 0 * BEAT;
fadeOutPrev(0, t1);
tl.to(groupF, { opacity: 1, duration: 0.10, ease: 'power3.out' }, t1 + 0.06);
tl.to(cardsF[0], { opacity: 1, y: 0, duration: 0.20, ease: 'power3.out' }, t1 + 0.10);
fadeInCopy(0, t1);
// Beat 2 — Wiki + Routines cards stagger in alongside Language model.
const t2 = 1 * BEAT;
fadeOutPrev(1, t2);
tl.to([cardsF[1], cardsF[2]], {
opacity: 1, y: 0,
duration: 0.20,
ease: 'power3.out',
stagger: 0.04,
}, t2 + 0.06);
fadeInCopy(1, t2);
// Beat 3 — Tools wrapper appears, 4 cards stagger left-to-right.
const t3 = 2 * BEAT;
fadeOutPrev(2, t3);
tl.to(groupT, { opacity: 1, duration: 0.10, ease: 'power3.out' }, t3 + 0.06);
tl.to(cardsT, {
opacity: 1, y: 0,
duration: 0.20,
ease: 'power3.out',
stagger: 0.04,
}, t3 + 0.10);
fadeInCopy(2, t3);
// Beat 4 — Agents wrapper appears, 4 cards stagger left-to-right.
const t4 = 3 * BEAT;
fadeOutPrev(3, t4);
tl.to(groupA, { opacity: 1, duration: 0.10, ease: 'power3.out' }, t4 + 0.06);
tl.to(cardsA, {
opacity: 1, y: 0,
duration: 0.20,
ease: 'power3.out',
stagger: 0.04,
}, t4 + 0.10);
fadeInCopy(3, t4);
// Beat 5 — closing summary panel. Diagram is fully assembled
// by now; only the copy stage swaps to the summary text.
const t5 = 4 * BEAT;
fadeOutPrev(4, t5);
fadeInCopy(4, t5);
}
// ─── "The Platform" Part A: The Question ────────────────────
// Full-viewport question moment; just the title + subtitle, with
// a simple stagger fade-in. Same gate the cards use.
function initQuestion(gsap, ScrollTrigger, scroller, reduceMotion) {
const els = document.querySelectorAll(
'#platform-question .pq-title, #platform-question .pq-body'
);
if (!els.length) return;
if (reduceMotion) {
els.forEach(e => { e.style.opacity = '1'; });
return;
}
gsap.set(els, { opacity: 0, y: 18 });
gsap.to(els, {
opacity: 1,
y: 0,
duration: 0.7,
ease: 'power3.out',
stagger: 0.15,
scrollTrigger: {
trigger: '#platform-question',
scroller,
start: 'top 70%',
once: true,
},
});
}
// ─── Section A: Cards ────────────────────────────────────────
function initCards(gsap, ScrollTrigger, scroller, reduceMotion) {
const cards = document.querySelectorAll('#platform-cards .platform-card');
if (!cards.length) return;
if (reduceMotion) {
cards.forEach(c => { c.style.opacity = '1'; });
return;
}
gsap.set(cards, { opacity: 0, y: 24 });
gsap.to(cards, {
opacity: 1,
y: 0,
duration: 0.6,
ease: 'power3.out',
stagger: 0.08,
scrollTrigger: {
trigger: '#platform-cards',
scroller,
start: 'top 70%',
once: true,
},
});
}
// ─── Public scrollTo (used when the dot is re-clicked while
// already on the deepdive page) ──────────────────────────────
function scrollTo(top) {
if (!initialized || !scrollerEl) return;
const y = typeof top === 'number' ? top : 0;
if (lenisInstance && typeof lenisInstance.scrollTo === 'function') {
lenisInstance.scrollTo(y, { immediate: false });
} else {
scrollerEl.scrollTo({ top: y, behavior: 'smooth' });
}
}
// ─── Lazy auto-init on page activation ───────────────────────
// We can't piggyback on bifrost's init (different page) and the
// dot-nav handler in timeline.js doesn't know about us. Instead,
// observe #page-product-deepdive for the `is-active` class flip
// that activatePage() applies — then init a beat later so the
// browser has applied layout.
function tryInit() {
if (initialized) return;
const page = document.getElementById('page-product-deepdive');
if (!page || !page.classList.contains('is-active')) return;
if (typeof window.gsap === 'undefined' ||
typeof window.ScrollTrigger === 'undefined' ||
typeof window.Lenis === 'undefined') return;
setTimeout(init, 60);
}
function attachObserver() {
const page = document.getElementById('page-product-deepdive');
if (!page) return;
new MutationObserver(tryInit).observe(page, {
attributes: true,
attributeFilter: ['class'],
});
tryInit();
}
window.__deepdive = { init, scrollTo };
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', attachObserver, { once: true });
} else {
attachObserver();
}
})();