commit a7131e0f798f2acec350a4d984b5c52831f697b8 Author: Jonathan Date: Sat Apr 18 16:09:49 2026 +0200 wip: scaffold and index before style-guide diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..aac6d9d --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:api.anthropic.com)", + "Bash(pnpm --version)", + "Bash(git init *)", + "Bash(git add *)", + "Bash(git commit *)" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db44ef0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +dist/ +node_modules/ +.env +.env.* +!.env.example +.astro/ +*.db +*.db-shm +*.db-wal +progress.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a12f7cb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,133 @@ +# Project Bifrost — Claude Code Memory + +> This file is persistent context for every Claude Code session on this repo. Keep it under 200 lines. If something is only relevant to one feature, it does not belong here — put it in `SPEC.md` or a scoped file. + +## Project + +**Project Bifrost** is Fenja AI's invite-only pilot hub — a private website for pilot participants and Customer Advisory Board members during a ~3–4 month pilot of Fenja's sovereign AI platform. Full product specification lives in `SPEC.md`. Read it before doing anything substantial; it is the source of truth. + +## Stack + +- **Framework:** Astro 4+ (server output mode where needed, static elsewhere) +- **Language:** TypeScript, strict mode +- **Styling:** Vanilla CSS with design tokens derived from `design/colors_and_type.css` — *no Tailwind*, no CSS-in-JS, no component libraries with baked-in styling +- **Database:** SQLite via `better-sqlite3` +- **Auth:** Custom. Session cookies + bcrypt + signed invite tokens. No third-party auth providers. +- **Runtime:** Node 22 LTS +- **Package manager:** pnpm (never npm, never yarn) +- **Hosting:** Single Hetzner Cloud VPS (CAX11, Helsinki) behind Caddy + +## Commands + +``` +pnpm install # install dependencies +pnpm dev # start dev server on :4321 +pnpm build # production build +pnpm preview # preview production build locally +pnpm test # run vitest +pnpm typecheck # tsc --noEmit +pnpm lint # eslint + prettier check +pnpm db:migrate # apply migrations to dev SQLite +pnpm db:seed # seed dev database with test users +``` + +Always run `pnpm typecheck` and `pnpm test` before claiming a task is done. + +## File structure + +``` +/ +├── SPEC.md # product spec (source of truth) +├── CLAUDE.md # this file +├── design/ # design system — authoritative for all visual language +│ ├── SKILL.md # instructions for how to use this design system (read first) +│ ├── README.md # human overview +│ ├── ICONOGRAPHY.md # icon style & usage +│ ├── colors_and_type.css # authoritative colour & type tokens +│ ├── assets/ # logo and brand assets +│ ├── fonts/ # font files +│ ├── preview/, screenshots/, ui_kits/, uploads/ # reference material +├── content/ # markdown content +│ ├── updates/ # YYYY-MM-DD-slug.md progress posts +│ ├── meetings/ # YYYY-MM-DD-slug.md meeting pages +│ └── roadmap.md # single roadmap file +├── src/ +│ ├── pages/ # Astro routes +│ ├── components/ # Astro components +│ ├── layouts/ # page layouts +│ ├── styles/ +│ │ ├── tokens.css # CSS custom properties — derived from design/colors_and_type.css +│ │ └── global.css # base styles, resets, @font-face declarations +│ ├── lib/ +│ │ ├── db.ts # SQLite connection & typed queries +│ │ ├── auth.ts # session + password + invite token helpers +│ │ └── content.ts # markdown collection helpers +│ └── env.d.ts +├── migrations/ # numbered .sql files +├── scripts/ # ops scripts (backup, deploy, seed) +└── public/ # static assets served as-is + ├── logo.svg # copied from design/assets/ on first session + └── fonts/ # copied from design/fonts/ on first session +``` + +## Code standards + +- **TypeScript strict.** No `any` without a comment explaining why. Prefer `unknown` and narrow. +- **No runtime dependencies unless asked.** Before adding a package, justify it. The current stack is chosen for minimalism — resist padding it. +- **SQL is written by hand.** No ORM. Queries live in `src/lib/db.ts` as typed functions (`getUserByEmail(email): User | null`, etc.). +- **Zod schemas** for all content collection frontmatter, all form inputs, and all env vars. Parse at boundaries; trust internally. +- **Server-only secrets** go through `astro:env` or a server-only `env.ts` module. Never import secrets into client code — verify this. +- **Errors** in route handlers return proper status codes with minimal JSON. Never leak stack traces to clients. +- **Dates** are always UTC in the database, rendered in Europe/Copenhagen on the page. +- **Accessibility is not optional.** Every interactive element keyboard-reachable; every image has `alt`; colour contrast meets WCAG AA against tokens from `design/colors_and_type.css`. + +## Design principles (from SPEC §6) + +The aesthetic is **authoritative quietude** — academic, editorial, Danish-minimal. The `design/` folder is the authoritative source; `design/SKILL.md` is the entry point for how to use it. Concrete rules that hold unless the design system overrides them: + +- **No visible borders** on cards or sections. Separation = tonal background shift + whitespace. +- **No drop shadows.** If elevation is needed, use a tonal shift. +- **Editorial type scale** — headlines are large, body copy has room to breathe, line-height ≥ 1.6 for body. +- **Asymmetric layouts preferred** over centred ones. Use the 12-column grid deliberately. +- **Motion is minimal.** Transitions < 200ms, ease-out. No parallax, no auto-play. +- **Light mode only** for v1. +- **All colour, font, and spacing values come from `design/colors_and_type.css`** via `src/styles/tokens.css`. Never hardcode hex values, font names, or magic spacing numbers in components — reference tokens. If a token is missing, stop and add it to `tokens.css` (faithful to the design system) before continuing. + +## First-session tasks (in order) + +When Claude Code first opens this project: + +1. Read `SPEC.md` end to end. +2. Read `design/SKILL.md` — follow its instructions. It is the primary guide for how to use the design system. +3. Read `design/README.md`, `design/ICONOGRAPHY.md`, and `design/colors_and_type.css`. Skim `design/preview/`, `design/screenshots/`, and `design/ui_kits/` for reference. +4. Scaffold the Astro project per the file structure above. Install only what the spec requires. +5. Copy logo assets from `design/assets/` into `public/` (prefer SVG; keep filenames stable, e.g. `public/logo.svg`). +6. Copy fonts from `design/fonts/` into `public/fonts/` and set up `@font-face` declarations in `src/styles/global.css`. +7. Create `src/styles/tokens.css` derived from `design/colors_and_type.css`. Either `@import` it directly, or re-express its values as CSS custom properties — whichever `design/SKILL.md` recommends. +8. Build a `/style-guide` internal route that renders: every colour token, every type scale step, spacing scale, buttons, form inputs, cards, iconography, and the logo in context. This is for human review before any real pages are built. +9. Stop and wait for review. + +Do not skip step 9. The visual language needs human approval before pages get built on top of it. + +## Behavioural preferences + +- **Plan before coding.** For any task larger than a trivial fix, propose a plan first and wait for approval. +- **Small, reviewable commits.** One logical change per commit. Conventional commit messages (`feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `style:`). +- **Ask before:** adding a dependency, changing the database schema, changing anything in `design/`, changing this file or `SPEC.md`, deploying. +- **Never:** commit secrets, commit `.env*` files, run `git push --force`, modify files inside `design/` (that folder is read-only from the code's perspective — if the design needs to change, update `design/` directly via a separate deliberate commit). +- **When uncertain about product intent**, check `SPEC.md`. If the spec is ambiguous, ask — do not guess. +- **When uncertain about visual language**, re-read `design/SKILL.md` and `design/colors_and_type.css`. If it is still ambiguous, ask. +- **When blocked by a missing decision**, write the options as a short note in `DECISIONS.md` and ask for a ruling. Do not silently pick. +- **Write state to files for long tasks.** If a task spans many steps, keep notes in `progress.md` so a fresh session can pick up. + +## Out of scope + +Read `SPEC.md §8`. Do not build: email sending, file uploads, rich-text editors, dark mode, mobile app, search, analytics, i18n, 2FA, public pages. If the user asks for one of these, confirm it is a scope change and suggest updating `SPEC.md` first. + +## Known gotchas + +- **better-sqlite3 is native.** On Hetzner ARM it must be built for ARM. The deploy script rebuilds it on the server. +- **Astro server output mode** requires an adapter — use `@astrojs/node` in `standalone` mode. +- **Signed invite tokens** are HMAC-signed, not JWTs. Keep them short and opaque. +- **Sessions are HttpOnly, Secure, SameSite=Lax cookies.** Never expose session IDs to client JS. +- **Font licensing.** The fonts in `design/fonts/` may be licensed — check `design/README.md` and `design/SKILL.md` for any usage restrictions before serving them publicly. diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..cc13f81 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,297 @@ +# Project Bifrost — Pilot Hub + +**Owner:** Fenja AI +**Purpose:** A private, invite-only hub for participants of the Project Bifrost pilot. +**Status:** Pre-build specification. This is the source of truth for what we are building and why. Everything in `CLAUDE.md` refers back to this. + +--- + +## 1. What this is + +Project Bifrost is Fenja AI's first product pilot — the foundation for a sovereign, customer-hosted AI platform for Denmark and, in time, the EU. The *hub* is the private website where pilot participants and Customer Advisory Board members live during the pilot: it is where they read what we are building, shape it, ask questions, and meet. + +The hub is not a marketing site. It is a working room. It should feel like being invited into a small, serious project — closer to a research group's private workspace than a product landing page. + +The name carries weight. *Bifrost* — the rainbow bridge between worlds in Norse mythology — is the bridge between what Fenja is building and the organisations that will first depend on it. The hub is where that bridge is walked. + +--- + +## 2. Vision copy (verbatim, for the site) + +### Fenja AI + +> Fenja enables the safe, sovereign use of Artificial Intelligence in organisations where data is highly sensitive, operations are mission-critical, and strict data privacy is essential. We are building the premier solution for public authorities and companies operating in heavily regulated industries — pharmaceuticals, critical infrastructure, finance — where standard AI solutions hosted on foreign-owned public clouds are simply not a viable or compliant option. +> +> Many organisations are eager to harness the transformative power of AI but cannot accept the risk of their data, workflows, and critical functions becoming dependent on external providers with foreign ownership and control. Fenja directly addresses this gap by enabling highly advanced, customer-hosted AI within the client's own secure infrastructure. Data remains under absolute, localised control while we deliver the robust security, traceability, and documentation demanded by the market. +> +> As the global demand for digital sovereignty accelerates, Fenja is positioned to generate profound societal value. We are making cutting-edge AI safely usable in the most critical of functions without ever compromising security or compliance. Backed by investment from the Innovation Fund, we are accelerating product development, executing high-impact pilot projects, and establishing foundational customer cases. Our long-term ambition is to establish Fenja as the definitive Danish and European platform for secure, locally controlled AI in regulated environments. + +### Project Bifrost + +> Project Bifrost is one of the first stones in the foundation of Denmark and the EU's sovereign AI platform — a platform controlled by its customers, that understands not just your business, but you and your department, and builds on that knowledge automatically. Open-source LLMs in a modern, secure, customer-hosted environment. +> +> This is Project Bifrost. + +### What comes after Bifrost + +A preview section, written in reserved language (no dates, no promises): + +- Full developer agentic experience +- Agentic data extraction and analysis +- Self-service agent work automation + +--- + +## 3. Audience & access + +### Roles + +| Role | Who | What they can do | +|---|---|---| +| **Pilot participant** | People from the ~2 organisations running the paid pilot | Full hub access; contribute ideas, ask questions, attend CAB meetings, see roadmap | +| **Advisory Board member** | People from the other ~3 organisations following & advising | Same hub access as pilot participants, except they see a *CAB* badge and the roadmap may mark some items as pilot-only | +| **Fenja internal** | Us | Everything above + admin panel | + +**Note:** Everyone in the hub sees each other's contributions. The "fully shared" community model is deliberate — the value of the hub is that participants see they are not alone, and ideas compound. + +### Scale + +- ~14 people at launch, across ~5 organisations +- Designed to grow to ~50 without re-architecture +- Not designed for public scale — this stays invite-only + +### Invitation flow + +1. Fenja admin (you) opens the admin panel, enters name + organisation + email + role +2. System generates a one-time signed invite link and shows it to you +3. You send the link personally (email, signal, in person — your choice; the hub does not send emails) +4. Invitee clicks link → lands on a branded "Welcome, *Mette*" screen that asks them to set a password +5. From then on they log in with email + password +6. Invite links expire 14 days after generation and are single-use + +**No self-registration. No "forgot password" email flow for v1** — if someone needs a reset, you issue them a new invite link from the admin panel. Simpler and more secure for 14 people. + +--- + +## 4. Information architecture + +Eight surfaces. No more. + +1. **Welcome** `/` — Personalised landing ("Welcome, Mette."). Short orientation. Three or four quiet cards pointing to the other surfaces. Latest update teaser. +2. **Vision** `/vision` — The Fenja vision, the Bifrost vision, "what comes after" — all from §2. +3. **Updates** `/updates` — Reverse-chronological log of progress posts written by Fenja. Each post is a markdown file. Permalinks. +4. **Roadmap** `/roadmap` — Editorial, timeline-style view of what is being built. Three horizons: *In progress*, *Next*, *Later*. Not a Trello board — closer to a museum plaque for each item. +5. **Calendar** `/calendar` — CAB meetings and milestones. Big, typographic, unrushed. One month per view. Click a meeting → its page (agenda before, notes after). +6. **Contribute** `/contribute` — Where participants post ideas, inspiration, and questions. Everyone sees everyone. Three tabs or filters: *Ideas*, *Inspiration*, *Questions*. Markdown-lite text input. Fenja can reply. Others can react (simple "+1" style, no threaded debates — this is a suggestion book, not a forum). +7. **Product preview** `/preview` — "Coming soon" for Fenja AI and Fenja Knowledge. Restrained prose about what's being built. When the MVP is ready, this page becomes the gateway. +8. **Participants** `/participants` — A directory of who is in the hub, by organisation. Reinforces the "you are in good company" feeling. Shows role badges. No contact info beyond what each participant chooses to share. + +### Admin-only + +9. **Admin** `/admin` — Behind auth + role check. Two tabs: *Invitations* (generate/list/revoke) and *Participants* (view, change role, deactivate). No content editing here — content lives in markdown, committed to git. + +--- + +## 5. Feature specifications + +### 5.1 Welcome page + +Personal, spare. Above the fold: the Fenja wordmark (small), then a single oversized editorial greeting — *"Welcome, Mette."* — set against generous whitespace. Below it, one or two sentences framing what Bifrost is and what the hub is for. Further down, three quiet navigation cards (*Latest update*, *Next CAB meeting*, *Contribute an idea*). That's it. No hero image, no CTA chrome. + +### 5.2 Updates + +- Posts are markdown files in `/content/updates/YYYY-MM-DD-slug.md` +- Frontmatter: `title`, `date`, `author` (defaults to "Fenja AI"), `summary` +- Rendered with proper editorial typography — generous line height, serif body text if the design system permits, large margins +- Each post has a permalink; RSS feed at `/updates.xml` (low effort, high signal of seriousness) + +### 5.3 Roadmap + +- One markdown file: `/content/roadmap.md` +- Structured as three sections: *In progress*, *Next*, *Later* +- Each item is a few lines — a name, a one-sentence description, optionally a status tag +- No Gantt charts, no percentages. If an item is pilot-only, that's marked with a small tag + +### 5.4 Calendar + +This is the showpiece. It should *feel* like the best page on the site. + +- Month view by default. Arrow keys navigate months. Today is marked with a subtle tonal shift, not a coloured pill. +- Meetings are written as markdown files in `/content/meetings/YYYY-MM-DD-slug.md` +- Each meeting's detail page shows: date, time, location (physical or video link), attendees (roles, not individuals, unless specified), agenda (before), notes (after), any attached documents +- Before a meeting, the page shows the agenda. After, notes appear below. +- Attendees can mark "I will attend" / "I cannot attend" — a simple tally, not a hard RSVP. Shown only to Fenja. + +### 5.5 Contribute + +- Participants write markdown-lite text (bold, italic, links, lists, code blocks — nothing exotic) +- Each contribution has an author, organisation, timestamp, type (*Idea* | *Inspiration* | *Question*), and body +- Reactions: a single "+1" style acknowledgement (a small mark, not a heart). Count shown. +- Fenja replies appear directly under the contribution, typographically distinct +- Contributions can be edited by author within 10 minutes of posting, then locked +- No deletion by authors. Admins can hide (soft-delete) anything. +- Default sort: newest. Also: *most acknowledged*. + +### 5.6 Product preview + +A single page. A restrained block of copy explaining what is coming (Fenja AI, Fenja Knowledge — a "Karpathy-style" wiki that lets the system understand the context you work in). No screenshots yet. When the MVP ships, this page will gain a "Request early access" path. + +### 5.7 Participants + +- Grouped by organisation. Each organisation shown with its name and participant cards. +- Each card: name, role, short self-written bio (optional, max 280 chars), role badge (*Pilot* / *CAB* / *Fenja*) +- Participants edit their own bio from `/account` (tiny page, just name display preference + bio) + +### 5.8 Admin panel + +- **Invitations tab:** form (name, email, organisation, role) → generates a signed link → shows it once, copy-to-clipboard. Table below lists outstanding invites, who they're for, when they expire, with *Revoke* and *Regenerate* actions. +- **Participants tab:** table of all users with role, last-seen, and actions (change role, deactivate, issue new password reset link) + +--- + +## 6. Design direction + +The visual language comes from the Fenja design system, which lives locally in the `design/` folder. That folder is the single source of truth for colours, typography, spacing, components, iconography, fonts, and logo assets. It contains: + +- `design/SKILL.md` — instructions for Claude Code on how to use the design system +- `design/README.md` — human-readable overview +- `design/ICONOGRAPHY.md` — icon style and usage +- `design/colors_and_type.css` — authoritative colour and type tokens +- `design/assets/` — logo and brand assets +- `design/fonts/` — font files +- `design/preview/`, `design/screenshots/`, `design/ui_kits/` — reference material + +Claude Code reads `design/SKILL.md` first and follows its instructions. The code in `src/` consumes the design system through `src/styles/tokens.css`, which is derived from `design/colors_and_type.css`. Fonts and logo assets are copied into `public/` during initial setup. + +The aesthetic the design system expresses — and that every page of the hub must respect — is **authoritative quietude**: + +> A rejection of the frantic, neon-soaked tropes of "Deep Tech" AI in favour of a timeless, academic, and human-centric aesthetic. Data treated like a high-end research publication; interface treated like a piece of minimalist Danish furniture. Intentional asymmetry, oversized editorial margins, a radical commitment to negative space. Separation comes from tonal shifts and "ghost layering" rather than borders. An interface that breathes, inviting contemplation rather than consumption. + +Practical implications for the build (these hold unless the design system specifies otherwise): + +- **No visible borders** on cards or sections. Separation = tonal background shift + whitespace. +- **No drop shadows** by default. If elevation is needed, use a near-imperceptible tonal shift. +- **Typography does the work.** Headline sizes are editorial-scale. Body copy has room to breathe. +- **Asymmetry is intentional.** Most pages use 2 or 3 optical zones with deliberate weight, not centred symmetry. +- **Motion is minimal.** Transitions are short, slow, ease-out. No parallax. No auto-playing anything. +- **Light mode primary.** Dark mode is a nice-to-have for v1.1, not v1. + +If the design system contradicts any of the above, the design system wins — flag the contradiction for review. + +--- + +## 7. Technical approach (proposed) + +Everything here is a recommendation. Overrule before the first build. + +### 7.1 Stack + +- **Framework:** [Astro](https://astro.build) with a few server endpoints. Astro is content-first, produces mostly static HTML, and treats markdown as a first-class citizen — ideal for the editorial aesthetic. Server routes handle only what needs a server (auth, contributions, admin). +- **Language:** TypeScript throughout. +- **Styling:** Vanilla CSS with design tokens (CSS custom properties) defined in `src/styles/tokens.css`, derived from `design/colors_and_type.css`. *No Tailwind.* Tailwind's utility soup runs against the editorial, bespoke aesthetic we want. +- **Database:** SQLite via [better-sqlite3](https://github.com/WiseLibs/better-sqlite3). For 14–50 users, SQLite is the right tool. One file, atomic backups, no daemon. +- **Auth:** Custom, minimal. Session cookie + bcrypt password hashes + signed invite tokens. No third-party auth provider — sovereignty matters. +- **Content:** Markdown files in `/content/{updates,meetings,roadmap}`. Parsed with Astro's built-in content collections (with `zod` schemas). +- **Package manager:** pnpm. +- **Node version:** 22 LTS. + +### 7.2 Data model (SQLite) + +``` +users + id, email (unique), password_hash, name, organisation, + role ('pilot' | 'cab' | 'fenja'), bio, created_at, last_seen_at, active + +invites + id, token_hash (unique), email, name, organisation, role, + expires_at, used_at, created_at, created_by_user_id + +sessions + id, user_id, expires_at, created_at + +contributions + id, user_id, type ('idea' | 'inspiration' | 'question'), + body_md, created_at, edited_at, hidden_at + +reactions + id, user_id, contribution_id, created_at + (unique on user_id + contribution_id) + +replies + id, contribution_id, user_id, body_md, created_at + (only fenja role can create) + +attendance + id, user_id, meeting_slug, status ('yes' | 'no'), updated_at + (unique on user_id + meeting_slug) +``` + +That's the whole schema. No ORM — plain SQL in typed query functions. + +### 7.3 Hosting + +- **Single VPS on Hetzner Cloud**, Helsinki region (fi-hel1) — EU, renewable power, closest Hetzner DC to Denmark. CAX11 (ARM, 2 vCPU, 4 GB) is sufficient; ~€4/month. +- **Reverse proxy:** Caddy, automatic HTTPS via Let's Encrypt. +- **Process manager:** systemd unit running the Node server. +- **Backups:** `sqlite3 .backup` nightly cronjob → Hetzner Storage Box, 30-day retention. +- **Deploys:** `git push` to the VPS's bare repo triggers a `post-receive` hook that builds and restarts. Or, GitHub Actions → rsync. Claude Code can set up either. +- **Domain:** something like `bifrost.fenja.ai` (confirm with Fenja DNS owner). + +### 7.4 Why not fully static + +A fully static site cannot: +- Authenticate users +- Accept and display user submissions +- Track invite usage +- Mark attendance + +Those features are core to the hub, so a small backend is non-negotiable. The backend stays small: fewer than ~15 server routes, one SQLite file, no external services. + +--- + +## 8. Out of scope for v1 + +Listed so Claude Code does not wander into them: + +- Email sending (invites are copied manually; no transactional email provider) +- File uploads from participants (they write text; Fenja attaches documents to meetings via git) +- Rich-text editor beyond markdown-lite +- Threaded discussion on contributions (replies from Fenja only, one level deep) +- Dark mode +- Mobile app +- Search +- Analytics beyond a small self-hosted Plausible instance (optional; skip for v1) +- Internationalisation (English only) +- Two-factor authentication (considered for v1.1) +- Public-facing pages (everything sits behind auth except the invite-redeem and login routes) + +--- + +## 9. Decisions made on your behalf + +Please overrule any of these before Claude Code starts: + +1. **Framework = Astro.** If you prefer SvelteKit or Next.js, say so — the spec adapts cleanly. +2. **CSS = vanilla with tokens.** Tailwind is available if you'd rather trade the editorial purity for build speed. +3. **Database = SQLite.** Postgres is overkill for 14 users but available if you want it. +4. **No email service.** You send invite links personally. +5. **No forgot-password flow.** Admin re-issues invite links instead. +6. **Hetzner Helsinki** over Falkenstein — personal call based on renewable power and latency to Denmark. Falkenstein is fine too. +7. **Light mode only for v1.** +8. **No public marketing site at the same domain.** The hub sits at `bifrost.fenja.ai` or similar. + +--- + +## 10. Definition of done for v1 + +The hub is done when: + +- An admin can generate an invite link and a new user can redeem it, set a password, and log in +- All eight surfaces in §4 render correctly, personalised where relevant, with copy from §2 +- A participant can post an *Idea*, *Inspiration*, or *Question*, and every other participant sees it, can acknowledge it, and Fenja can reply +- A meeting can be created as a markdown file and appears on the calendar; attendance can be marked; notes appear after the meeting +- The roadmap renders from its markdown file +- The whole thing runs on a Hetzner CAX11, backed up nightly, with HTTPS +- The visual language faithfully applies the design system in `design/` without requiring further design review + +When all of the above is true and you have sent the first batch of invite links — Bifrost is live. diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..0aaeeb3 --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,7 @@ +import { defineConfig } from 'astro/config'; +import node from '@astrojs/node'; + +export default defineConfig({ + output: 'server', + adapter: node({ mode: 'standalone' }), +}); diff --git a/content/roadmap.md b/content/roadmap.md new file mode 100644 index 0000000..288a18e --- /dev/null +++ b/content/roadmap.md @@ -0,0 +1,9 @@ +--- +title: Roadmap +--- + +## In progress + +## Next + +## Later diff --git a/design/ICONOGRAPHY.md b/design/ICONOGRAPHY.md new file mode 100644 index 0000000..4cecd50 --- /dev/null +++ b/design/ICONOGRAPHY.md @@ -0,0 +1,70 @@ +# Iconography — Fenja AI + +Icons in Fenja are the meeting point between **hand-drawn human mark** and **sharp angular Norse-runic geometry**. They are the system's strongest carrier of voice after the serif itself. + +## Rules + +1. **Stroke-only, thin.** 1px or 1.5px. Color: `--on-surface` (#383831). No fills on the main silhouette. +2. **Graphite character.** The line should feel drawn, not perfectly mechanical — slight variation is welcome; pure geometric precision is not. +3. **Runic overlay.** Pair the human mark with one or two angular line-work elements (triangles, diamonds, vertical strokes) that feel Norse in origin. +4. **Pigment accents only as small details.** A single conceptual element inside the icon may carry a flat Archival Pigment fill — never the whole mark. +5. **No filled Material icons.** No rounded "friendly" cartoon icons. No emoji in the icon slot. Ever. + +## Size & spacing + +| Size | Context | +|---|---| +| 16×16 | Inline with label-md, dense tables | +| 20×20 | Navigation, form field adornments | +| 24×24 | Default UI icon size | +| 32×32 | Feature callouts | +| 48×48+ | Hero illustrations only | + +Icons are optically centered on the cap-height of adjacent serif headlines or the x-height of sans body. + +## Sources — what we use today + +### Custom (aspirational, not yet shipped) +A bespoke set matching the "graphite + runic" description is the target. None exist in this project yet. This is the biggest open gap in the system. + +### Substitute library: Lucide +Until the custom set is produced, we use **Lucide** at 1.5px stroke weight. Reasons: matches our stroke weight, has the thin monoline character closest to what we want, and the Apache-licensed source SVGs are easy to re-skin. + +```html + + + + +``` + +**Flag:** this is a substitution. We have not yet produced the custom runic set called for in the spec. Most icons will look "correct enough" but will miss the *hand-drawn + Norse-runic* layering that the brand ultimately requires. + +### Logos (custom, shipped) +Located in `assets/`: +- `fenja-icon-black.svg` — the spear mark, charcoal, on light +- `fenja-icon-white.svg` — the spear mark, paper, on dark +- `fenja-wordmark-black.svg` — spear + "Fenja AI" lockup, charcoal +- `fenja-wordmark-white.svg` — spear + "Fenja AI" lockup, paper + +**Minimum sizes:** icon 24×36px, wordmark 96×36px. Maintain clear space equal to the height of the spear-head around all sides. + +## Unicode as icon — permitted + +Typographic glyphs may serve as minimal "icons" in editorial contexts: + +| Glyph | Use | +|---|---| +| `—` em-dash | Section breaks, attribution | +| `§` | Note references | +| `¶` | Paragraph marker in archive views | +| `·` | List interpuncts | +| `↗` | External link | +| `↳` | Reply / thread descent | + +## Emoji — forbidden + +No emoji. Not in empty states, not in CTAs, not in marketing. Use charcoal runic line-work or Archival Pigment flat marks instead. + +## Data viz glyphs + +Axes, gridlines, and tick marks use the **Ghost Border** (`--outline-variant` at 15–30% opacity). Data series use Archival Pigments at 70–100% for primary, 20–40% for overlapping fills. No drop shadows, no gradients on chart elements. diff --git a/design/README.md b/design/README.md new file mode 100644 index 0000000..f192ff7 --- /dev/null +++ b/design/README.md @@ -0,0 +1,285 @@ +# Fenja AI — Nordic Editorial Design System + +> **The Digital Archivist.** An interface that breathes. A design system that treats data like a high-end research publication and an interface like a piece of minimalist Danish furniture. + +--- + +## 1. Brand Context + +**Fenja AI** is an AI product whose brand rejects the frantic, neon-soaked tropes of "Deep Tech" AI in favor of a timeless, academic, and human-centric aesthetic. The name **Fenja** comes from Norse myth — Fenja and Menja are the giantesses of the mill *Grótti*, grinders of gold and salt. The logo is a single **spear** (Norse iconography), rendered in warm charcoal `#383831`. + +The goal of every surface: **authoritative quietude**. Move beyond the "template" look by using intentional asymmetry, oversized editorial margins, tonal surface shifts (no hairline dividers), and a radical commitment to negative space. + +### Sources in this project + +| Source | What it contained | +|---|---| +| `uploads/fenja icon black.svg` | Primary spear icon (charcoal) | +| `uploads/fenja icon white.svg` | Spear icon (on dark) | +| `uploads/fenja black(1).svg` | Full wordmark lockup: spear + "Fenja AI" in Manrope Medium | +| `uploads/fenja white(1).svg` | Wordmark lockup (on dark) | +| `uploads/Manrope-*.ttf` | 7 weights of Manrope — the functional sans | +| `uploads/Newsreader_36pt-*.ttf` | 5 cuts of Newsreader — the editorial serif | +| Pasted spec | "Nordic Editorial" design principles: surfaces, pigments, "no-line" rule, ghost borders, tonal layering, runic iconography | + +No codebase or Figma file was attached. UI kit screens in this system are derived from the written spec alone and should be treated as **reference implementations**, not pixel ports of a shipping product. + +--- + +## 2. Content Fundamentals + +**Voice: the quiet expert.** Fenja doesn't shout. It speaks the way a good research librarian does — precise, unhurried, generous with context. The tone is *academic but warm*, not clinical. + +### Tone rules + +| Do | Don't | +|---|---| +| "A study in stillness." | "🚀 Blazingly fast AI!" | +| "Archive of 4,812 documents." | "4.8K+ files uploaded 🔥" | +| "We found a pattern." | "AI magic unlocked ✨" | +| Serif subheads paired with crisp sans body | All-caps marketing shouts | + +### Casing & grammar + +- **Sentence case everywhere.** Buttons, headlines, menu items. No Title Case marketing jargon. +- **Oxford comma, always.** +- **Full stops on standalone sentences**, none on single-phrase labels or buttons. +- **Numerals for data** (`12 results`), spelled for prose (`twelve years of research`). +- Prefer **"you"** for user-facing prose; **"we"** is used sparingly and only for Fenja itself (e.g. "We found a pattern."). Avoid "our" marketing padding. + +### The Definitive Emphasis — headline syntax + +A strict grammatical and visual rhythm, to reinforce the Archivist persona. Applies to **every product-level headline** (hero H1, section H2, card H3, ask-pane titles). + +| Rule | Detail | +|---|---| +| **Terminal keyword placement** | The core capability or subject of the headline must always sit at the **end** of the sentence. | +| **Styling** | That final keyword or phrase is rendered in **Newsreader Bold Italic**. Everything before it stays in the standard serif weight. | +| **The absolute period** | Every headline concludes with a period — even in contexts where headlines are conventionally left open. The period is a visual anchor and a signal of finality. | + +Examples: + +- "A quiet place to keep ***what you are reading***." +- "Recently ***filed***." +- "Reading alongside you, ***Fenja***." +- "Ask the ***archive***." + +Apply this rule only to product-authored headlines — do **not** rewrite user-generated content (document titles, note headings) into the syntax. + +### Emoji & glyph policy + +- **No emoji.** Anywhere. Not in UI, not in empty states, not in marketing. +- Unicode glyphs are acceptable as **typographic** devices only: em-dashes (—), en-dashes (–), interpuncts (·), section marks (§), pilcrow (¶). +- For state markers, use the **Archival Pigment** accent colors applied to runic geometry, not colored emoji. + +### Copy examples + +| Context | Copy | +|---|---| +| Empty state | "Nothing here yet. Drop a document to begin." | +| Loading | "Reading…" (not "Loading…", never "Please wait") | +| Error | "Could not reach the archive. Retry?" | +| Success | "Filed." or "Saved to your archive." | +| CTA | "Begin" / "Open archive" / "Read the study" | +| Destructive confirm | "Remove this forever?" (full stop, honest) | + +### Microcopy rhythm + +Fenja microcopy often uses a **period** where other brands use an exclamation point. Confidence comes from restraint. + +> "A thinking tool. Built slowly." +> "Search your own library. Quietly." +> "Ask the archive." + +--- + +## 3. Visual Foundations + +### Surface philosophy — the "no-line" rule + +**Borders are forbidden** for structural sectioning. Define boundaries through background color shifts between surface tiers only. Treat the UI as stacked sheets of fine cotton paper. + +``` +surface-container-lowest #fffcf7 ← floating search, topmost elevation (unbleached paper — the only near-white) +surface / background #faf6ee ← primary canvas +surface-container-low #f6f2e8 ← first tonal nest +surface-container #efeadc ← inset sections, sidebars +surface-container-high #e7e1d0 ← deeper insets +surface-container-highest #ddd6c3 ← recessed / pressed button fill +``` + +A `surface-container-lowest` card sits on a `surface-container-low` section on a `surface` page with **zero dividers**. + +### Archival Pigment accent palette + +Flat, matte inks — extracted from historical manuscripts. Never gradient, never glossy. For overlapping data areas, apply at **20–40% opacity** so the paper texture bleeds through. + +| Swatch | Hex | Use | +|---|---|---| +| Faded Terracotta | `#b96b58` | Warnings, critical alerts, destructive | +| Oxidized Copper | `#6d8c7c` | Success, growth, positive trends | +| Raw Ochre | `#c29d59` | Cautions, tertiary accents | +| Faded Indigo | `#5a6d83` | Info, neutral data (the only "blue") | +| Dusty Heather | `#8d7a85` | Categorical, supporting iconography | + +**No "tech blue."** Ever. The only permissible blue is the heavily desaturated `#5a6d83`. + +### Typography + +- **Headlines (Newsreader serif)** — `display` and `headline` scales. At `display-lg` use letter-spacing `-0.02em` for a locked-in editorial feel. +- **Body & labels (Manrope sans)** — `title`, `body`, `label` scales. Body line-height **1.6** against the warm background. +- **Hierarchy rule:** always lead with the serif for *intent*, follow with sans for *execution*. +- **Labels** are all-caps, tracked `+0.08em`, in `--on-surface-variant`. + +Headlines are **always left-aligned**. No centered text in hero sections. A strong vertical axis is non-negotiable. + +### Spacing — the editorial margin + +Desktop lateral padding is `--space-20` (**7rem / 112px**). Hero cards use `--space-8` (44px) padding. Between list items use `--space-6` (32px). Generous whitespace is the system's primary visual tool. + +### Backgrounds & imagery + +- **No full-bleed photography in chrome.** Imagery lives in contained frames, always. +- **Preferred imagery vibe:** warm, matte, desaturated — think museum-catalog reproductions. Film grain is welcome; high saturation is not. +- **Hand-drawn graphite illustrations** are the primary iconography: 1–1.5px stroke in `--on-surface` `#383831`, clean but slightly organic. One **Archival Pigment** accent per icon, used on a single conceptual element (Oxidized Copper for a positive trend line, Faded Terracotta for a warning mark). +- **Data & connectivity** is drawn as **topographic currents** — smooth, parallel, hand-drawn contour lines that wrap around figures and interface elements. Never nodes-and-links. Never solid dots. +- **Data grids** (axes, rules) use the **Ghost Border** at 15–30% opacity. No solid axis lines. +- **No patterns, no gradients** on chrome. Primary CTA is a flat fill — no gradient, no shadow. + +### Elevation & depth + +Depth comes from **tonal layering**, not shadow. Stack surface tiers. When a floating effect is *truly* needed: + +- Blur **32–64px**, opacity **4–6%**, color `--on-surface` (not pure black — keeps the shadow warm). +- `--shadow-ambient` for cards, `--shadow-float` for popovers, `--shadow-modal` for overlays. + +### Borders + +Forbidden for layout. If accessibility requires a border (focus ring, form field bottom edge): use the **Ghost Border** — `--outline-variant` at **15% opacity**. Never a solid 100% border. Focus rings use a 2px outset in `--secondary` at 40% opacity. + +### Corner radii + +- `--radius-md` **0.75rem (12px)** — the default. Buttons, cards, inputs. +- `--radius-lg` **1.25rem (20px)** — hero cards, modals. +- `--radius-sm` **0.375rem (6px)** — dense UI, tags, chips. +- **No fully-rounded pills** except for runic state dots and avatars (`--radius-full`). + +### Cards + +- `--radius-md`, no border, no solid shadow — **tonal lift only**. Place a lighter tier card (`surface-container-lowest`) on a darker tier section (`surface-container-low`). +- Hero cards: `--space-8` (44px) padding, generous. +- Standard cards: `--space-5` to `--space-6` padding. +- If elevation is needed, use `--shadow-ambient` — a whisper. + +### Transparency & blur (Glass) + +Reserved for **floating overlays only**: command palettes, toasts, floating search, dropdowns over scrollable content. Apply `backdrop-filter: blur(12–20px)` to a `surface` token at **80% opacity**. Do not use glass on chrome, cards, or hero sections. + +### Hover, press, focus + +| State | Treatment | +|---|---| +| Hover (primary button) | fill darkens `#785f53` → `#6b5348`; no scale | +| Hover (secondary) | fill darkens one tonal tier | +| Hover (tertiary) | fill deepens, arrow slides 4px right | +| Hover (link) | border-bottom from 30% opacity → 100% | +| Press | 1px translateY down, no scale-shrink | +| Focus | 2px `--secondary` ring at 40% opacity, 3px offset | +| Disabled | 40% opacity, no cursor change on hover | + +No bounce. No elastic. No "playful" overshoots. The whole system has **one easing**: `cubic-bezier(0.2, 0.0, 0, 1)` and **one speed**: 240ms. Fades and tonal shifts only. + +### Animation + +- Transitions: 140ms (fast micro), 240ms (default), 420ms (layout). +- No bounces, no springs, no overshoots. +- Loading state uses **interactive outline typography** (drifting Newsreader letterforms behind hand-drawn human illustrations) paired with **topographic currents**. No spinners, no progress circles, no runic rotations. +- Page entrances: **fade + 4px translate-up**. Never scale. Never slide from off-screen. + +### Layout rules + +- Desktop content max-width `72rem` (1152px), reading measure max `42rem` (672px). +- Lateral margin `--space-20` (7rem) on desktop; `--space-5` (24px) on mobile. +- Fixed header (if present) uses glass surface; never a solid fill. +- **Asymmetry is encouraged.** Illustrations and runic accents may bleed off-center or overlap container edges. + +--- + +## 4. Components + +### Buttons + +| Variant | Spec | +|---|---| +| **Primary** | `#785f53` flat fill · white text · `radius-md` (12px) · no shadow · hover darkens to `#6b5348` | +| **Secondary** | `surface-container-highest` `#ddd6c3` fill · `--on-surface` text · reads like a subtle indentation in the page | +| **Tertiary** | Warm-wheat `#d9c9a8` fill · dark-walnut text · italic serif arrow that slides right on hover. Kept as a distinct colored button — no underline, no ghost states. | + +### Cards & lists + +- **The rule of no dividers.** Forbid horizontal lines between list items. Separate with `--space-6` (2rem) of whitespace, or alternate background tints between `surface` and `surface-container`. +- **Cards:** `--radius-md` (12px). Generous padding — `--space-8` (2.75rem) for hero cards to maintain the editorial feel; `--space-5` to `--space-6` for standard cards. +- No borders. Elevation is tonal — place a lighter tier card on a darker tier section. + +### Input fields + +- **Forgo the box.** `surface-variant` (`#e7e1d0`) background with a **bottom-only Ghost Border** (`--outline-variant` at 30% opacity). No side borders, no rounded box. +- **Labels:** `label-sm` size, all-caps, tracked `+0.08em`, in `--secondary-fixed-dim` (`#9a8679`) — muted, sophisticated. +- **Focus:** bottom border thickens to `--secondary` (`#785f53`). + +### Iconography + +See [`ICONOGRAPHY.md`](./ICONOGRAPHY.md) for full detail. + +- **Hand-drawn, thin stroke** (1px or 1.5px) in `--on-surface` `#383831`. Clean but slightly organic — intentional micro-asymmetry. +- **Pigment accents:** one **Archival Pigment** per icon, applied to a single conceptual element (Oxidized Copper trend line, Faded Terracotta warning flame). +- **No filled Material icons. No emoji. No cartoon illustration.** + +### Signature components + +- **Interactive outline typography** (loader). Replaces all spinners and runic geometry. Outlined Newsreader letterforms drift, rotate and morph behind hand-drawn human illustrations — the meeting point of human expression and advanced technology. See `preview/outline-type-loader.html`. +- **Topographic currents** (data & connectivity). Parallel, hand-drawn contour lines that wrap around figures and interface elements like elevation rings. Replaces all node-and-link visualizations. See `preview/topographic-currents.html`. + +--- + +## 5. Font substitutions + +No substitutions required — all font files were provided. + +- **Manrope** (supplied, 7 weights: 200, 300, 400, 500, 600, 700, 800) — self-hosted from `fonts/`. +- **Newsreader** (supplied, 5 cuts: Regular, Italic, Bold, BoldItalic, ExtraBold) — self-hosted from `fonts/`. + +Missing from the upload but referenced in the system: +- No monospace was provided. We default to **JetBrains Mono** / system mono stack. If a specific mono is desired, flag it and we'll swap it in. + +--- + +## 6. Index — files in this project + +### Root +- `README.md` — this file +- `ICONOGRAPHY.md` — icon approach, library choice, usage rules +- `colors_and_type.css` — all CSS variables + semantic base styles (import this everywhere) +- `SKILL.md` — agent-skill manifest +- `fonts/` — Manrope + Newsreader TTFs (self-hosted) +- `assets/` — logos (icon + wordmark, black + white variants) + +### Preview cards +- `preview/` — individual HTML cards that populate the Design System review tab (one card per sub-concept) + +### UI kits +- `ui_kits/product/` — hi-fi recreation of the Fenja AI product surface (chat-with-archive) + - `index.html` — interactive demo + - `*.jsx` — component files + +### Slides +- Not included. No slide template was attached. + +--- + +## 7. Known limitations / asks + +- **No codebase or Figma was attached.** UI kit screens are designed from the written spec and should be treated as a reference implementation. +- **Custom iconography does not yet exist.** We use Lucide at 1.5px stroke weight as a stand-in. A bespoke set matching the "runic + graphite" description is the biggest open gap. +- **Illustration assets are not yet produced.** Where illustrations are called for, we ship labelled placeholders. +- **Product scope unclear.** The system as written points toward a research/knowledge product — we modeled a single surface ("Archive") accordingly. Please confirm whether there's a marketing site, a docs site, or other surfaces we should cover. diff --git a/design/SKILL.md b/design/SKILL.md new file mode 100644 index 0000000..a415eb3 --- /dev/null +++ b/design/SKILL.md @@ -0,0 +1,32 @@ +--- +name: fenja-design +description: Use this skill to generate well-branded interfaces and assets for Fenja AI, either for production or throwaway prototypes/mocks/etc. Contains essential design guidelines, colors, type, fonts, assets, and UI kit components for prototyping. +user-invocable: true +--- + +Read the README.md file within this skill, and explore the other available files. + +Fenja AI is a "Nordic Editorial" / "Digital Archivist" brand. The design language is quiet, academic, and paper-like — unbleached neutrals, hand-rubbed wood, and five matte "Archival Pigment" accents. No 1px dividers; depth comes from tonal surface shifts. Typography pairs Newsreader (serif, for intent) with Manrope (sans, for execution). + +Core files: +- `README.md` — brand context, content fundamentals, visual foundations +- `ICONOGRAPHY.md` — icon approach, Lucide substitution, usage rules +- `colors_and_type.css` — all CSS variables + semantic base styles (`@import` or `` this into every artifact) +- `fonts/` — self-hosted Manrope + Newsreader TTFs +- `assets/` — logos (icon + wordmark, black + white) +- `preview/` — small preview cards for each system concept +- `ui_kits/product/` — reference product implementation (React + JSX) + +If creating visual artifacts (slides, mocks, throwaway prototypes, etc), copy `colors_and_type.css` and the needed assets out, then build static HTML files that link the stylesheet. If working on production code, copy assets and read the rules here to become an expert in designing with this brand. + +If the user invokes this skill without any other guidance, ask them what they want to build or design, ask some questions, and act as an expert designer who outputs HTML artifacts or production code, depending on the need. + +### Non-negotiables +- No 1px solid borders for layout (use tonal surface shifts or 15%-opacity ghost borders) +- No emoji anywhere +- No "tech blue" — only `#5a6d83` Faded Indigo +- No centered hero text — always left-aligned with a strong vertical axis +- Primary CTA: vertical gradient `#785f53 → #6b5348`, radius 12px +- Headlines always Newsreader with `letter-spacing: -0.02em` at display sizes +- One easing (`cubic-bezier(.2, 0, 0, 1)`), three durations (140 / 240 / 420ms), no bounces +- Generous margins — `7rem` lateral padding on desktop hero sections diff --git a/design/assets/fenja-icon-black.svg b/design/assets/fenja-icon-black.svg new file mode 100644 index 0000000..2ee63b2 --- /dev/null +++ b/design/assets/fenja-icon-black.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/assets/fenja-icon-white.svg b/design/assets/fenja-icon-white.svg new file mode 100644 index 0000000..97e9386 --- /dev/null +++ b/design/assets/fenja-icon-white.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/assets/fenja-wordmark-black.svg b/design/assets/fenja-wordmark-black.svg new file mode 100644 index 0000000..a39a16b --- /dev/null +++ b/design/assets/fenja-wordmark-black.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/assets/fenja-wordmark-white.svg b/design/assets/fenja-wordmark-white.svg new file mode 100644 index 0000000..b049d06 --- /dev/null +++ b/design/assets/fenja-wordmark-white.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/assets/reference-boulderer.png b/design/assets/reference-boulderer.png new file mode 100644 index 0000000..78272dd Binary files /dev/null and b/design/assets/reference-boulderer.png differ diff --git a/design/assets/reference-flowerman-ochre.png b/design/assets/reference-flowerman-ochre.png new file mode 100644 index 0000000..259dfc9 Binary files /dev/null and b/design/assets/reference-flowerman-ochre.png differ diff --git a/design/assets/reference-flowerman-white.png b/design/assets/reference-flowerman-white.png new file mode 100644 index 0000000..f633125 Binary files /dev/null and b/design/assets/reference-flowerman-white.png differ diff --git a/design/assets/reference-waves.png b/design/assets/reference-waves.png new file mode 100644 index 0000000..ce8bb8e Binary files /dev/null and b/design/assets/reference-waves.png differ diff --git a/design/colors_and_type.css b/design/colors_and_type.css new file mode 100644 index 0000000..547e18b --- /dev/null +++ b/design/colors_and_type.css @@ -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); /* 56–88 */ + --text-display-lg: clamp(3rem, 5vw, 4.5rem); /* 48–72 */ + --text-display-md: clamp(2.5rem, 4vw, 3.5rem); /* 40–56 */ + --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); } diff --git a/design/fonts/Manrope-Bold.ttf b/design/fonts/Manrope-Bold.ttf new file mode 100644 index 0000000..62a6183 Binary files /dev/null and b/design/fonts/Manrope-Bold.ttf differ diff --git a/design/fonts/Manrope-ExtraBold.ttf b/design/fonts/Manrope-ExtraBold.ttf new file mode 100644 index 0000000..2fa671c Binary files /dev/null and b/design/fonts/Manrope-ExtraBold.ttf differ diff --git a/design/fonts/Manrope-ExtraLight.ttf b/design/fonts/Manrope-ExtraLight.ttf new file mode 100644 index 0000000..c55745a Binary files /dev/null and b/design/fonts/Manrope-ExtraLight.ttf differ diff --git a/design/fonts/Manrope-Light.ttf b/design/fonts/Manrope-Light.ttf new file mode 100644 index 0000000..8a771c2 Binary files /dev/null and b/design/fonts/Manrope-Light.ttf differ diff --git a/design/fonts/Manrope-Medium.ttf b/design/fonts/Manrope-Medium.ttf new file mode 100644 index 0000000..c6d28de Binary files /dev/null and b/design/fonts/Manrope-Medium.ttf differ diff --git a/design/fonts/Manrope-Regular.ttf b/design/fonts/Manrope-Regular.ttf new file mode 100644 index 0000000..9a108f1 Binary files /dev/null and b/design/fonts/Manrope-Regular.ttf differ diff --git a/design/fonts/Manrope-SemiBold.ttf b/design/fonts/Manrope-SemiBold.ttf new file mode 100644 index 0000000..46a13d6 Binary files /dev/null and b/design/fonts/Manrope-SemiBold.ttf differ diff --git a/design/fonts/Newsreader-Bold.ttf b/design/fonts/Newsreader-Bold.ttf new file mode 100644 index 0000000..6d9e20a Binary files /dev/null and b/design/fonts/Newsreader-Bold.ttf differ diff --git a/design/fonts/Newsreader-BoldItalic.ttf b/design/fonts/Newsreader-BoldItalic.ttf new file mode 100644 index 0000000..bc57925 Binary files /dev/null and b/design/fonts/Newsreader-BoldItalic.ttf differ diff --git a/design/fonts/Newsreader-ExtraBold.ttf b/design/fonts/Newsreader-ExtraBold.ttf new file mode 100644 index 0000000..69a726d Binary files /dev/null and b/design/fonts/Newsreader-ExtraBold.ttf differ diff --git a/design/fonts/Newsreader-Italic.ttf b/design/fonts/Newsreader-Italic.ttf new file mode 100644 index 0000000..477facd Binary files /dev/null and b/design/fonts/Newsreader-Italic.ttf differ diff --git a/design/fonts/Newsreader-Regular.ttf b/design/fonts/Newsreader-Regular.ttf new file mode 100644 index 0000000..9fe7694 Binary files /dev/null and b/design/fonts/Newsreader-Regular.ttf differ diff --git a/design/preview/brand-logos.html b/design/preview/brand-logos.html new file mode 100644 index 0000000..8d4ca58 --- /dev/null +++ b/design/preview/brand-logos.html @@ -0,0 +1,27 @@ + + + + +Brand — Logos + + + + +
+
Fenja icon
+
Fenja wordmark
+
Fenja icon, white
+
Fenja wordmark, white
+
+ + diff --git a/design/preview/buttons.html b/design/preview/buttons.html new file mode 100644 index 0000000..68bc231 --- /dev/null +++ b/design/preview/buttons.html @@ -0,0 +1,39 @@ + + + + +Buttons + + + + +
+ + + +
+ + diff --git a/design/preview/card-hero.html b/design/preview/card-hero.html new file mode 100644 index 0000000..1d4966a --- /dev/null +++ b/design/preview/card-hero.html @@ -0,0 +1,25 @@ + + + + +Card — Hero + + + + +
+
+
§ Field note · 04.18.26
+

