fix: replace em dashes with hyphens in README.md, package.json, scene.ts, projects.ts, site.ts, BaseLayout.astro, about.astro, contact.astro, index.astro, [slug].astro, and work/index.astro

This commit is contained in:
LOUIS POTEVIN
2026-06-24 14:27:42 +02:00
parent fa37f0358d
commit d1cc9de1f1
11 changed files with 51 additions and 55 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
# louis-potevin.dev # louis-potevin.dev
Professional portfolio of **Louis Potevin** full-stack developer. Professional portfolio of **Louis Potevin** - full-stack developer.
Static multi-page website built with **Astro + TypeScript + Sass**, styled Static multi-page website built with **Astro + TypeScript + Sass**, styled
**exclusively** by the in-house design system **`@unkn0wndo3s/nova-design-system`** **exclusively** by the in-house design system **`@unkn0wndo3s/nova-design-system`**
(tokens, components), featuring a 3D space background controlled via scrolling. (tokens, components), featuring a 3D space background controlled via scrolling.
@@ -28,9 +28,9 @@ compatibility with Astro 7 in its `peerDependencies`. The **`.npmrc`**
All modifiable content is centralized. Values to be confirmed are All modifiable content is centralized. Values to be confirmed are
marked with `TODO` in the code : marked with `TODO` in the code :
- **`src/data/site.ts`** name, role, email, external links (Gitea, GitHub, - **`src/data/site.ts`** - name, role, email, external links (Gitea, GitHub,
npm, LinkedIn). In particular, verify : npm, LinkedIn). In particular, verify :
- **`src/data/projects.ts`** la liste des projets (les « stations »). - **`src/data/projects.ts`** - la liste des projets (les « stations »).
Chaque entrée : résumé, contexte, contributions, stack, liens. Chaque entrée : résumé, contexte, contributions, stack, liens.
--- ---
+1 -1
View File
@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"description": "Portfolio Louis Potevin louis-potevin.dev. Astro + Nova Design System.", "description": "Portfolio Louis Potevin - louis-potevin.dev. Astro + Nova Design System.",
"engines": { "engines": {
"node": ">=22.12.0" "node": ">=22.12.0"
}, },
+6 -8
View File
@@ -1,13 +1,11 @@
import * as THREE from 'three'; import * as THREE from 'three';
// ─────────────────────────────────────────────────────────────────────────────
// Ambient space background: starfield, subtle nebula, and a slow-drifting, // Ambient space background: starfield, subtle nebula, and a slow-drifting,
// slow-rotating asteroid. No scroll binding, no engine flames. // slow-rotating asteroid. No scroll binding, no engine flames.
// Slight pointer parallax. Respects prefers-reduced-motion. // Slight pointer parallax. Respects prefers-reduced-motion.
// ─────────────────────────────────────────────────────────────────────────────
export interface SceneHandle { export interface SceneHandle {
/** Kept for compatibility no-op (no more impact/flame effect). */ /** Kept for compatibility - no-op (no more impact/flame effect). */
setImpact(value: number): void; setImpact(value: number): void;
destroy(): void; destroy(): void;
} }
@@ -64,7 +62,7 @@ export function initScene(canvas: HTMLCanvasElement): SceneHandle {
); );
camera.position.set(0, 0, 18); camera.position.set(0, 0, 18);
// ── Lighting (cold, consistent with Nova tokens) ─────────────────────────── // Lighting (cold, consistent with Nova tokens)
const ambient = new THREE.AmbientLight(0x16323a, 1.0); const ambient = new THREE.AmbientLight(0x16323a, 1.0);
scene.add(ambient); scene.add(ambient);
const key = new THREE.DirectionalLight(COLORS.starCool, 0.9); const key = new THREE.DirectionalLight(COLORS.starCool, 0.9);
@@ -74,7 +72,7 @@ export function initScene(canvas: HTMLCanvasElement): SceneHandle {
rimLight.position.set(8, -3, 4); rimLight.position.set(8, -3, 4);
scene.add(rimLight); scene.add(rimLight);
// ── Starfield ────────────────────────────────────────────────────────────── // Starfield
const STAR_COUNT = prefersReduced ? 700 : 1700; const STAR_COUNT = prefersReduced ? 700 : 1700;
const starGeo = new THREE.BufferGeometry(); const starGeo = new THREE.BufferGeometry();
const starPos = new Float32Array(STAR_COUNT * 3); const starPos = new Float32Array(STAR_COUNT * 3);
@@ -106,7 +104,7 @@ export function initScene(canvas: HTMLCanvasElement): SceneHandle {
const stars = new THREE.Points(starGeo, starMat); const stars = new THREE.Points(starGeo, starMat);
scene.add(stars); scene.add(stars);
// ── Nebula (soft sprites, pushed far into the background) ────────────────── // Nebula (soft sprites, pushed far into the background)
const nebula = new THREE.Group(); const nebula = new THREE.Group();
const nebTex = radialTexture('rgba(40,129,129,0.55)'); const nebTex = radialTexture('rgba(40,129,129,0.55)');
[ [
@@ -129,7 +127,7 @@ export function initScene(canvas: HTMLCanvasElement): SceneHandle {
}); });
scene.add(nebula); scene.add(nebula);
// ── Asteroid (cold rock, subtle halo) ────────────────────────────────────── // Asteroid (cold rock, subtle halo)
const asteroid = new THREE.Group(); const asteroid = new THREE.Group();
const rockGeo = new THREE.IcosahedronGeometry(1.9, 2); const rockGeo = new THREE.IcosahedronGeometry(1.9, 2);
const pos = rockGeo.attributes.position as THREE.BufferAttribute; const pos = rockGeo.attributes.position as THREE.BufferAttribute;
@@ -171,7 +169,7 @@ export function initScene(canvas: HTMLCanvasElement): SceneHandle {
asteroid.position.set(6.5, 4.5, -2); asteroid.position.set(6.5, 4.5, -2);
scene.add(asteroid); scene.add(asteroid);
// ── State & loop ─────────────────────────────────────────────────────────── // State & loop
const pointer = { x: 0, y: 0, tx: 0, ty: 0 }; const pointer = { x: 0, y: 0, tx: 0, ty: 0 };
let running = true; let running = true;
+7 -9
View File
@@ -1,7 +1,5 @@
// ───────────────────────────────────────────────────────────────────────────── // Projects - real content. Edit freely.
// Projects — real content. Edit freely.
// `icon` matches a simple-icons-astro component (using PascalCase elsewhere). // `icon` matches a simple-icons-astro component (using PascalCase elsewhere).
// ─────────────────────────────────────────────────────────────────────────────
export type ProjectStatus = 'live' | 'maintenu' | 'archive' | 'r&d'; export type ProjectStatus = 'live' | 'maintenu' | 'archive' | 'r&d';
@@ -32,8 +30,8 @@ export const projects: Project[] = [
slug: 'nova-design-system', slug: 'nova-design-system',
designation: '01', designation: '01',
title: 'Nova Design System', title: 'Nova Design System',
tagline: 'Astro component library tokens, accessibility, npm distribution.', tagline: 'Astro component library - tokens, accessibility, npm distribution.',
period: '2026 ongoing', period: '2026 - ongoing',
role: 'Author · maintainer', role: 'Author · maintainer',
status: 'maintenu', status: 'maintenu',
featured: true, featured: true,
@@ -65,7 +63,7 @@ export const projects: Project[] = [
designation: '02', designation: '02',
title: 'Self-hosted Infrastructure & Deployment', title: 'Self-hosted Infrastructure & Deployment',
tagline: 'Containerized services, CI/CD, and secured ingress on a fully controlled infrastructure.', tagline: 'Containerized services, CI/CD, and secured ingress on a fully controlled infrastructure.',
period: '2025 ongoing', period: '2025 - ongoing',
role: 'Design & operations', role: 'Design & operations',
status: 'live', status: 'live',
featured: true, featured: true,
@@ -120,9 +118,9 @@ export const projects: Project[] = [
{ {
slug: 'rust-file-organizer', slug: 'rust-file-organizer',
designation: '04', designation: '04',
title: 'File Organizer Java to Rust', title: 'File Organizer - Java to Rust',
tagline: 'File sorting tool, currently being ported from Java to Rust.', tagline: 'File sorting tool, currently being ported from Java to Rust.',
period: '2025 ongoing', period: '2025 - ongoing',
role: 'Development', role: 'Development',
status: 'r&d', status: 'r&d',
featured: false, featured: false,
@@ -147,7 +145,7 @@ export const projects: Project[] = [
designation: '05', designation: '05',
title: 'LLM Training & Inference', title: 'LLM Training & Inference',
tagline: 'Small language model trained on a Reddit conversation dataset.', tagline: 'Small language model trained on a Reddit conversation dataset.',
period: '2025 ongoing', period: '2025 - ongoing',
role: 'Development', role: 'Development',
status: 'r&d', status: 'r&d',
featured: false, featured: false,
+1 -1
View File
@@ -5,7 +5,7 @@ export const site = {
domain: "louis-potevin.dev", domain: "louis-potevin.dev",
location: "Limoges · Nouvelle-Aquitaine", location: "Limoges · Nouvelle-Aquitaine",
// Job Search // Job Search
availability: "Permanent contract (CDI) September 2026", availability: "Permanent contract (CDI) - September 2026",
mobility: "Anywhere in France, on-site or remote", mobility: "Anywhere in France, on-site or remote",
// Contact // Contact
email: "contact@louis-potevin.dev", email: "contact@louis-potevin.dev",
+3 -3
View File
@@ -15,7 +15,7 @@ export interface Props {
} }
const { const {
title = 'Louis Potevin Full-stack Developer (front, back, web)', 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.", 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 = [], keywords = [],
} = Astro.props; } = Astro.props;
@@ -91,7 +91,7 @@ const personLd = {
const siteLd = { const siteLd = {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'WebSite', '@type': 'WebSite',
name: `${site.name} Portfolio`, name: `${site.name} - Portfolio`,
url: Astro.site?.href ?? 'https://louis-potevin.dev', url: Astro.site?.href ?? 'https://louis-potevin.dev',
inLanguage: ['en-US', 'fr-FR'], inLanguage: ['en-US', 'fr-FR'],
}; };
@@ -113,7 +113,7 @@ const siteLd = {
<link rel="canonical" href={canonical} /> <link rel="canonical" href={canonical} />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:site_name" content={`${site.name} Portfolio`} /> <meta property="og:site_name" content={`${site.name} - Portfolio`} />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="fr_FR" /> <meta property="og:locale:alternate" content="fr_FR" />
<meta property="og:title" content={title} /> <meta property="og:title" content={title} />
+5 -5
View File
@@ -16,23 +16,23 @@ import {
import { Send, Mail } from '@lucide/astro'; import { Send, Mail } from '@lucide/astro';
import { site } from '../data/site'; import { site } from '../data/site';
const title = 'About Louis Potevin, full-stack web developer / développeur full-stack'; const title = 'About - Louis Potevin, full-stack web developer / développeur full-stack';
const description = 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."; "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 = [ const parcours = [
{ {
year: '2024 2026', year: '2024 - 2026',
title: 'Professional Experience · Legrand France', 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.", 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', year: '2024 - 2026',
title: 'BUT MMI DWDI Track · IUT du Limousin', 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.", 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', year: '2026 - ongoing',
title: 'Personal Projects', 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.", body: "Designing an open-source design system published on npm, managing a self-hosted server infrastructure, and building system tools with Rust and Python.",
}, },
+5 -5
View File
@@ -15,7 +15,7 @@ import { Send, Mail } from '@lucide/astro';
import BrandLinkedin from '../components/BrandLinkedin.astro'; import BrandLinkedin from '../components/BrandLinkedin.astro';
import { site } from '../data/site'; import { site } from '../data/site';
const title = 'Contact Louis Potevin'; const title = 'Contact - Louis Potevin';
const description = const description =
'Contact Louis Potevin, full-stack web developer. Available for permanent positions (CDI) starting September 2026, anywhere in France or remote.'; 'Contact Louis Potevin, full-stack web developer. Available for permanent positions (CDI) starting September 2026, anywhere in France or remote.';
@@ -46,7 +46,7 @@ const directLinks = [
</section> </section>
<div class="layout"> <div class="layout">
{/* ── Form (Nova components) ── */} {/* Form (Nova components) */}
<section class="formWrap" aria-labelledby="form-title"> <section class="formWrap" aria-labelledby="form-title">
<h2 id="form-title" class="formWrap__title">Message</h2> <h2 id="form-title" class="formWrap__title">Message</h2>
@@ -95,7 +95,7 @@ const directLinks = [
</div> </div>
</section> </section>
{/* ── Direct Links ── */} {/* Direct Links */}
<aside class="direct" aria-label="Direct channels"> <aside class="direct" aria-label="Direct channels">
<p class="direct__title">Direct Channels</p> <p class="direct__title">Direct Channels</p>
<ul class="direct__list"> <ul class="direct__list">
@@ -140,7 +140,7 @@ const directLinks = [
const subject = subjectEl?.textContent?.trim() || 'Inquiry'; const subject = subjectEl?.textContent?.trim() || 'Inquiry';
const message = val('cf-message'); const message = val('cf-message');
const body = `${message}\n\n ${name}${from ? ` (${from})` : ''}`; const body = `${message}\n\n- ${name}${from ? ` (${from})` : ''}`;
const href = `mailto:${email}?subject=${encodeURIComponent( const href = `mailto:${email}?subject=${encodeURIComponent(
'[Portfolio] ' + subject, '[Portfolio] ' + subject,
)}&body=${encodeURIComponent(body)}`; )}&body=${encodeURIComponent(body)}`;
@@ -289,7 +289,7 @@ const directLinks = [
margin: 0; margin: 0;
} }
/* ── RESPONSIVE ── */ /* RESPONSIVE */
@media (max-width: 820px) { @media (max-width: 820px) {
.layout { grid-template-columns: 1fr; gap: var(--nds-spacing-xl); } .layout { grid-template-columns: 1fr; gap: var(--nds-spacing-xl); }
.direct { position: static; } .direct { position: static; }
+15 -15
View File
@@ -13,7 +13,7 @@ import { featuredProjects } from '../data/projects';
const capabilities = [ const capabilities = [
{ {
label: 'Front-end', label: 'Front-end',
body: 'Sleek and accessible interfaces built with Astro, TypeScript, and Sass all the way to an in-house design system.', body: 'Sleek and accessible interfaces built with Astro, TypeScript, and Sass - all the way to an in-house design system.',
}, },
{ {
label: 'Back-end', label: 'Back-end',
@@ -42,7 +42,7 @@ const instruments = [
--- ---
<BaseLayout> <BaseLayout>
{/* ─────────────────────────── HERO ─────────────────────────── */} {/* HERO */}
<section class="hero" aria-labelledby="hero-title"> <section class="hero" aria-labelledby="hero-title">
<div class="hero__status"> <div class="hero__status">
<span class="hero__dot" aria-hidden="true"></span> <span class="hero__dot" aria-hidden="true"></span>
@@ -55,7 +55,7 @@ const instruments = [
</h1> </h1>
<p class="hero__lede"> <p class="hero__lede">
I design and develop web applications from end to end from front-end I design and develop web applications from end to end - from front-end
to back-end and production deployment. I am equally comfortable working 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. on the user interface as I am with the services and infrastructure running them.
</p> </p>
@@ -79,7 +79,7 @@ const instruments = [
</div> </div>
</section> </section>
{/* ─────────────────────── PROFILE / CAPABILITIES ─────────────────────── */} {/* PROFILE / CAPABILITIES */}
<section class="band" aria-labelledby="profile-title"> <section class="band" aria-labelledby="profile-title">
<Eyebrow index="01" label="Profile" /> <Eyebrow index="01" label="Profile" />
<div class="band__split"> <div class="band__split">
@@ -93,7 +93,7 @@ const instruments = [
on npm, a self-hosted infrastructure, and tools built with Rust and Python. on npm, a self-hosted infrastructure, and tools built with Rust and Python.
</p> </p>
<p> <p>
What drives me: mastering the entire cycle the component you look at, 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 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. a prime example, built entirely on top of my own design system.
</p> </p>
@@ -112,7 +112,7 @@ const instruments = [
</ul> </ul>
</section> </section>
{/* ─────────────────────────── FEATURED PROJECTS ─────────────────────────── */} {/* FEATURED PROJECTS */}
<section class="band" aria-labelledby="proj-title"> <section class="band" aria-labelledby="proj-title">
<Eyebrow index="02" label="Featured projects" /> <Eyebrow index="02" label="Featured projects" />
<div class="band__head"> <div class="band__head">
@@ -126,7 +126,7 @@ const instruments = [
</div> </div>
</section> </section>
{/* ─────────────────────────── STACK ─────────────────────────── */} {/* STACK */}
<section class="band" aria-labelledby="stack-title"> <section class="band" aria-labelledby="stack-title">
<Eyebrow index="03" label="Tech stack" /> <Eyebrow index="03" label="Tech stack" />
<h2 id="stack-title" class="band__lead band__lead--center"> <h2 id="stack-title" class="band__lead band__lead--center">
@@ -146,9 +146,9 @@ const instruments = [
</ul> </ul>
</section> </section>
{/* ─────────────────────────── CONTACT CTA ─────────────────────────── */} {/* CONTACT CTA */}
<section class="closing" aria-labelledby="cta-title"> <section class="closing" aria-labelledby="cta-title">
<p class="closing__eyebrow">Available {site.availability}</p> <p class="closing__eyebrow">Available - {site.availability}</p>
<h2 id="cta-title" class="closing__title">Let's talk!</h2> <h2 id="cta-title" class="closing__title">Let's talk!</h2>
<p class="closing__text"> <p class="closing__text">
I am looking for a full-stack permanent position (CDI) starting September 2026. I am looking for a full-stack permanent position (CDI) starting September 2026.
@@ -170,7 +170,7 @@ const instruments = [
<style lang="scss"> <style lang="scss">
@use 'styles/type' as *; @use 'styles/type' as *;
/* ── HERO ── */ /* HERO */
.hero { .hero {
min-height: clamp(520px, 80vh, 820px); min-height: clamp(520px, 80vh, 820px);
display: flex; display: flex;
@@ -247,7 +247,7 @@ const instruments = [
font-size: 0.75rem; font-size: 0.75rem;
} }
/* ── SECTION BANDS ── */ /* SECTION BANDS */
.band { padding: var(--nds-spacing-2xl) 0; } .band { padding: var(--nds-spacing-2xl) 0; }
.band__lead { .band__lead {
@include text-3xl; @include text-3xl;
@@ -295,7 +295,7 @@ const instruments = [
&:hover { color: var(--nds-accent); } &:hover { color: var(--nds-accent); }
} }
/* ── Capabilities ── */ /* Capabilities */
.caps { .caps {
list-style: none; list-style: none;
margin: var(--nds-spacing-2xl) 0 0; margin: var(--nds-spacing-2xl) 0 0;
@@ -330,7 +330,7 @@ const instruments = [
gap: var(--nds-spacing-lg); gap: var(--nds-spacing-lg);
} }
/* ── INSTRUMENTS ── */ /* INSTRUMENTS */
.instruments { .instruments {
list-style: none; list-style: none;
margin: 0; margin: 0;
@@ -364,7 +364,7 @@ const instruments = [
color: var(--nds-neutral); color: var(--nds-neutral);
} }
/* ── CLOSING ── */ /* CLOSING */
.closing { .closing {
text-align: center; text-align: center;
padding: var(--nds-spacing-3xl) 0; padding: var(--nds-spacing-3xl) 0;
@@ -403,7 +403,7 @@ const instruments = [
.hero__dot { animation: none; } .hero__dot { animation: none; }
} }
/* ── RESPONSIVE ── */ /* RESPONSIVE */
@media (max-width: 900px) { @media (max-width: 900px) {
.band__split { grid-template-columns: 1fr; gap: var(--nds-spacing-lg); } .band__split { grid-template-columns: 1fr; gap: var(--nds-spacing-lg); }
.caps { grid-template-columns: 1fr; } .caps { grid-template-columns: 1fr; }
+3 -3
View File
@@ -24,7 +24,7 @@ interface Props {
} }
const { project } = Astro.props; const { project } = Astro.props;
const status = statusMeta[project.status]; const status = statusMeta[project.status];
const title = `${project.title} Louis Potevin`; const title = `${project.title} - Louis Potevin`;
--- ---
<BaseLayout title={title} description={project.tagline}> <BaseLayout title={title} description={project.tagline}>
@@ -115,7 +115,7 @@ const title = `${project.title} — Louis Potevin`;
project.links.length === 0 && ( project.links.length === 0 && (
<div class="panel panel--muted"> <div class="panel panel--muted">
<p class="panel__note"> <p class="panel__note">
Internal or private project no public link available. Internal or private project - no public link available.
</p> </p>
</div> </div>
) )
@@ -297,7 +297,7 @@ const title = `${project.title} — Louis Potevin`;
border-top: var(--nds-border-width-thin) solid var(--nds-border); border-top: var(--nds-border-width-thin) solid var(--nds-border);
} }
/* ── RESPONSIVE ── */ /* RESPONSIVE */
@media (max-width: 820px) { @media (max-width: 820px) {
.detail__grid { grid-template-columns: 1fr; gap: var(--nds-spacing-xl); } .detail__grid { grid-template-columns: 1fr; gap: var(--nds-spacing-xl); }
.detail__side { position: static; } .detail__side { position: static; }
+2 -2
View File
@@ -5,7 +5,7 @@ import ProjectCard from '../../components/ProjectCard.astro';
import { Breadcrumb, BreadcrumbItem } from '@unkn0wndo3s/nova-design-system'; import { Breadcrumb, BreadcrumbItem } from '@unkn0wndo3s/nova-design-system';
import { projects } from '../../data/projects'; import { projects } from '../../data/projects';
const title = 'Projects Louis Potevin, full-stack web developer / développeur full-stack'; const title = 'Projects - Louis Potevin, full-stack web developer / développeur full-stack';
const description = const description =
'Projects by Louis Potevin: Astro design system, self-hosted infrastructure, systems tools in Rust, and LLM explorations. Full-stack web developer.'; 'Projects by Louis Potevin: Astro design system, self-hosted infrastructure, systems tools in Rust, and LLM explorations. Full-stack web developer.';
--- ---
@@ -20,7 +20,7 @@ const description =
<Eyebrow index="//" label="All projects" /> <Eyebrow index="//" label="All projects" />
<h1 class="head__title">Projects</h1> <h1 class="head__title">Projects</h1>
<p class="head__lede"> <p class="head__lede">
A showcase of everything I build from business software running in production to A showcase of everything I build - from business software running in production to
the infrastructure supporting it, alongside my personal tools. the infrastructure supporting it, alongside my personal tools.
</p> </p>
</section> </section>