diff --git a/README.md b/README.md
index daf1295..c2f90dc 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,106 @@
-# portfolio
+# louis-potevin.dev
+Professional portfolio of **Louis Potevin** — full-stack developer.
+Static multi-page website built with **Astro + TypeScript + Sass**, styled
+**exclusively** by the in-house design system **`@unkn0wndo3s/nova-design-system`**
+(tokens, components), featuring a 3D space background controlled via scrolling.
+
+---
+
+## Getting Started
+
+```bash
+npm install
+npm run dev # dev server → http://localhost:4321
+npm run build # static build → ./dist
+npm run preview # preview the build
+```
+
+> **Installation Note.** `@lucide/astro` has not yet declared its
+compatibility with Astro 7 in its `peerDependencies`. The **`.npmrc`**
+> file therefore enables `legacy-peer-deps=true` so that `npm install` succeeds.
+> Functionality is not affected.
+
+---
+
+## Edit Before Going Live
+
+All modifiable content is centralized. Values to be confirmed are
+marked with `TODO` in the code :
+
+- **`src/data/site.ts`** — name, role, email, external links (Gitea, GitHub,
+npm, LinkedIn). In particular, verify :
+- **`src/data/projects.ts`** — la liste des projets (les « stations »).
+ Chaque entrée : résumé, contexte, contributions, stack, liens.
+
+---
+
+## Structure
+
+```
+src/
+├─ data/
+│ ├─ site.ts # profile + links (single source of truth)
+│ └─ projects.ts # project content
+├─ layouts/
+│ └─ BaseLayout.astro # NDS styles + 3D background + nav + footer + HUD
+├─ components/
+│ ├─ scene/scene.ts # Three.js background (stars, nebula, ambient asteroid)
+│ ├─ SpaceBackground.astro
+│ ├─ ProjectCard.astro # project card (NDS Card component)
+│ ├─ SiteNav / SiteFooter / Eyebrow / TechIcon / BrandLinkedin
+└─ pages/
+ ├─ index.astro # home
+ ├─ work/index.astro # all projects
+ ├─ work/[slug].astro # project details (getStaticPaths)
+ ├─ about.astro
+ └─ contact.astro
+```
+
+## Design system
+
+Components come from `@unkn0wndo3s/nova-design-system` :
+`import { Button, Card, Badge, Navbar, Breadcrumb, TextField, Select, ... }`.
+Global styles and tokens : `import '@unkn0wndo3s/nova-design-system/styles'`
+(done once in `BaseLayout.astro`).
+
+The package ships its raw `.astro`/`.scss` source files - Astro compiles them
+on the consumer side. Since its `exports` field does not publish token partials
+individually, the typography partial is exposed via a Sass
+`loadPath` (see `astro.config.mjs`) and then re-exported by
+`src/styles/_type.scss` (mixins `text-sm` … `text-5xl`).
+
+Icons : `@lucide/astro` (UI) and `simple-icons-astro` (tech). Since LinkedIn is
+no longer distributed by either of them (trademark policy), its glyph is provided
+locally in `BrandLinkedin.astro`.
+
+## 3D Background
+
+An ambient asteroid slowly rotates and floats in the background, in the middle of a
+starfield and a discrete nebula (Three.js). It does not follow scroll actions
+and produces no engine flame effects: it is a backdrop, not the subject.
+
+- **Without WebGL** : the canvas hides cleanly, the site remains readable.
+
+## SEO
+
+Meticulous search engine optimization, pre-configured:
+
+- Per-page metadata (title, description, keywords), canonical URLs;
+- Open Graph + Twitter Card with `public/og.png` (1200×630) ;
+- Structured data JSON-LD (`Person` + `WebSite`) ;
+- `sitemap-index.xml` (via `@astrojs/sitemap`) + `public/robots.txt`.
+
+Remember to update `og.png` if you change your job title, and confirm the
+links in `site.ts` (used in the `sameAs` property of the structured data).
+
+## Contact Form
+
+The site is 100% static: the form in `contact.astro` does not have a
+backend. The **Send** button opens the user's mail client with a
+pre-filled message (`mailto:`). For a server-side submission, connect a service
+(Formspree, Resend, edge function…) to the `#cf-send` button.
+
+---
+
+Built with Astro + Nova Design System.
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..b23b416
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,12 @@
+
diff --git a/public/og.png b/public/og.png
new file mode 100644
index 0000000..12f7896
Binary files /dev/null and b/public/og.png differ
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..fb21f4b
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://louis-potevin.dev/sitemap-index.xml
diff --git a/src/components/BrandLinkedin.astro b/src/components/BrandLinkedin.astro
new file mode 100644
index 0000000..af66c75
--- /dev/null
+++ b/src/components/BrandLinkedin.astro
@@ -0,0 +1,23 @@
+---
+// LinkedIn is no longer distributed by @lucide/astro or simple-icons-astro
+// (trademark policy). Official glyph reproduced inline, monochrome
+// via `currentColor` to remain consistent with the other icons.
+export interface Props {
+ size?: number;
+}
+const { size = 18 } = Astro.props;
+---
+
+
\ No newline at end of file
diff --git a/src/components/Eyebrow.astro b/src/components/Eyebrow.astro
new file mode 100644
index 0000000..a0d5db1
--- /dev/null
+++ b/src/components/Eyebrow.astro
@@ -0,0 +1,39 @@
+---
+export interface Props {
+ index?: string; // ex. "01"
+ label: string;
+}
+const { index, label } = Astro.props;
+---
+
+
+ {index && {index}}
+
+ {label}
+
+
+
diff --git a/src/components/ProjectCard.astro b/src/components/ProjectCard.astro
new file mode 100644
index 0000000..d49981f
--- /dev/null
+++ b/src/components/ProjectCard.astro
@@ -0,0 +1,104 @@
+---
+import { Card, Badge, Link } from '@unkn0wndo3s/nova-design-system';
+import { ArrowUpRight } from '@lucide/astro';
+import TechIcon from './TechIcon.astro';
+import { statusMeta, type Project } from '../data/projects';
+
+export interface Props {
+ project: Project;
+}
+const { project } = Astro.props;
+const status = statusMeta[project.status];
+const href = `/work/${project.slug}`;
+---
+
+
+
+
+
+
diff --git a/src/components/SpaceBackground.astro b/src/components/SpaceBackground.astro
new file mode 100644
index 0000000..eb9a2b3
--- /dev/null
+++ b/src/components/SpaceBackground.astro
@@ -0,0 +1,51 @@
+---
+// Persistent space background. Placed in the layout → loaded on all pages.
+// Content remains 100% readable and functional even if WebGL fails (decorative).
+---
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/TechIcon.astro b/src/components/TechIcon.astro
new file mode 100644
index 0000000..b63af57
--- /dev/null
+++ b/src/components/TechIcon.astro
@@ -0,0 +1,52 @@
+---
+// Monochrome tech icon (inherits `color` via fill=currentColor).
+import {
+ Typescript,
+ Javascript,
+ Postgresql,
+ Docker,
+ Astro as AstroIcon,
+ Sass,
+ Gitea,
+ Linux,
+ Cloudflare,
+ Threedotjs,
+ Rust,
+ Python,
+ Npm,
+ Github,
+ Svelte,
+ Git,
+ Nodedotjs,
+} from 'simple-icons-astro';
+
+export interface Props {
+ name: string;
+ size?: number;
+}
+const { name, size = 18 } = Astro.props;
+
+const map: Record = {
+ typescript: Typescript,
+ javascript: Javascript,
+ postgresql: Postgresql,
+ docker: Docker,
+ astro: AstroIcon,
+ sass: Sass,
+ gitea: Gitea,
+ linux: Linux,
+ cloudflare: Cloudflare,
+ threedotjs: Threedotjs,
+ rust: Rust,
+ python: Python,
+ npm: Npm,
+ github: Github,
+ svelte: Svelte,
+ git: Git,
+ nodedotjs: Nodedotjs,
+};
+
+const Icon = map[name];
+---
+
+{Icon && }
\ No newline at end of file
diff --git a/src/components/scene/scene.ts b/src/components/scene/scene.ts
new file mode 100644
index 0000000..5e16c95
--- /dev/null
+++ b/src/components/scene/scene.ts
@@ -0,0 +1,250 @@
+import * as THREE from 'three';
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Ambient space background: starfield, subtle nebula, and a slow-drifting,
+// slow-rotating asteroid. No scroll binding, no engine flames.
+// Slight pointer parallax. Respects prefers-reduced-motion.
+// ─────────────────────────────────────────────────────────────────────────────
+
+export interface SceneHandle {
+ /** Kept for compatibility — no-op (no more impact/flame effect). */
+ setImpact(value: number): void;
+ destroy(): void;
+}
+
+const COLORS = {
+ star: 0xecf5f5,
+ starCool: 0x34c2c9,
+ starAccent: 0x2efafa,
+ nebulaA: 0x288181,
+ nebulaB: 0x1fa6ad,
+ rock: 0x1a2a2a,
+ rim: 0x34c2c9,
+};
+
+const prefersReduced =
+ typeof window !== 'undefined' &&
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+
+/** Small reusable radial texture (soft glow). */
+function radialTexture(inner: string, outer = 'rgba(0,0,0,0)'): THREE.Texture {
+ const c = document.createElement('canvas');
+ c.width = c.height = 128;
+ const ctx = c.getContext('2d')!;
+ const g = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
+ g.addColorStop(0, inner);
+ g.addColorStop(0.4, inner);
+ g.addColorStop(1, outer);
+ ctx.fillStyle = g;
+ ctx.fillRect(0, 0, 128, 128);
+ const tex = new THREE.CanvasTexture(c);
+ tex.colorSpace = THREE.SRGBColorSpace;
+ return tex;
+}
+
+export function initScene(canvas: HTMLCanvasElement): SceneHandle {
+ const renderer = new THREE.WebGLRenderer({
+ canvas,
+ antialias: true,
+ alpha: true,
+ powerPreference: 'high-performance',
+ });
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.75));
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.setClearColor(0x000000, 0);
+
+ const scene = new THREE.Scene();
+ scene.fog = new THREE.FogExp2(0x040b0b, 0.012);
+
+ const camera = new THREE.PerspectiveCamera(
+ 55,
+ window.innerWidth / window.innerHeight,
+ 0.1,
+ 400,
+ );
+ camera.position.set(0, 0, 18);
+
+ // ── Lighting (cold, consistent with Nova tokens) ───────────────────────────
+ const ambient = new THREE.AmbientLight(0x16323a, 1.0);
+ scene.add(ambient);
+ const key = new THREE.DirectionalLight(COLORS.starCool, 0.9);
+ key.position.set(-6, 5, 9);
+ scene.add(key);
+ const rimLight = new THREE.DirectionalLight(COLORS.starAccent, 0.4);
+ rimLight.position.set(8, -3, 4);
+ scene.add(rimLight);
+
+ // ── Starfield ──────────────────────────────────────────────────────────────
+ const STAR_COUNT = prefersReduced ? 700 : 1700;
+ const starGeo = new THREE.BufferGeometry();
+ const starPos = new Float32Array(STAR_COUNT * 3);
+ const starCol = new Float32Array(STAR_COUNT * 3);
+ const palette = [
+ new THREE.Color(COLORS.star),
+ new THREE.Color(COLORS.starCool),
+ new THREE.Color(COLORS.starAccent),
+ ];
+ for (let i = 0; i < STAR_COUNT; i++) {
+ starPos[i * 3] = (Math.random() - 0.5) * 120;
+ starPos[i * 3 + 1] = (Math.random() - 0.5) * 120;
+ starPos[i * 3 + 2] = (Math.random() - 0.5) * 120 - 20;
+ const col = palette[Math.random() < 0.78 ? 0 : Math.random() < 0.6 ? 1 : 2];
+ starCol[i * 3] = col.r;
+ starCol[i * 3 + 1] = col.g;
+ starCol[i * 3 + 2] = col.b;
+ }
+ starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
+ starGeo.setAttribute('color', new THREE.BufferAttribute(starCol, 3));
+ const starMat = new THREE.PointsMaterial({
+ size: 0.42,
+ map: radialTexture('rgba(255,255,255,0.95)'),
+ transparent: true,
+ depthWrite: false,
+ vertexColors: true,
+ blending: THREE.AdditiveBlending,
+ });
+ const stars = new THREE.Points(starGeo, starMat);
+ scene.add(stars);
+
+ // ── Nebula (soft sprites, pushed far into the background) ──────────────────
+ const nebula = new THREE.Group();
+ const nebTex = radialTexture('rgba(40,129,129,0.55)');
+ [
+ { x: -14, y: 8, z: -45, s: 60, c: COLORS.nebulaA },
+ { x: 16, y: -10, z: -55, s: 72, c: COLORS.nebulaB },
+ { x: 4, y: 14, z: -40, s: 46, c: COLORS.nebulaA },
+ ].forEach((n) => {
+ const mat = new THREE.SpriteMaterial({
+ map: nebTex,
+ color: n.c,
+ transparent: true,
+ opacity: 0.2,
+ blending: THREE.AdditiveBlending,
+ depthWrite: false,
+ });
+ const sp = new THREE.Sprite(mat);
+ sp.position.set(n.x, n.y, n.z);
+ sp.scale.set(n.s, n.s, 1);
+ nebula.add(sp);
+ });
+ scene.add(nebula);
+
+ // ── Asteroid (cold rock, subtle halo) ──────────────────────────────────────
+ const asteroid = new THREE.Group();
+ const rockGeo = new THREE.IcosahedronGeometry(1.9, 2);
+ const pos = rockGeo.attributes.position as THREE.BufferAttribute;
+ const v = new THREE.Vector3();
+ for (let i = 0; i < pos.count; i++) {
+ v.fromBufferAttribute(pos, i);
+ const h = Math.sin(v.x * 4.1) * Math.cos(v.y * 3.7) * Math.sin(v.z * 4.3);
+ const d = 1 + h * 0.16;
+ v.multiplyScalar(d);
+ pos.setXYZ(i, v.x, v.y, v.z);
+ }
+ rockGeo.computeVertexNormals();
+ const rockMat = new THREE.MeshStandardMaterial({
+ color: COLORS.rock,
+ emissive: COLORS.rim,
+ emissiveIntensity: 0.12,
+ roughness: 0.92,
+ metalness: 0.12,
+ flatShading: true,
+ });
+ const rock = new THREE.Mesh(rockGeo, rockMat);
+ asteroid.add(rock);
+
+ // very light cold halo, just to separate the rock from the background
+ const glow = new THREE.Sprite(
+ new THREE.SpriteMaterial({
+ map: radialTexture('rgba(52,194,201,0.7)'),
+ color: COLORS.rim,
+ transparent: true,
+ opacity: 0.28,
+ blending: THREE.AdditiveBlending,
+ depthWrite: false,
+ }),
+ );
+ glow.scale.set(7.5, 7.5, 1);
+ asteroid.add(glow);
+
+ // ambient position: top right, set back
+ asteroid.position.set(6.5, 4.5, -2);
+ scene.add(asteroid);
+
+ // ── State & loop ───────────────────────────────────────────────────────────
+ const pointer = { x: 0, y: 0, tx: 0, ty: 0 };
+ let running = true;
+
+ function onPointer(e: PointerEvent) {
+ pointer.tx = (e.clientX / window.innerWidth - 0.5) * 2;
+ pointer.ty = (e.clientY / window.innerHeight - 0.5) * 2;
+ }
+
+ function onResize() {
+ camera.aspect = window.innerWidth / window.innerHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ }
+
+ function onVisibility() {
+ running = document.visibilityState === 'visible';
+ if (running) loop();
+ }
+
+ window.addEventListener('resize', onResize);
+ window.addEventListener('pointermove', onPointer, { passive: true });
+ document.addEventListener('visibilitychange', onVisibility);
+
+ const clock = new THREE.Clock();
+ let rafId = 0;
+
+ function loop() {
+ if (!running) return;
+ rafId = requestAnimationFrame(loop);
+ const dt = Math.min(clock.getDelta(), 0.05);
+ const t = clock.elapsedTime;
+
+ // slow background drift
+ stars.rotation.y += dt * 0.01;
+ nebula.rotation.z += dt * 0.004;
+
+ // asteroid: self-rotation + slight float (no scroll binding)
+ if (!prefersReduced) {
+ rock.rotation.x += dt * 0.18;
+ rock.rotation.y += dt * 0.26;
+ asteroid.position.y = 4.5 + Math.sin(t * 0.25) * 0.5;
+ asteroid.position.x = 6.5 + Math.cos(t * 0.18) * 0.4;
+ }
+
+ // smooth camera parallax via pointer
+ pointer.x += (pointer.tx - pointer.x) * dt * 2;
+ pointer.y += (pointer.ty - pointer.y) * dt * 2;
+ camera.position.x = pointer.x * 1.2;
+ camera.position.y = -pointer.y * 0.8 + Math.sin(t * 0.3) * 0.15;
+ camera.lookAt(0, 0, 0);
+
+ renderer.render(scene, camera);
+ }
+ loop();
+
+ return {
+ setImpact() {
+ /* no-op: kept for API compatibility */
+ },
+ destroy() {
+ running = false;
+ cancelAnimationFrame(rafId);
+ window.removeEventListener('resize', onResize);
+ window.removeEventListener('pointermove', onPointer);
+ document.removeEventListener('visibilitychange', onVisibility);
+ renderer.dispose();
+ starGeo.dispose();
+ rockGeo.dispose();
+ scene.traverse((o) => {
+ const m = (o as THREE.Mesh).material;
+ if (Array.isArray(m)) m.forEach((mm) => mm.dispose());
+ else if (m) (m as THREE.Material).dispose();
+ });
+ },
+ };
+}
\ No newline at end of file
diff --git a/src/data/projects.ts b/src/data/projects.ts
new file mode 100644
index 0000000..f370495
--- /dev/null
+++ b/src/data/projects.ts
@@ -0,0 +1,179 @@
+// ─────────────────────────────────────────────────────────────────────────────
+// Projects — real content. Edit freely.
+// `icon` matches a simple-icons-astro component (using PascalCase elsewhere).
+// ─────────────────────────────────────────────────────────────────────────────
+
+export type ProjectStatus = 'live' | 'maintenu' | 'archive' | 'r&d';
+
+export interface ProjectLink {
+ label: string;
+ url: string;
+ icon?: string;
+}
+
+export interface Project {
+ slug: string;
+ designation: string; // catalog index, e.g. "01"
+ title: string;
+ tagline: string;
+ period: string;
+ role: string;
+ status: ProjectStatus;
+ featured: boolean;
+ summary: string;
+ context: string[];
+ contributions: string[];
+ stack: { icon: string; label: string }[];
+ links: ProjectLink[];
+}
+
+export const projects: Project[] = [
+ {
+ slug: 'nova-design-system',
+ designation: '01',
+ title: 'Nova Design System',
+ tagline: 'Astro component library — tokens, accessibility, npm distribution.',
+ period: '2026 — ongoing',
+ role: 'Author · maintainer',
+ status: 'maintenu',
+ featured: true,
+ summary:
+ 'The design system that powers this website and will power my other projects. 29 Astro components across six families, a complete set of tokens (color, typography, spacing, radius), and a light/dark theme managed via CSS variables.',
+ context: [
+ 'Designed for clean, technical interfaces: mono fonts for headings, sans-serif for body text.',
+ 'Distributed under the npm scope @unkn0wndo3s, automatically published via a CI pipeline.',
+ 'License: free to use, resale of the design system as a standalone product is prohibited.',
+ ],
+ contributions: [
+ 'Architecture of tokens and components (Custom Elements, typed events, slots).',
+ 'Gitea Actions CI/CD pipeline for automated npm publishing and documentation deployment.',
+ ],
+ stack: [
+ { icon: 'astro', label: 'Astro' },
+ { icon: 'typescript', label: 'TypeScript' },
+ { icon: 'sass', label: 'Sass' },
+ { icon: 'gitea', label: 'Gitea CI' },
+ ],
+ links: [
+ { label: 'Nova Design System v1.2.1', url: 'https://nds.louis-potevin.dev/', icon: 'astro' },
+ { label: 'npm', url: 'https://www.npmjs.com/package/@unkn0wndo3s/nova-design-system', icon: 'npm' },
+ { label: 'Sources (Gitea)', url: 'https://git.novaprojects.dev/unkn0wn/nova-design-system', icon: 'gitea' },
+ ],
+ },
+ {
+ slug: 'nova-infra',
+ designation: '02',
+ title: 'Self-hosted Infrastructure & Deployment',
+ tagline: 'Containerized services, CI/CD, and secured ingress on a fully controlled infrastructure.',
+ period: '2025 — ongoing',
+ role: 'Design & operations',
+ status: 'live',
+ featured: true,
+ summary:
+ 'A comprehensive self-hosted infrastructure: Git hosting, secret management, and application services, all containerized with Docker and continuously deployed.',
+ context: [
+ 'Fully containerized with Docker, orchestrated, and reproducible.',
+ 'Certificates and DNS managed through Cloudflare.',
+ 'Services cleanly exposed using a reverse proxy.',
+ ],
+ contributions: [
+ 'Setup of a self-hosted Gitea instance with a Postgres backend and action runners.',
+ 'End-to-end automated CI/CD pipelines (build, test, publish).',
+ 'Day-to-day operations: Docker networks, volume mounts, DNS, and diagnostics.',
+ ],
+ stack: [
+ { icon: 'docker', label: 'Docker' },
+ { icon: 'linux', label: 'Linux' },
+ { icon: 'cloudflare', label: 'Cloudflare Tunnel' },
+ { icon: 'gitea', label: 'Gitea' },
+ ],
+ links: [{ label: 'Gitea', url: 'https://git.novaprojects.dev/unkn0wn', icon: 'gitea' }],
+ },
+ {
+ slug: 'portfolio',
+ designation: '03',
+ title: 'louis-potevin.dev',
+ tagline: 'This website. Multi-page Astro app, 3D space background, 100% powered by Nova Design System.',
+ period: '2026',
+ role: 'Design & development',
+ status: 'live',
+ featured: true,
+ summary:
+ 'A static multi-page portfolio website. The content is entirely built using Nova components, featuring a subtle, discrete Three.js space background.',
+ context: [
+ 'Three.js space background: starfield, nebula, and an ambient asteroid.',
+ 'UI entirely composed using the in-house design system.',
+ 'Astro multi-page setup with Static Site Generation (SSG), optimized for SEO.',
+ ],
+ contributions: [
+ 'Full integration using Nova tokens and components.',
+ 'Adherence to prefers-reduced-motion and graceful degradation when WebGL is unavailable.',
+ 'Meticulous SEO: metadata, structured data, and sitemap generation.',
+ ],
+ stack: [
+ { icon: 'astro', label: 'Astro' },
+ { icon: 'typescript', label: 'TypeScript' },
+ { icon: 'sass', label: 'Sass' },
+ ],
+ links: [{ label: 'Unkn0wn Projects', url: 'https://louis-potevin.dev/', icon: 'astro' }],
+ },
+ {
+ slug: 'rust-file-organizer',
+ designation: '04',
+ title: 'File Organizer — Java to Rust',
+ tagline: 'File sorting tool, currently being ported from Java to Rust.',
+ period: '2025 — ongoing',
+ role: 'Development',
+ status: 'r&d',
+ featured: false,
+ summary:
+ 'A file organizer tool that currently runs in Java on Windows. It is being converted to Rust to achieve cross-platform Linux compatibility and optimize file search performance.',
+ context: [
+ 'Current version: operational in Java, targeting Windows.',
+ 'Gradual porting to Rust for performance gains and low-level control.',
+ ],
+ contributions: [
+ 'Rewriting sorting logic from Java to Rust.',
+ 'Setting objectives for Linux compatibility and file search optimization.',
+ ],
+ stack: [
+ { icon: 'rust', label: 'Rust' },
+ { icon: 'linux', label: 'Linux (upcoming)' },
+ ],
+ links: [],
+ },
+ {
+ slug: 'llm-tooling',
+ designation: '05',
+ title: 'LLM Training & Inference',
+ tagline: 'Small language model trained on a Reddit conversation dataset.',
+ period: '2025 — ongoing',
+ role: 'Development',
+ status: 'r&d',
+ featured: false,
+ summary:
+ 'A language model training and inference project running on a dedicated machine. The model is trained on a custom dataset of Reddit conversations. Development is ongoing.',
+ context: [
+ 'Execution on a dedicated workstation optimized for training and inference.',
+ 'Dataset: conversation threads sourced from Reddit.',
+ ],
+ contributions: [
+ 'Assembling and preprocessing the conversation dataset.',
+ 'Setting up the training and inference loop, currently iterating on results.',
+ ],
+ stack: [
+ { icon: 'python', label: 'Python' },
+ { icon: 'linux', label: 'Linux' },
+ ],
+ links: [],
+ },
+];
+
+export const featuredProjects = projects.filter((p) => p.featured);
+
+export const statusMeta: Record = {
+ live: { label: 'Live', tone: 'success' },
+ maintenu: { label: 'Maintained', tone: 'primary' },
+ 'r&d': { label: 'In Progress', tone: 'warning' },
+ archive: { label: 'Archived', tone: 'neutral' },
+};
\ No newline at end of file
diff --git a/src/data/site.ts b/src/data/site.ts
new file mode 100644
index 0000000..2dd522a
--- /dev/null
+++ b/src/data/site.ts
@@ -0,0 +1,37 @@
+export const site = {
+ name: "Louis Potevin",
+ handle: "unkn0wn",
+ role: "Full-stack developer",
+ domain: "louis-potevin.dev",
+ location: "Limoges · Nouvelle-Aquitaine",
+ // Job Search
+ availability: "Permanent contract (CDI) — September 2026",
+ mobility: "Anywhere in France, on-site or remote",
+ // Contact
+ email: "contact@louis-potevin.dev",
+ // External links (icons via simple-icons-astro)
+ links: {
+ gitea: {
+ label: "Gitea",
+ url: "https://git.novaprojects.dev/unkn0wn",
+ icon: "gitea",
+ },
+ github: {
+ label: "GitHub",
+ url: "https://github.com/unkn0wndo3s",
+ icon: "github",
+ },
+ npm: {
+ label: "npm",
+ url: "https://www.npmjs.com/~unkn0wndo3s",
+ icon: "npm",
+ },
+ linkedin: {
+ label: "LinkedIn",
+ url: "https://www.linkedin.com/in/louis-potevin",
+ icon: "linkedin",
+ },
+ },
+} as const;
+
+export type SiteLink = { label: string; url: string; icon: string };
diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro
new file mode 100644
index 0000000..f265fde
--- /dev/null
+++ b/src/layouts/BaseLayout.astro
@@ -0,0 +1,169 @@
+---
+import '@unkn0wndo3s/nova-design-system/styles';
+import '../styles/global.scss';
+
+import SpaceBackground from '../components/SpaceBackground.astro';
+import SiteNav from '../components/SiteNav.astro';
+import SiteFooter from '../components/SiteFooter.astro';
+import { site } from '../data/site';
+
+export interface Props {
+ title?: string;
+ description?: string;
+ /** Page-specific keywords (appended to the global keywords list). */
+ keywords?: string[];
+}
+
+const {
+ title = 'Louis Potevin — Full-stack Developer (front, back, web)',
+ description = "Louis Potevin, full-stack web developer: front-end, back-end, TypeScript, Astro, Node. Available for a permanent contract (CDI) starting September 2026, anywhere in France or remote.",
+ keywords = [],
+} = Astro.props;
+
+const baseKeywords = [
+ // French Keywords
+ 'Louis Potevin',
+ 'développeur web',
+ 'developpeur web',
+ 'dev web',
+ 'développeur full-stack',
+ 'developpeur full stack',
+ 'dev front',
+ 'développeur front-end',
+ 'dev back',
+ 'développeur back-end',
+ 'développeur TypeScript',
+ 'développeur Astro',
+ 'développeur JavaScript',
+ 'développeur Node.js',
+ 'développeur freelance France',
+ 'portfolio développeur',
+ 'web developer',
+ 'full-stack developer',
+ 'full stack developer',
+ 'front-end developer',
+ 'front end dev',
+ 'back-end developer',
+ 'back end dev',
+ 'TypeScript developer',
+ 'Astro developer',
+ 'JavaScript developer',
+ 'Node.js developer',
+ 'software engineer France',
+ 'developer portfolio',
+];
+const allKeywords = [...baseKeywords, ...keywords].join(', ');
+
+const canonical = new URL(Astro.url.pathname, Astro.site ?? 'https://louis-potevin.dev').href;
+const ogImage = new URL('/og.png', Astro.site ?? 'https://louis-potevin.dev').href;
+
+// Structured data: Person profile (helps Google link your name ↔ profession ↔ social links).
+const personLd = {
+ '@context': 'https://schema.org',
+ '@type': 'Person',
+ name: site.name,
+ jobTitle: 'Full-stack Developer / Développeur full-stack',
+ description,
+ url: canonical,
+ email: `mailto:${site.email}`,
+ address: { '@type': 'PostalAddress', addressCountry: 'FR', addressLocality: 'Limoges' },
+ knowsAbout: [
+ 'Web development',
+ 'Développement web',
+ 'Front-end',
+ 'Back-end',
+ 'TypeScript',
+ 'JavaScript',
+ 'Astro',
+ 'Node.js',
+ 'Sass',
+ 'Docker',
+ 'CI/CD',
+ ],
+ sameAs: [
+ site.links.github.url,
+ site.links.gitea.url,
+ site.links.npm.url,
+ site.links.linkedin.url,
+ ],
+};
+
+const siteLd = {
+ '@context': 'https://schema.org',
+ '@type': 'WebSite',
+ name: `${site.name} — Portfolio`,
+ url: Astro.site?.href ?? 'https://louis-potevin.dev',
+ inLanguage: ['en-US', 'fr-FR'],
+};
+---
+
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Skip to content
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pages/about.astro b/src/pages/about.astro
new file mode 100644
index 0000000..834d189
--- /dev/null
+++ b/src/pages/about.astro
@@ -0,0 +1,315 @@
+---
+import BaseLayout from '../layouts/BaseLayout.astro';
+import Eyebrow from '../components/Eyebrow.astro';
+import TechIcon from '../components/TechIcon.astro';
+import BrandLinkedin from '../components/BrandLinkedin.astro';
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ Badge,
+ Button,
+ Link,
+ ListItem,
+ ListItemTitle,
+ ListItemSubtitle,
+} from '@unkn0wndo3s/nova-design-system';
+import { Send, Mail } from '@lucide/astro';
+import { site } from '../data/site';
+
+const title = 'About — Louis Potevin, full-stack web developer / développeur full-stack';
+const description =
+ "Louis Potevin, full-stack web developer with a BUT MMI degree (DWDI track). 18 months of professional experience at Legrand France. Available for a permanent contract (CDI), anywhere in France or remote.";
+
+const parcours = [
+ {
+ year: '2024 — 2026',
+ title: 'Professional Experience · Legrand France',
+ body: "18 months total: 12 weeks of internship, 1 month on a fixed-term contract (CDD), followed by 1 year of work-study apprenticeship. Web development on a production-ready internal corporate application.",
+ },
+ {
+ year: '2024 — 2026',
+ title: 'BUT MMI — DWDI Track · IUT du Limousin',
+ body: "Multimedia and Internet Job Degrees (Métiers du Multimédia et de l'Internet), specializing in Web Development and Interactive Devices (DWDI). Degree completed in 2026.",
+ },
+ {
+ year: '2026 — ongoing',
+ title: 'Personal Projects',
+ body: "Designing an open-source design system published on npm, managing a self-hosted server infrastructure, and building system tools with Rust and Python.",
+ },
+];
+
+const stack = [
+ { icon: 'astro', label: 'Astro' },
+ { icon: 'typescript', label: 'TypeScript' },
+ { icon: 'javascript', label: 'JavaScript' },
+ { icon: 'sass', label: 'Sass' },
+ { icon: 'postgresql', label: 'PostgreSQL' },
+ { icon: 'docker', label: 'Docker' },
+ { icon: 'linux', label: 'Linux' },
+ { icon: 'cloudflare', label: 'Cloudflare' },
+ { icon: 'gitea', label: 'Gitea' },
+ { icon: 'rust', label: 'Rust' },
+ { icon: 'python', label: 'Python' },
+ { icon: 'git', label: 'Git' },
+];
+---
+
+
+
+
+ Home
+ About
+
+
+
About me
+
+ Full-stack developer based in {site.location}. I design complete web applications,
+ from front-end interfaces to back-end architecture, and manage their deployment into production.
+
+
+
+
+
+
+
+
+ I graduated with a BUT MMI degree, specializing in Web Development and Interactive
+ Devices (DWDI) at the IUT du Limousin. I solidified my practical engineering skills
+ in the industry: spending 18 months at Legrand France split between a 12-week internship,
+ a 1-month contract, and a full year as a software apprentice, working on a business-critical
+ production application.
+
+
+ Joining Legrand as an apprentice without knowing anyone prior, I quickly integrated
+ into the team and built strong working relationships across departments. I am equally
+ comfortable collaborating in agile team structures as I am working independently, and I excel
+ at adapting swiftly to new technical environments.
+
+
+ Outside of my professional employment, I build and maintain my own digital infrastructure:
+ a modular design system published on npm, a private self-hosted suite of services, and custom
+ automation tools written in Rust and Python. This is my preferred way of keeping up with industry
+ standards and thoroughly exploring engineering topics end-to-end.
+
+
+
+
+
+
+ {
+ parcours.map((p) => (
+
+ {p.year}
+
+
+ {p.title}
+ {p.body}
+
+
+
+ ))
+ }
+
+
+
+
+
+
+ {
+ stack.map((s) => (
+
+
+ {s.label}
+
+ ))
+ }
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pages/contact.astro b/src/pages/contact.astro
new file mode 100644
index 0000000..f585c08
--- /dev/null
+++ b/src/pages/contact.astro
@@ -0,0 +1,297 @@
+---
+import BaseLayout from '../layouts/BaseLayout.astro';
+import Eyebrow from '../components/Eyebrow.astro';
+import TechIcon from '../components/TechIcon.astro';
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ TextField,
+ Select,
+ SelectOption,
+ Button,
+ Badge,
+} from '@unkn0wndo3s/nova-design-system';
+import { Send, Mail } from '@lucide/astro';
+import BrandLinkedin from '../components/BrandLinkedin.astro';
+import { site } from '../data/site';
+
+const title = 'Contact — Louis Potevin';
+const description =
+ 'Contact Louis Potevin, full-stack web developer. Available for permanent positions (CDI) starting September 2026, anywhere in France or remote.';
+
+const directLinks = [
+ { kind: 'mail', label: site.email, url: `mailto:${site.email}` },
+ { kind: 'linkedin', label: 'LinkedIn', url: site.links.linkedin.url },
+ { kind: 'gitea', label: 'Gitea', url: site.links.gitea.url },
+ { kind: 'github', label: 'GitHub', url: site.links.github.url },
+];
+---
+
+
+
+
+ Home
+ Contact
+
+
+
Get in touch
+
+ An opening, a project, or just a question? Email is the most direct
+ channel, but the form works just as well.
+
+
+ Available
+ {site.availability} · {site.mobility}
+
+
+
+
+ {/* ── Form (Nova components) ── */}
+
+
Message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Opens your mail client with a pre-filled draft.
+
+
+
+
+
+ {/* ── Direct Links ── */}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pages/index.astro b/src/pages/index.astro
new file mode 100644
index 0000000..5d5ae3f
--- /dev/null
+++ b/src/pages/index.astro
@@ -0,0 +1,418 @@
+---
+import { Button, Badge } from '@unkn0wndo3s/nova-design-system';
+import { ArrowUpRight, Send } from '@lucide/astro';
+
+import BaseLayout from '../layouts/BaseLayout.astro';
+import Eyebrow from '../components/Eyebrow.astro';
+import ProjectCard from '../components/ProjectCard.astro';
+import TechIcon from '../components/TechIcon.astro';
+
+import { site } from '../data/site';
+import { featuredProjects } from '../data/projects';
+
+const capabilities = [
+ {
+ label: 'Front-end',
+ body: 'Sleek and accessible interfaces built with Astro, TypeScript, and Sass — all the way to an in-house design system.',
+ },
+ {
+ label: 'Back-end',
+ body: 'Server-side services and APIs, data modeling, and business logic using Node and PostgreSQL.',
+ },
+ {
+ label: 'Tools & deployment',
+ body: 'Docker containerization, automated CI/CD, and end-to-end production deployment.',
+ },
+];
+
+const instruments = [
+ { icon: 'astro', label: 'Astro' },
+ { icon: 'typescript', label: 'TypeScript' },
+ { icon: 'javascript', label: 'JavaScript' },
+ { icon: 'sass', label: 'Sass' },
+ { icon: 'postgresql', label: 'PostgreSQL' },
+ { icon: 'docker', label: 'Docker' },
+ { icon: 'linux', label: 'Linux' },
+ { icon: 'cloudflare', label: 'Cloudflare' },
+ { icon: 'gitea', label: 'Gitea CI/CD' },
+ { icon: 'rust', label: 'Rust' },
+ { icon: 'python', label: 'Python' },
+ { icon: 'git', label: 'Git' },
+];
+---
+
+
+ {/* ─────────────────────────── HERO ─────────────────────────── */}
+
+
+
+ Available · {site.availability}
+
+
+
+ Louis Potevin
+ Full-stack developer
+
+
+
+ I design and develop web applications from end to end — from front-end
+ to back-end and production deployment. I am equally comfortable working
+ on the user interface as I am with the services and infrastructure running them.
+
+
+
+
+
+
+
+
+ Available
+ {site.availability}
+ ·
+ {site.mobility}
+
+ A full-stack profile, comfortable across the entire stack.
+
+
+
+ I have hands-on experience running business applications in production while
+ simultaneously developing my own side projects: a design system published
+ on npm, a self-hosted infrastructure, and tools built with Rust and Python.
+
+
+ What drives me: mastering the entire cycle — the component you look at,
+ the service that delivers it, and the CI that deploys it. This website is
+ a prime example, built entirely on top of my own design system.
+
+ I am looking for a full-stack permanent position (CDI) starting September 2026.
+ {' '}{site.mobility}.
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pages/work/[slug].astro b/src/pages/work/[slug].astro
new file mode 100644
index 0000000..a3687b0
--- /dev/null
+++ b/src/pages/work/[slug].astro
@@ -0,0 +1,305 @@
+---
+import BaseLayout from '../../layouts/BaseLayout.astro';
+import Eyebrow from '../../components/Eyebrow.astro';
+import TechIcon from '../../components/TechIcon.astro';
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ Badge,
+ Button,
+ Link,
+} from '@unkn0wndo3s/nova-design-system';
+import { ArrowUpRight, ArrowLeft } from '@lucide/astro';
+import { projects, statusMeta, type Project } from '../../data/projects';
+
+export function getStaticPaths() {
+ return projects.map((project) => ({
+ params: { slug: project.slug },
+ props: { project },
+ }));
+}
+
+interface Props {
+ project: Project;
+}
+const { project } = Astro.props;
+const status = statusMeta[project.status];
+const title = `${project.title} — Louis Potevin`;
+---
+
+
+
+
+
+ Home
+ Projects
+
+ {project.designation}
+
+
+
+
+ {project.designation}
+ {status.label}
+
+
+
{project.title}
+
{project.tagline}
+
+
+
+
Role
+
{project.role}
+
+
+
Timeline
+
{project.period}
+
+
+
+
+
+
+
{project.summary}
+
+
+
+
+ {project.context.map((c) =>
{c}
)}
+
+
+
+
+
+
+ {project.contributions.map((c) =>
{c}
)}
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pages/work/index.astro b/src/pages/work/index.astro
new file mode 100644
index 0000000..3e5168a
--- /dev/null
+++ b/src/pages/work/index.astro
@@ -0,0 +1,62 @@
+---
+import BaseLayout from '../../layouts/BaseLayout.astro';
+import Eyebrow from '../../components/Eyebrow.astro';
+import ProjectCard from '../../components/ProjectCard.astro';
+import { Breadcrumb, BreadcrumbItem } from '@unkn0wndo3s/nova-design-system';
+import { projects } from '../../data/projects';
+
+const title = 'Projects — Louis Potevin, full-stack web developer / développeur full-stack';
+const description =
+ 'Projects by Louis Potevin: Astro design system, self-hosted infrastructure, systems tools in Rust, and LLM explorations. Full-stack web developer.';
+---
+
+
+
+
+ Home
+ Projects
+
+
+
+
Projects
+
+ A showcase of everything I build — from business software running in production to
+ the infrastructure supporting it, alongside my personal tools.
+
+
+
+
+ {projects.map((p) => )}
+
+
+
+
\ No newline at end of file
diff --git a/src/styles/_type.scss b/src/styles/_type.scss
new file mode 100644
index 0000000..643b86c
--- /dev/null
+++ b/src/styles/_type.scss
@@ -0,0 +1,5 @@
+// Re-exports the official typography mixins from Nova DS.
+// The partial is resolved via a loadPath pointing to the package's
+// style folder (its `exports` field prevents direct access to the subpath).
+// Usage in a component: @use "styles/type" as *; then @include text-3xl;
+@forward "tokens/typography";
diff --git a/src/styles/global.scss b/src/styles/global.scss
new file mode 100644
index 0000000..b471d99
--- /dev/null
+++ b/src/styles/global.scss
@@ -0,0 +1,44 @@
+@use 'styles/type' as *;
+
+html {
+ scroll-behavior: smooth;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ html {
+ scroll-behavior: auto;
+ }
+}
+
+body {
+ margin: 0;
+ min-height: 100vh;
+ color: var(--nds-text);
+ font-family: var(--nds-font-sans);
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ background-color: var(--nds-background);
+}
+
+::selection {
+ background: color-mix(in srgb, var(--nds-primary) 35%, transparent);
+ color: var(--nds-text);
+}
+
+* {
+ scrollbar-width: thin;
+ scrollbar-color: var(--nds-border-strong) transparent;
+}
+
+a {
+ color: inherit;
+}
+
+img {
+ max-width: 100%;
+ display: block;
+}
+
+:focus-visible {
+ outline: none;
+}