A patient reader, by design.

+

Fenja reads slowly, annotates quietly, and files the thought where you can find it again six months from now.

+
+
+ + diff --git a/design/preview/colors-pigments.html b/design/preview/colors-pigments.html new file mode 100644 index 0000000..4af89dc --- /dev/null +++ b/design/preview/colors-pigments.html @@ -0,0 +1,29 @@ + + + + +Colors — Archival Pigments + + + + +
+
+
Faded Terracotta
#b96b58 · critical
+
Oxidized Copper
#6d8c7c · success
+
Raw Ochre
#c29d59 · caution
+
Faded Indigo
#5a6d83 · info
+
Dusty Heather
#8d7a85 · categorical
+
+
Flat, matte inks — never gradient. 20–40% opacity for overlapping data fills.
+
+ + diff --git a/design/preview/colors-surfaces.html b/design/preview/colors-surfaces.html new file mode 100644 index 0000000..6ad6f35 --- /dev/null +++ b/design/preview/colors-surfaces.html @@ -0,0 +1,30 @@ + + + + +Colors — Surfaces + + + + +
+
+
lowest
#fffcf7
+
surface
#faf6ee
+
low
#f6f2e8
+
container
#efeadc
+
high
#e7e1d0
+
highest
#ddd6c3
+
+
Six tonal tiers of unbleached paper to warm clay. Never pure white — #fffcf7 is the lightest.
+
+ + diff --git a/design/preview/colors-text.html b/design/preview/colors-text.html new file mode 100644 index 0000000..b828145 --- /dev/null +++ b/design/preview/colors-text.html @@ -0,0 +1,27 @@ + + + + +Colors — Text & Neutrals + + + + +
+
+
on-surface
#383831
+
on-surface-variant
#5f5e5e
+
muted
#8a887f
+
secondary
#785f53
+
secondary-dim
#6b5348
+
+
+ + diff --git a/design/preview/elevation.html b/design/preview/elevation.html new file mode 100644 index 0000000..01fe5be --- /dev/null +++ b/design/preview/elevation.html @@ -0,0 +1,26 @@ + + + + +Elevation — Atmospheric Shadows + + + + +
+
ambient
cards · 6% warm
+
float
popovers
+
modal
overlays
+
+ + diff --git a/design/preview/glass.html b/design/preview/glass.html new file mode 100644 index 0000000..a426454 --- /dev/null +++ b/design/preview/glass.html @@ -0,0 +1,27 @@ + + + + +Glass Overlay + + + + +
+
+
Floating search · glass surface
+
Ask the archive.
+
80% surface · 16px backdrop blur · warm atmospheric shadow.
+
+
+ + diff --git a/design/preview/hand-drawn-icons.html b/design/preview/hand-drawn-icons.html new file mode 100644 index 0000000..42b5d19 --- /dev/null +++ b/design/preview/hand-drawn-icons.html @@ -0,0 +1,129 @@ + + + + +Hand-drawn Iconography + + + + +
+ + +
+ + + + + + + + + + +
Reader
+
+ + +
+ + + + + + + + +
Archive
+
+ + +
+ + + + + + +
Write
+
+ + +
+ + + + + + + +
Search
+
+ + +
+ + + + + + +
Trend
+
+ + +
+ + + + + +
Warning
+
+ + +
+ + + + + +
Saved
+
+ + +
+ + + + + + + + +
Conversation
+
+ +
+ + diff --git a/design/preview/icons-lucide.html b/design/preview/icons-lucide.html new file mode 100644 index 0000000..7ce9e7e --- /dev/null +++ b/design/preview/icons-lucide.html @@ -0,0 +1,177 @@ + + + + +Iconography — hand-drawn + + + + +
+ + +
+ + + + + + +
Feather
+
+ + +
+ + + + + + + +
Book
+
+ + +
+ + + + + +
Search
+
+ + +
+ + + + + +
Saved
+
+ + +
+ + + +
Star
+
+ + +
+ + + + + + +
File
+
+ + +
+ + + + + + +
Archive
+
+ + +
+ + + + + +
Spear
+
+ + +
+ + + + + + + + +
Conversation
+
+ + +
+ + + + + + +
Trend
+
+ + +
+ + + + +
Warning
+
+ + +
+ + + + + + + +
Map
+
+ +
+ Thin 1.25px graphite stroke · intentional micro-asymmetry · a single pigment accent per icon (Oxidized Copper · Faded Terracotta · Raw Ochre · Faded Indigo). Substituted set — bespoke Fenja mark pending. +
+ +
+ + diff --git a/design/preview/inputs.html b/design/preview/inputs.html new file mode 100644 index 0000000..771ff2f --- /dev/null +++ b/design/preview/inputs.html @@ -0,0 +1,52 @@ + + + + +Input Fields + + + + +
+
+ + +
+
+ + +
Auto-saved a moment ago.
+
+
+ + diff --git a/design/preview/list-items.html b/design/preview/list-items.html new file mode 100644 index 0000000..44ef4de --- /dev/null +++ b/design/preview/list-items.html @@ -0,0 +1,24 @@ + + + + +List — No-Dividers + + + + +
+
On stillness, and other tools
§ 12 min
Filed in Essays · 3 days ago
+
Norse metaphors for modern ML
§ 8 min
Filed in Research · last week
+
The margin is the point
§ 4 min
Filed in Essays · March
+
+ + diff --git a/design/preview/motion.html b/design/preview/motion.html new file mode 100644 index 0000000..c8ad563 --- /dev/null +++ b/design/preview/motion.html @@ -0,0 +1,228 @@ + + + + +Motion + + + + +
+ + +
+
Standard ease
cubic-bezier(0.2, 0, 0, 1)
+
+
+
+
One curve for the entire system. No bounces, no springs.
+
+ + +
+
Durations
140 · 240 · 420 ms
+
+
+
140 · micro
+
240 · default
+
420 · layout
+
+
+
Fades and tonal shifts only.
+
+ + +
+
Entrance
Fade + 4px translate-up
+
+
+
§ 1
+
§ 2
+
§ 3
+
+
+
Staggered. Never scale. Never off-screen.
+
+ + +
+
Press
1px translateY, no scale
+
+ +
+
Fill darkens one step on press.
+
+ + +
+
Focus
2px ring · 3px offset
+
+
Search the archive
+
+
Secondary brown at 40% opacity. Never a solid border.
+
+ + +
+
Tonal shift
Background tier, not shadow
+
+
+
+
Depth is a paper tier. Elevation is never a box-shadow first.
+
+ +
+ + diff --git a/design/preview/outline-type-loader.html b/design/preview/outline-type-loader.html new file mode 100644 index 0000000..3589c09 --- /dev/null +++ b/design/preview/outline-type-loader.html @@ -0,0 +1,184 @@ + + + + +Outline Typography Loader + + + + +
+
+
Signature · Loader
+
A slow hand-drawn bloom.
+

+ In place of spinners or runic geometry, the loader is a hand-drawn illustrative graphic — skeletal, single-weight line, graphite with one archival pigment — that rotates slowly around its own centre. Inside the form, Topographic Currents flow as contour lines. +

+

+ Concepts are chosen to match the task: a bloom for reading, a spiral for thinking, a vessel for filing. Always introspective. Never a spinner. +

+
+ Palette graphite #383831 + one pigment · Line uniform 1.5px · Motion 9–22s linear rotate · Never dots, grids, or 3D +
+
+ +
+ + +
+ + + + + + + + + + + + + + + + +
Reading · bloom
+
+ + +
+ + + + + + + + + + + +
Thinking · spiral
+
+ + +
+ + + + + + + + + + + + + + + + + +
Filing · vessel
+
+ +
+
+ + diff --git a/design/preview/radii.html b/design/preview/radii.html new file mode 100644 index 0000000..db964bd --- /dev/null +++ b/design/preview/radii.html @@ -0,0 +1,25 @@ + + + + +Radii + + + + +
+
none
0
+
sm
6px
+
md · default
12px
+
lg
20px
+
full
+
+ + diff --git a/design/preview/spacing-scale.html b/design/preview/spacing-scale.html new file mode 100644 index 0000000..2dc17bd --- /dev/null +++ b/design/preview/spacing-scale.html @@ -0,0 +1,28 @@ + + + + +Spacing Scale + + + + +
+
space-2
0.5 · 8
+
space-3
0.75 · 12
+
space-4
1 · 16
+
space-5
1.5 · 24
+
space-6
2 · 32
+
space-8
2.75 · 44
+
space-12
5 · 80
+
space-20
7 · 112
+
+ + diff --git a/design/preview/state-chips.html b/design/preview/state-chips.html new file mode 100644 index 0000000..9fcdc09 --- /dev/null +++ b/design/preview/state-chips.html @@ -0,0 +1,32 @@ + + + + +Pigment States + + + + +
+
Filed
Saved to your archive.
+
Review before publishing
Two citations are missing.
+
Could not reach the archive
Retry?
+
Reading your document
Just a moment.
+
+ + diff --git a/design/preview/tonal-layering.html b/design/preview/tonal-layering.html new file mode 100644 index 0000000..146227b --- /dev/null +++ b/design/preview/tonal-layering.html @@ -0,0 +1,30 @@ + + + + +Tonal Layering + + + + +
+
+
surface-container #efeadc
+
+
surface · canvas #faf6ee
+
+
surface-container-lowest #fffcf7
+
+
+
+
+ + diff --git a/design/preview/topographic-currents.html b/design/preview/topographic-currents.html new file mode 100644 index 0000000..1eda8d1 --- /dev/null +++ b/design/preview/topographic-currents.html @@ -0,0 +1,516 @@ + + + + +Topographic Currents + + + + +
+ +
+
Signature · Data & connectivity
+
Topographic currents.
+

+ Parallel wavy lines running across a form, contained by its silhouette like water filling a vessel. Each region has a dominant flow axis; neighbouring lines undulate in the same phase with small offsets, producing a current that moves through the shape. +

+

+ The silhouette is a firm graphite contour. The interior is the same graphite at the same weight — but spaced so the cream ground between lines breathes as much as the ink itself. +

+ +
+
Flow axisPick one dominant direction per region. The lines run across it, not around it.
+
Phase matchAdjacent lines bend together. Coordinated waves, never independent noise.
+
SpacingEqual cream gaps between lines. The negative space is half the drawing.
+
TerminationLines end where the silhouette ends. They do not close.
+
PaletteGraphite silhouette + cream interior lines over any Archival Pigment ground. The pigment is the field, graphite holds the form, cream carries the current.
+
+
+ +
+
+ Raw ochre +
Figure · vertical current
+
+
+ Oxidized copper +
Leaf · grain along the blade
+
+
+ Faded terracotta +
Petal · radial current
+
+
+ Faded indigo +
Stone · horizontal current
+
+
+ +
+ + + + diff --git a/design/preview/type-body.html b/design/preview/type-body.html new file mode 100644 index 0000000..75bf0d4 --- /dev/null +++ b/design/preview/type-body.html @@ -0,0 +1,25 @@ + + + + +Type — Body & Labels + + + + +
+
lead · italic
A thinking tool. Built slowly.
+
body-md
Manrope 16 / 1.6. The working body size for long-form reading against the warm paper.
+
body-sm
Manrope 14 / 1.5. Used for metadata, captions, and secondary annotations.
+
label-md
Filed · 3 minutes ago
+
+ + diff --git a/design/preview/type-display.html b/design/preview/type-display.html new file mode 100644 index 0000000..ea83323 --- /dev/null +++ b/design/preview/type-display.html @@ -0,0 +1,22 @@ + + + + +Type — Display + + + + +
+
Display · Newsreader 72 / 400 / −0.02em
+
A study in stillness.
+
Always left-aligned. Never centered. Lead with the serif — follow with the sans.
+
+ + diff --git a/design/preview/type-families.html b/design/preview/type-families.html new file mode 100644 index 0000000..1c2e5a6 --- /dev/null +++ b/design/preview/type-families.html @@ -0,0 +1,31 @@ + + + + +Type — Font Families + + + + +
+
+
Serif · intent
+
Newsreader
+
400 / 400i / 700 / 700i / 800
+
+
+
Sans · execution
+
Manrope
+
200 · 300 · 400 · 500 · 600 · 700 · 800
+
+
+ + diff --git a/design/preview/type-headlines.html b/design/preview/type-headlines.html new file mode 100644 index 0000000..02a9305 --- /dev/null +++ b/design/preview/type-headlines.html @@ -0,0 +1,26 @@ + + + + +Type — Headlines & Titles + + + + +
+
headline-lg
The archive is listening.
+
headline-md
A patient reader, by design.
+
headline-sm
Notes from the margin
+
title-lg · Sans
Saved to your archive
+
+ + diff --git a/design/screenshots/brand-logos-check.png b/design/screenshots/brand-logos-check.png new file mode 100644 index 0000000..35f3f9d Binary files /dev/null and b/design/screenshots/brand-logos-check.png differ diff --git a/design/screenshots/brand-logos-check2.png b/design/screenshots/brand-logos-check2.png new file mode 100644 index 0000000..35f3f9d Binary files /dev/null and b/design/screenshots/brand-logos-check2.png differ diff --git a/design/screenshots/brand-logos-final.png b/design/screenshots/brand-logos-final.png new file mode 100644 index 0000000..eeaf468 Binary files /dev/null and b/design/screenshots/brand-logos-final.png differ diff --git a/design/screenshots/brand-logos-v3.png b/design/screenshots/brand-logos-v3.png new file mode 100644 index 0000000..08b2b36 Binary files /dev/null and b/design/screenshots/brand-logos-v3.png differ diff --git a/design/screenshots/brand-logos.png b/design/screenshots/brand-logos.png new file mode 100644 index 0000000..35f3f9d Binary files /dev/null and b/design/screenshots/brand-logos.png differ diff --git a/design/screenshots/ui-kit-archive.png b/design/screenshots/ui-kit-archive.png new file mode 100644 index 0000000..761100c Binary files /dev/null and b/design/screenshots/ui-kit-archive.png differ diff --git a/design/ui_kits/product/README.md b/design/ui_kits/product/README.md new file mode 100644 index 0000000..7e9a547 --- /dev/null +++ b/design/ui_kits/product/README.md @@ -0,0 +1,25 @@ +# Fenja — Product UI Kit + +A hi-fi reference implementation of the Fenja archive product: a quiet reading + annotation surface for a personal AI research companion. + +## What's here + +- `index.html` — interactive demo. Click any document to open the reader + "Ask this document" pane; ask a question to see the runic loader and a cited reply. +- `kit.css` — kit-level styles composed over `../../colors_and_type.css`. +- `components.jsx` — atoms: `Icon`, `Button`, `IconButton`, `Tag`, `Sidebar`, `Topbar`, `FloatingSearch`, `ArchiveItem`, `Composer`, `RuneSpinner`. +- `screens.jsx` — molecules: `ArchiveScreen`, `ReaderScreen`. + +## Disclaimer + +No Fenja codebase or Figma was attached. This kit is built from the written spec and should be treated as a **reference implementation** rather than a port. The written "Nordic Editorial" system pointed toward a patient, research-oriented product surface — we modeled that as an archive + ask experience. + +## What follows the spec + +- No 1px dividers anywhere. List separation is alternating tonal tint (`surface-container-low`). +- Primary CTA uses the `secondary → secondary-dim` vertical gradient ("hand-rubbed wood"). +- Floating search uses `surface-container-lowest` with atmospheric shadow. +- The topbar uses glass (80% surface + 16px blur). +- All icons are 1.5px-stroke Lucide-style SVGs (inline). These are flagged as a substitution for the aspirational custom runic set. +- Loader is the signature runic geometry, not a spinner. +- Headlines: Newsreader, left-aligned, `−0.02em` tracking. Body: Manrope 1.6 line-height. +- Highlights in the reader use raw-ochre pigment at 22% opacity (matches the "paper bleed-through" rule). diff --git a/design/ui_kits/product/components.jsx b/design/ui_kits/product/components.jsx new file mode 100644 index 0000000..d3fd1aa --- /dev/null +++ b/design/ui_kits/product/components.jsx @@ -0,0 +1,128 @@ +// Fenja UI Kit — Components +// Shared icons +const Ic = { + feather: (p) => (), + search: (p) => (), + book: (p) => (), + bookmark: (p) => (), + note: (p) => (), + plus: (p) => (), + settings: (p) => (), + send: (p) => (), + star: (p) => (), + archive: (p) => (), + quote: (p) => (), +}; + +const Icon = ({ name, size = 18, ...rest }) => { + const C = Ic[name]; + return C ? : null; +}; + +const RuneSpinner = () => ( + + + + + + + +); + +const Button = ({ kind = "primary", children, onClick }) => ( + +); + +const IconButton = ({ name, label, onClick }) => ( + +); + +const Tag = ({ children }) => {children}; + +const Sidebar = ({ view, setView }) => ( + +); + +const Topbar = ({ crumbs, children }) => ( +
+
+ {crumbs.map((c, i) => ( + + {i > 0 && } + {c} + + ))} +
+
{children}
+
+); + +const FloatingSearch = ({ onFocus }) => ( +
+ + + ⌘K +
+); + +const ArchiveItem = ({ item, onOpen }) => ( +
onOpen(item)}> +
+
{item.title}
+
{item.excerpt}
+
+
{item.collection}
+
{item.meta}
+
+); + +const Composer = ({ value, onChange, onSend, loading }) => ( +
+ {loading ? : } + onChange(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && onSend()} + /> + +
+); + +Object.assign(window, { + Icon, Button, IconButton, Tag, Sidebar, Topbar, FloatingSearch, + ArchiveItem, Composer, RuneSpinner, +}); diff --git a/design/ui_kits/product/index.html b/design/ui_kits/product/index.html new file mode 100644 index 0000000..eb4ea06 --- /dev/null +++ b/design/ui_kits/product/index.html @@ -0,0 +1,58 @@ + + + + + Fenja — Archive + + + + + + + +
+ + + + + + + diff --git a/design/ui_kits/product/kit.css b/design/ui_kits/product/kit.css new file mode 100644 index 0000000..48b0e40 --- /dev/null +++ b/design/ui_kits/product/kit.css @@ -0,0 +1,166 @@ +/* Fenja UI Kit — composed over colors_and_type.css */ + +.app { + display: grid; + grid-template-columns: 260px 1fr; + height: 100vh; + background: var(--surface-container-low); + color: var(--on-surface); + font-family: var(--font-sans); +} + +/* ---------- Sidebar ---------- */ +.sidebar { + background: var(--surface-container); + padding: 28px 22px; + display: flex; flex-direction: column; + gap: 28px; + overflow-y: auto; +} +.brand { + display: flex; align-items: center; + padding-bottom: 8px; +} +.brand .wordmark { height: 26px; width: auto; display: block; } + +.nav-section { display: flex; flex-direction: column; gap: 4px; } +.nav-section .heading { + font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; + color: var(--on-surface-muted); font-weight: 500; + margin: 0 0 6px 8px; +} +.nav-item { + display: flex; align-items: center; gap: 10px; + padding: 9px 10px; + border-radius: 8px; + color: var(--on-surface-variant); + cursor: pointer; + font-size: 14px; + transition: background var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); +} +.nav-item:hover { background: var(--surface-container-high); color: var(--on-surface); } +.nav-item.active { background: var(--surface-container-lowest); color: var(--on-surface); font-weight: 500; } +.nav-item svg { width: 16px; height: 16px; stroke: currentColor; stroke-width: 1.5; fill: none; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; } +.nav-item .count { margin-left: auto; font-size: 11px; color: var(--on-surface-muted); letter-spacing: 0.04em; } + +.sidebar-footer { margin-top: auto; display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 10px; background: var(--surface-container-high); } +.avatar { width: 28px; height: 28px; border-radius: 50%; background: var(--pigment-indigo); color: #fffcf7; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; } +.sidebar-footer .name { font-size: 13px; color: var(--on-surface); font-weight: 500; } +.sidebar-footer .email { font-size: 11px; color: var(--on-surface-muted); letter-spacing: 0.02em; } + +/* ---------- Main ---------- */ +.main { + background: var(--surface); + overflow-y: auto; + position: relative; +} + +.topbar { + position: sticky; top: 0; z-index: 5; + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + background: rgba(255, 252, 247, 0.82); + padding: 18px 56px; + display: flex; align-items: center; gap: 20px; +} +.crumb { font-family: var(--font-sans); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--on-surface-muted); } +.crumb .sep { margin: 0 10px; } +.topbar-actions { margin-left: auto; display: flex; gap: 10px; align-items: center; } + +.page { padding: 56px 96px 96px; max-width: 1100px; } +.page-hero { margin-bottom: 64px; } +.eyebrow { font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--on-surface-muted); font-weight: 500; margin-bottom: 18px; } +.page-hero h1 { font-family: var(--font-serif); font-size: clamp(44px, 5vw, 72px); letter-spacing: -0.02em; line-height: 1.05; font-weight: 400; margin: 0 0 18px; max-width: 20ch; } +.page-hero h1 em { font-style: italic; font-weight: 700; } +.page-hero .lead { font-family: var(--font-serif); font-style: italic; font-size: 19px; color: var(--on-surface-variant); line-height: 1.55; max-width: 54ch; margin: 0; } + +/* ---------- Buttons ---------- */ +.btn { font-family: var(--font-sans); font-size: 14px; font-weight: 500; border: 0; cursor: pointer; padding: 10px 20px; border-radius: 12px; letter-spacing: 0; transition: transform var(--duration-fast) var(--ease-standard), filter var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard); } +.btn-primary { color: #fff; background: linear-gradient(180deg, #785f53, #6b5348); } +.btn-primary:hover { filter: brightness(1.08); } +.btn-primary:active { transform: translateY(1px); } +.btn-secondary { background: var(--surface-container-highest); color: var(--on-surface); } +.btn-secondary:hover { background: #e0ddd2; } +.btn-tertiary { background: transparent; color: var(--primary); padding: 10px 8px; position: relative; } +.btn-tertiary::after { content: ""; position: absolute; left: 8px; right: 14px; bottom: 6px; height: 2px; background: var(--secondary); transform: scaleX(0); transform-origin: left; transition: transform 240ms var(--ease-standard); } +.btn-tertiary:hover::after { transform: scaleX(1); } + +.btn-icon { background: transparent; border: 0; width: 34px; height: 34px; border-radius: 8px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; color: var(--on-surface-variant); transition: background var(--duration-fast) var(--ease-standard); } +.btn-icon:hover { background: var(--surface-container); color: var(--on-surface); } +.btn-icon svg { width: 16px; height: 16px; stroke: currentColor; stroke-width: 1.5; fill: none; stroke-linecap: round; stroke-linejoin: round; } + +/* ---------- Search bar ---------- */ +.floating-search { + background: var(--surface-container-lowest); + border-radius: 14px; + box-shadow: var(--shadow-float); + padding: 14px 18px; + display: flex; align-items: center; gap: 14px; + margin: 0 0 64px; +} +.floating-search svg { width: 18px; height: 18px; stroke: var(--on-surface-muted); stroke-width: 1.5; fill: none; stroke-linecap: round; stroke-linejoin: round; } +.floating-search input { flex: 1; border: 0; background: transparent; font: inherit; font-family: var(--font-sans); font-size: 16px; color: var(--on-surface); outline: none; } +.floating-search input::placeholder { color: var(--on-surface-muted); font-style: italic; font-family: var(--font-serif); } +.floating-search .shortcut { font-size: 11px; color: var(--on-surface-muted); letter-spacing: 0.06em; padding: 3px 8px; background: var(--surface-container); border-radius: 6px; font-family: var(--font-mono); } + +/* ---------- Section headers ---------- */ +.section-head { display: flex; align-items: baseline; margin: 0 0 24px; gap: 14px; } +.section-head h2 { font-family: var(--font-serif); font-size: 28px; letter-spacing: -0.01em; font-weight: 400; margin: 0; color: var(--on-surface); } +.section-head h2 em { font-style: italic; font-weight: 700; } +.section-head .count { font-family: var(--font-sans); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--on-surface-muted); } + +/* ---------- Archive list ---------- */ +.archive-list { display: grid; gap: 0; } +.archive-item { + padding: 22px 22px; + border-radius: 10px; + display: grid; + grid-template-columns: 1fr 120px 80px; + column-gap: 20px; + align-items: baseline; + cursor: pointer; + transition: background var(--duration-fast) var(--ease-standard); +} +.archive-item:nth-child(odd) { background: var(--surface-container-low); } +.archive-item:hover { background: var(--surface-container); } +.archive-item .title { font-family: var(--font-serif); font-size: 19px; line-height: 1.3; color: var(--on-surface); font-weight: 400; } +.archive-item .meta { font-family: var(--font-sans); font-size: 12px; color: var(--on-surface-muted); letter-spacing: 0.04em; } +.archive-item .pigment { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--on-surface-variant); } +.archive-item .pigment .dot { width: 8px; height: 8px; border-radius: 50%; } +.archive-item .excerpt { grid-column: 1 / 2; margin-top: 8px; font-family: var(--font-sans); font-size: 14px; color: var(--on-surface-variant); line-height: 1.55; max-width: 72ch; } + +/* ---------- Reader panel (chat) ---------- */ +.reader { + display: grid; grid-template-columns: 1fr 380px; + gap: 48px; +} +.reader-main { min-width: 0; } +.reader-main .doc-title { font-family: var(--font-serif); font-size: 42px; letter-spacing: -0.015em; line-height: 1.1; margin: 0 0 12px; font-weight: 400; } +.reader-main .doc-meta { font-family: var(--font-sans); font-size: 12px; color: var(--on-surface-muted); letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 36px; } +.reader-main p { font-size: 17px; line-height: 1.7; color: var(--on-surface); max-width: 62ch; } +.reader-main p .highlight { background: rgba(194, 157, 89, 0.22); padding: 0 2px; border-radius: 2px; } + +.ask-pane { background: var(--surface-container-low); border-radius: var(--radius-lg); padding: 28px; align-self: start; position: sticky; top: 88px; } +.ask-pane .eyebrow { margin-bottom: 10px; } +.ask-pane h3 { font-family: var(--font-serif); font-size: 22px; font-weight: 400; margin: 0 0 18px; } +.ask-pane h3 em { font-style: italic; font-weight: 700; } +.thread { display: flex; flex-direction: column; gap: 20px; margin-bottom: 22px; } +.msg { font-size: 14px; line-height: 1.55; } +.msg .who { font-family: var(--font-sans); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--on-surface-muted); margin-bottom: 6px; font-weight: 500; } +.msg.user .body { color: var(--on-surface); font-family: var(--font-sans); } +.msg.fenja .body { color: var(--on-surface); font-family: var(--font-serif); font-style: italic; } +.msg .cite { display: inline-block; margin-top: 6px; font-family: var(--font-sans); font-style: normal; font-size: 11px; color: var(--secondary); letter-spacing: 0.04em; border-bottom: 1px solid rgba(120,95,83,0.3); padding-bottom: 1px; } + +.composer { background: var(--surface-container-lowest); border-radius: 12px; padding: 12px 14px; display: flex; align-items: center; gap: 10px; box-shadow: var(--shadow-ambient); } +.composer input { flex: 1; border: 0; background: transparent; font: inherit; font-family: var(--font-sans); font-size: 14px; color: var(--on-surface); outline: none; padding: 6px 0; } +.composer input::placeholder { font-family: var(--font-serif); font-style: italic; color: var(--on-surface-muted); } + +/* ---------- Runic loader ---------- */ +.rune-spinner { width: 20px; height: 20px; animation: fenja-spin 6s linear infinite; } +.rune-spinner svg { width: 100%; height: 100%; } +.rune-spinner path, .rune-spinner line, .rune-spinner polygon { stroke: var(--on-surface); stroke-width: 1.5; fill: none; stroke-linecap: round; stroke-linejoin: round; } +.rune-spinner .accent { stroke: var(--pigment-terracotta); } +@keyframes fenja-spin { to { transform: rotate(360deg); } } + +/* ---------- Tags ---------- */ +.tag { display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 6px; font-family: var(--font-sans); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; background: var(--surface-container-high); color: var(--on-surface-variant); font-weight: 500; } diff --git a/design/ui_kits/product/screens.jsx b/design/ui_kits/product/screens.jsx new file mode 100644 index 0000000..3623593 --- /dev/null +++ b/design/ui_kits/product/screens.jsx @@ -0,0 +1,106 @@ +// Archive screen — main listing view +const ArchiveScreen = ({ documents, onOpen, query, setQuery }) => { + const filtered = query ? documents.filter(d => d.title.toLowerCase().includes(query.toLowerCase()) || d.excerpt.toLowerCase().includes(query.toLowerCase())) : documents; + + return ( + <> + + + + + +
+
+
§ Archive · {documents.length} documents · updated today
+

A quiet place to keep what you are reading.

+

Every document annotated. Every question remembered. Search the thing you half-recall, the way you actually remember it.

+
+ + {}} /> + +
+

Recently filed.

+ § {filtered.length} results +
+ +
+ {filtered.map(d => )} +
+
+ + ); +}; + +// Reader screen — doc + ask panel +const ReaderScreen = ({ doc, onBack }) => { + const [input, setInput] = React.useState(""); + const [loading, setLoading] = React.useState(false); + const [thread, setThread] = React.useState([ + { who: "fenja", body: "This essay returns to the metaphor of the mill three times — once as labor, once as inheritance, once as grief. The terracotta highlights above mark those returns." } + ]); + + const onSend = () => { + if (!input.trim()) return; + const q = input; + setThread(t => [...t, { who: "user", body: q }]); + setInput(""); + setLoading(true); + setTimeout(() => { + setThread(t => [...t, { + who: "fenja", + body: "The mill appears first on page two, as the giantesses' labor — then, in the third section, reframed as a family inheritance passed reluctantly.", + cite: "§ pp. 2, 7" + }]); + setLoading(false); + }, 1400); + }; + + return ( + <> + + + + + + +
+
+
+

{doc.title}

+
{doc.meta} · {doc.collection}
+ +

+ The mill does not stop, and the giantesses do not sleep. Fenja and Menja grind what the king asks them to grind, and for a long time this is gold, and for a short time this is an army, and then — the ship founders — it is salt, forever, at the bottom of the sea. +

+

+ What the old poem understands and what the new ones rarely do is that the tool and the hand are the same patient object. The mill is not separate from the women who turn it. The archive is not separate from the reader who returns, and returns, and annotates in the margin the third time through. +

+

+ This is the case I want to make for slowness. Not as a posture — the internet has enough postures — but as an epistemic stance. The things worth knowing are the things you come back to. +

+
+ + +
+
+ + ); +}; + +Object.assign(window, { ArchiveScreen, ReaderScreen }); diff --git a/design/uploads/Manrope-Bold.ttf b/design/uploads/Manrope-Bold.ttf new file mode 100644 index 0000000..62a6183 Binary files /dev/null and b/design/uploads/Manrope-Bold.ttf differ diff --git a/design/uploads/Manrope-ExtraBold.ttf b/design/uploads/Manrope-ExtraBold.ttf new file mode 100644 index 0000000..2fa671c Binary files /dev/null and b/design/uploads/Manrope-ExtraBold.ttf differ diff --git a/design/uploads/Manrope-ExtraLight.ttf b/design/uploads/Manrope-ExtraLight.ttf new file mode 100644 index 0000000..c55745a Binary files /dev/null and b/design/uploads/Manrope-ExtraLight.ttf differ diff --git a/design/uploads/Manrope-Light.ttf b/design/uploads/Manrope-Light.ttf new file mode 100644 index 0000000..8a771c2 Binary files /dev/null and b/design/uploads/Manrope-Light.ttf differ diff --git a/design/uploads/Manrope-Medium.ttf b/design/uploads/Manrope-Medium.ttf new file mode 100644 index 0000000..c6d28de Binary files /dev/null and b/design/uploads/Manrope-Medium.ttf differ diff --git a/design/uploads/Manrope-Regular.ttf b/design/uploads/Manrope-Regular.ttf new file mode 100644 index 0000000..9a108f1 Binary files /dev/null and b/design/uploads/Manrope-Regular.ttf differ diff --git a/design/uploads/Manrope-SemiBold.ttf b/design/uploads/Manrope-SemiBold.ttf new file mode 100644 index 0000000..46a13d6 Binary files /dev/null and b/design/uploads/Manrope-SemiBold.ttf differ diff --git a/design/uploads/Newsreader_36pt-Bold.ttf b/design/uploads/Newsreader_36pt-Bold.ttf new file mode 100644 index 0000000..6d9e20a Binary files /dev/null and b/design/uploads/Newsreader_36pt-Bold.ttf differ diff --git a/design/uploads/Newsreader_36pt-BoldItalic.ttf b/design/uploads/Newsreader_36pt-BoldItalic.ttf new file mode 100644 index 0000000..bc57925 Binary files /dev/null and b/design/uploads/Newsreader_36pt-BoldItalic.ttf differ diff --git a/design/uploads/Newsreader_36pt-ExtraBold.ttf b/design/uploads/Newsreader_36pt-ExtraBold.ttf new file mode 100644 index 0000000..69a726d Binary files /dev/null and b/design/uploads/Newsreader_36pt-ExtraBold.ttf differ diff --git a/design/uploads/Newsreader_36pt-Italic.ttf b/design/uploads/Newsreader_36pt-Italic.ttf new file mode 100644 index 0000000..477facd Binary files /dev/null and b/design/uploads/Newsreader_36pt-Italic.ttf differ diff --git a/design/uploads/Newsreader_36pt-Regular.ttf b/design/uploads/Newsreader_36pt-Regular.ttf new file mode 100644 index 0000000..9fe7694 Binary files /dev/null and b/design/uploads/Newsreader_36pt-Regular.ttf differ diff --git a/design/uploads/boulderer big 300 dpi(1).png b/design/uploads/boulderer big 300 dpi(1).png new file mode 100644 index 0000000..78272dd Binary files /dev/null and b/design/uploads/boulderer big 300 dpi(1).png differ diff --git a/design/uploads/fenja black(1)-1.svg b/design/uploads/fenja black(1)-1.svg new file mode 100644 index 0000000..8a1121c --- /dev/null +++ b/design/uploads/fenja black(1)-1.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/uploads/fenja black(1)-2.svg b/design/uploads/fenja black(1)-2.svg new file mode 100644 index 0000000..8a1121c --- /dev/null +++ b/design/uploads/fenja black(1)-2.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/uploads/fenja black(1)-3.svg b/design/uploads/fenja black(1)-3.svg new file mode 100644 index 0000000..8a1121c --- /dev/null +++ b/design/uploads/fenja black(1)-3.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/uploads/fenja black(1).svg b/design/uploads/fenja black(1).svg new file mode 100644 index 0000000..8a1121c --- /dev/null +++ b/design/uploads/fenja black(1).svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/uploads/fenja icon black-1.svg b/design/uploads/fenja icon black-1.svg new file mode 100644 index 0000000..b7546e9 --- /dev/null +++ b/design/uploads/fenja icon black-1.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/uploads/fenja icon black.svg b/design/uploads/fenja icon black.svg new file mode 100644 index 0000000..b7546e9 --- /dev/null +++ b/design/uploads/fenja icon black.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/uploads/fenja icon white-1.svg b/design/uploads/fenja icon white-1.svg new file mode 100644 index 0000000..a6ef123 --- /dev/null +++ b/design/uploads/fenja icon white-1.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/uploads/fenja icon white-2.svg b/design/uploads/fenja icon white-2.svg new file mode 100644 index 0000000..a6ef123 --- /dev/null +++ b/design/uploads/fenja icon white-2.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/uploads/fenja icon white.svg b/design/uploads/fenja icon white.svg new file mode 100644 index 0000000..a6ef123 --- /dev/null +++ b/design/uploads/fenja icon white.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/uploads/fenja white(1)-1.svg b/design/uploads/fenja white(1)-1.svg new file mode 100644 index 0000000..4841594 --- /dev/null +++ b/design/uploads/fenja white(1)-1.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/uploads/fenja white(1)-2.svg b/design/uploads/fenja white(1)-2.svg new file mode 100644 index 0000000..4841594 --- /dev/null +++ b/design/uploads/fenja white(1)-2.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/uploads/fenja white(1)-3.svg b/design/uploads/fenja white(1)-3.svg new file mode 100644 index 0000000..4841594 --- /dev/null +++ b/design/uploads/fenja white(1)-3.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/uploads/fenja white(1).svg b/design/uploads/fenja white(1).svg new file mode 100644 index 0000000..4841594 --- /dev/null +++ b/design/uploads/fenja white(1).svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/uploads/flowerman_white_transparent(1)-1.png b/design/uploads/flowerman_white_transparent(1)-1.png new file mode 100644 index 0000000..f633125 Binary files /dev/null and b/design/uploads/flowerman_white_transparent(1)-1.png differ diff --git a/design/uploads/flowerman_white_transparent(1)-2.png b/design/uploads/flowerman_white_transparent(1)-2.png new file mode 100644 index 0000000..f633125 Binary files /dev/null and b/design/uploads/flowerman_white_transparent(1)-2.png differ diff --git a/design/uploads/flowerman_white_transparent(1).png b/design/uploads/flowerman_white_transparent(1).png new file mode 100644 index 0000000..f633125 Binary files /dev/null and b/design/uploads/flowerman_white_transparent(1).png differ diff --git a/design/uploads/flowerman_yellow(1)-1.png b/design/uploads/flowerman_yellow(1)-1.png new file mode 100644 index 0000000..259dfc9 Binary files /dev/null and b/design/uploads/flowerman_yellow(1)-1.png differ diff --git a/design/uploads/flowerman_yellow(1)-2.png b/design/uploads/flowerman_yellow(1)-2.png new file mode 100644 index 0000000..259dfc9 Binary files /dev/null and b/design/uploads/flowerman_yellow(1)-2.png differ diff --git a/design/uploads/flowerman_yellow(1).png b/design/uploads/flowerman_yellow(1).png new file mode 100644 index 0000000..259dfc9 Binary files /dev/null and b/design/uploads/flowerman_yellow(1).png differ diff --git a/design/uploads/waves_concentraded(1).png b/design/uploads/waves_concentraded(1).png new file mode 100644 index 0000000..ce8bb8e Binary files /dev/null and b/design/uploads/waves_concentraded(1).png differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..ec8057d --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "project-bifrost", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "typecheck": "tsc --noEmit", + "lint": "eslint . && prettier --check .", + "test": "vitest run", + "db:migrate": "node scripts/migrate.js", + "db:seed": "node scripts/seed.js" + }, + "dependencies": { + "@astrojs/node": "^8.3.0", + "astro": "^4.16.0", + "bcryptjs": "^2.4.3", + "better-sqlite3": "^11.3.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/better-sqlite3": "^7.6.11", + "typescript": "^5.6.3", + "vitest": "^2.1.4" + }, + "engines": { + "node": ">=22" + } +} diff --git a/public/fonts/Manrope-Bold.ttf b/public/fonts/Manrope-Bold.ttf new file mode 100644 index 0000000..62a6183 Binary files /dev/null and b/public/fonts/Manrope-Bold.ttf differ diff --git a/public/fonts/Manrope-ExtraBold.ttf b/public/fonts/Manrope-ExtraBold.ttf new file mode 100644 index 0000000..2fa671c Binary files /dev/null and b/public/fonts/Manrope-ExtraBold.ttf differ diff --git a/public/fonts/Manrope-ExtraLight.ttf b/public/fonts/Manrope-ExtraLight.ttf new file mode 100644 index 0000000..c55745a Binary files /dev/null and b/public/fonts/Manrope-ExtraLight.ttf differ diff --git a/public/fonts/Manrope-Light.ttf b/public/fonts/Manrope-Light.ttf new file mode 100644 index 0000000..8a771c2 Binary files /dev/null and b/public/fonts/Manrope-Light.ttf differ diff --git a/public/fonts/Manrope-Medium.ttf b/public/fonts/Manrope-Medium.ttf new file mode 100644 index 0000000..c6d28de Binary files /dev/null and b/public/fonts/Manrope-Medium.ttf differ diff --git a/public/fonts/Manrope-Regular.ttf b/public/fonts/Manrope-Regular.ttf new file mode 100644 index 0000000..9a108f1 Binary files /dev/null and b/public/fonts/Manrope-Regular.ttf differ diff --git a/public/fonts/Manrope-SemiBold.ttf b/public/fonts/Manrope-SemiBold.ttf new file mode 100644 index 0000000..46a13d6 Binary files /dev/null and b/public/fonts/Manrope-SemiBold.ttf differ diff --git a/public/fonts/Newsreader-Bold.ttf b/public/fonts/Newsreader-Bold.ttf new file mode 100644 index 0000000..6d9e20a Binary files /dev/null and b/public/fonts/Newsreader-Bold.ttf differ diff --git a/public/fonts/Newsreader-BoldItalic.ttf b/public/fonts/Newsreader-BoldItalic.ttf new file mode 100644 index 0000000..bc57925 Binary files /dev/null and b/public/fonts/Newsreader-BoldItalic.ttf differ diff --git a/public/fonts/Newsreader-ExtraBold.ttf b/public/fonts/Newsreader-ExtraBold.ttf new file mode 100644 index 0000000..69a726d Binary files /dev/null and b/public/fonts/Newsreader-ExtraBold.ttf differ diff --git a/public/fonts/Newsreader-Italic.ttf b/public/fonts/Newsreader-Italic.ttf new file mode 100644 index 0000000..477facd Binary files /dev/null and b/public/fonts/Newsreader-Italic.ttf differ diff --git a/public/fonts/Newsreader-Regular.ttf b/public/fonts/Newsreader-Regular.ttf new file mode 100644 index 0000000..9fe7694 Binary files /dev/null and b/public/fonts/Newsreader-Regular.ttf differ diff --git a/public/logo-icon-white.svg b/public/logo-icon-white.svg new file mode 100644 index 0000000..97e9386 --- /dev/null +++ b/public/logo-icon-white.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/logo-icon.svg b/public/logo-icon.svg new file mode 100644 index 0000000..2ee63b2 --- /dev/null +++ b/public/logo-icon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/logo-white.svg b/public/logo-white.svg new file mode 100644 index 0000000..b049d06 --- /dev/null +++ b/public/logo-white.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..a39a16b --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..acef35f --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro new file mode 100644 index 0000000..cd99c29 --- /dev/null +++ b/src/layouts/BaseLayout.astro @@ -0,0 +1,24 @@ +--- +import '../styles/global.css'; + +interface Props { + title: string; + description?: string; +} + +const { title, description = 'Project Bifrost — Fenja AI pilot hub' } = Astro.props; +--- + + + + + + + + {title} — Project Bifrost + + + + + + diff --git a/src/pages/index.astro b/src/pages/index.astro new file mode 100644 index 0000000..aec3cdc --- /dev/null +++ b/src/pages/index.astro @@ -0,0 +1,12 @@ +--- +import BaseLayout from '../layouts/BaseLayout.astro'; +--- + +
+ Project Bifrost +

+ Welcome to the hub. +

+

Authentication and personalised content coming soon.

+
+
diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..8ac8696 --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,239 @@ +/* ============================================================= + Global styles — @font-face, resets, base semantic styles + ============================================================= */ + +/* --- Fonts (self-hosted, served from /public/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"); +} + +@import "./tokens.css"; + +/* --- Reset --- */ +*, *::before, *::after { + box-sizing: border-box; +} + +/* --- Base --- */ +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, 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, 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, all-caps) --- */ +.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; + margin: 0; +} +.label-md { font-size: var(--text-label-md); } +.label-sm { font-size: var(--text-label-sm); } + +/* --- Lead (serif italic, editorial aside) --- */ +.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); +} + +/* --- Mono --- */ +code, kbd, samp, pre, .mono { + font-family: var(--font-mono); + font-size: 0.92em; + color: var(--on-surface); +} + +/* --- Links (editorial, underline on 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); +} + +/* --- Ghost border utilities --- */ +.ghost-border { border: var(--ghost-border); } +.ghost-border-bottom { border-bottom: var(--ghost-border); } + +/* --- Focus ring --- */ +:focus-visible { + outline: 2px solid var(--secondary); + outline-offset: 3px; + opacity: 0.4; +} diff --git a/src/styles/tokens.css b/src/styles/tokens.css new file mode 100644 index 0000000..fe79423 --- /dev/null +++ b/src/styles/tokens.css @@ -0,0 +1,127 @@ +/* ============================================================= + Fenja AI — Design tokens + Derived from design/colors_and_type.css + ============================================================= */ + +:root { + /* --- Surface tiers (unbleached paper → clay) --- */ + --background: #faf6ee; + --surface: #faf6ee; + --surface-container-lowest: #fffcf7; + --surface-container-low: #f6f2e8; + --surface-container: #efeadc; + --surface-container-high: #e7e1d0; + --surface-container-highest: #ddd6c3; + --surface-variant: #ddd6c3; + + /* --- Text --- */ + --on-surface: #383831; + --on-surface-variant: #5f5e5e; + --on-surface-muted: #8a887f; + + /* --- Brand --- */ + --primary: #5f5e5e; + --on-primary: #fffcf7; + + --secondary: #785f53; + --secondary-dim: #6b5348; + --on-secondary: #ffffff; + --secondary-fixed-dim: #9a8679; + + /* --- Ghost border --- */ + --outline: #babab0; + --outline-variant: #babab0; + --ghost-border-color: rgba(186, 186, 176, 0.15); + --ghost-border: 1px solid var(--ghost-border-color); + + /* --- Archival Pigments (flat, matte inks) --- */ + --pigment-terracotta: #b96b58; + --pigment-copper: #6d8c7c; + --pigment-ochre: #c29d59; + --pigment-indigo: #5a6d83; + --pigment-heather: #8d7a85; + + /* --- 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 (responsive, clamped) --- */ + --text-display-xl: clamp(3.5rem, 6vw, 5.5rem); + --text-display-lg: clamp(3rem, 5vw, 4.5rem); + --text-display-md: clamp(2.5rem, 4vw, 3.5rem); + --text-headline-lg: 2.25rem; + --text-headline-md: 1.75rem; + --text-headline-sm: 1.375rem; + --text-title-lg: 1.125rem; + --text-title-md: 1rem; + --text-body-lg: 1.0625rem; + --text-body-md: 1rem; + --text-body-sm: 0.875rem; + --text-label-md: 0.8125rem; + --text-label-sm: 0.75rem; + + /* --- Tracking --- */ + --tracking-tight: -0.02em; + --tracking-snug: -0.01em; + --tracking-normal: 0; + --tracking-wide: 0.04em; + --tracking-wider: 0.08em; + + /* --- Leading --- */ + --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; /* 4px */ + --space-2: 0.5rem; /* 8px */ + --space-3: 0.75rem; /* 12px */ + --space-4: 1rem; /* 16px */ + --space-5: 1.5rem; /* 24px */ + --space-6: 2rem; /* 32px — list separator default */ + --space-7: 2.5rem; /* 40px */ + --space-8: 2.75rem; /* 44px — hero-card padding */ + --space-10: 4rem; /* 64px */ + --space-12: 5rem; /* 80px */ + --space-16: 6rem; /* 96px */ + --space-20: 7rem; /* 112px — desktop lateral margin */ + --space-24: 8rem; /* 128px */ + + /* --- Radii --- */ + --radius-none: 0; + --radius-sm: 0.375rem; /* 6px */ + --radius-md: 0.75rem; /* 12px — primary */ + --radius-lg: 1.25rem; /* 20px */ + --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); + + /* --- 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; /* 1152px */ + --reading-max: 42rem; /* 672px */ +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c5450d3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + } +}