feat: apply pro creative-lab art direction and validate astro pipeline

This commit is contained in:
Clément SAILLANT
2026-03-01 21:50:31 +01:00
parent 8484c16c09
commit a607feda60
57 changed files with 28622 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
name: Deploy Astro to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
PUBLIC_GTM_CONTAINER_ID: GTM-5SLM67QF
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+9
View File
@@ -0,0 +1,9 @@
# Internal private notes
notes-interne/
# Node / tooling
node_modules/
dist/
storybook-static/
.astro/
.DS_Store
+25
View File
@@ -0,0 +1,25 @@
import type { StorybookConfig } from '@storybook/react-vite';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(ts|tsx)'],
addons: ['@storybook/addon-onboarding', '@storybook/addon-docs', '@storybook/addon-a11y', '@storybook/addon-vitest'],
framework: {
name: '@storybook/react-vite',
options: {}
},
viteFinal: async (config) => {
config.resolve = config.resolve || {};
config.resolve.alias = {
...(typeof config.resolve.alias === 'object' ? config.resolve.alias : {}),
'@': path.resolve(fileURLToPath(new URL('../src', import.meta.url)))
};
return config;
},
docs: {
autodocs: 'tag'
}
};
export default config;
+17
View File
@@ -0,0 +1,17 @@
import type { Preview } from '@storybook/react-vite';
import '../src/styles/global.css';
const preview: Preview = {
parameters: {
layout: 'fullscreen',
backgrounds: {
default: 'studio-dark',
values: [
{ name: 'studio-dark', value: '#0f0e17' },
{ name: 'studio-deep', value: '#151325' }
]
}
}
};
export default preview;
@@ -0,0 +1,35 @@
# Architecture — Site personnel statique GitHub Pages
- **Date**: 2026-03-01
- **Gate visée**: G1 Arch Freeze
- **Responsable (agent)**: Architect
## Vue d'ensemble
Architecture front statique en 2 fichiers:
- `index.html`: structure sémantique (header/nav/main/sections/footer)
- `assets/styles.css`: thème, layout responsive, composants visuels
## Interfaces
- **Entrée**: navigateur web moderne
- **Sortie**: rendu d'un site vitrine personnel
- **Hébergement**: GitHub Pages (branch `main`, root)
## Structure UI
- Bloc `hero` avec CTA.
- Section `#a-propos` pour la présentation.
- Section `#projets` avec grille de cartes.
- Section `#contact` avec liens sortants.
## Contraintes techniques
- Aucun JS requis pour la version initiale.
- Chemins relatifs uniquement (`assets/styles.css`).
- CSS pur (variables, grid, media queries).
## Budgets
- Poids page + CSS < 80KB (source non compressée).
- Temps de chargement perçu < 1s sur connexion standard (contenu statique).
## Validation
- Smoke test HTTP local.
- Vérification ancres + rendu responsive.
- Capture visuelle de preuve.
@@ -0,0 +1,5 @@
# G1 Arch Freeze Evidence (v2)
- Architecture UI détaillée: hero, about, projets, contact.
- Contraintes GitHub Pages (chemins relatifs, statique sans build) confirmées.
- Budgets et stratégie de validation ajustés.
@@ -0,0 +1,5 @@
# G1 Arch Freeze Evidence
- Architecture statique validée: `index.html` + `assets/styles.css`
- Interfaces et contraintes de chemins relatifs définies
- Budgets simplifiés documentés
@@ -0,0 +1,259 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clément Saillant — L'électron rare</title>
<meta name="theme-color" content="#f7f4ec" />
<meta
name="description"
content="Site personnel de Clément Saillant (L'électron rare) : projets audiovisuels, performances et contact."
/>
<link rel="canonical" href="https://electron-rare.github.io/" />
<meta name="robots" content="index,follow,max-image-preview:large" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:site_name" content="L'électron rare" />
<meta property="og:title" content="Clément Saillant — L'électron rare" />
<meta property="og:url" content="https://electron-rare.github.io/" />
<meta
property="og:description"
content="Performances, création sonore expérimentale, collaborations vidéo et projets noise."
/>
<meta
property="og:image"
content="https://electron-rare.github.io/assets/og-cover.jpg"
/>
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="L'électron rare - performances et créations audiovisuelles" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Clément Saillant — L'électron rare" />
<meta
name="twitter:description"
content="Performances et création audiovisuelle expérimentale."
/>
<meta
name="twitter:image"
content="https://electron-rare.github.io/assets/og-cover.jpg"
/>
<link rel="preload" href="assets/styles.css" as="style" />
<link rel="stylesheet" href="assets/styles.css" />
<script>
window.GTM_CONTAINER_ID = "GTM-5SLM67QF";
window.dataLayer = window.dataLayer || [];
</script>
<script>
(function (w, d, s, l, i) {
if (!i) return;
w[l] = w[l] || [];
w[l].push({ "gtm.start": new Date().getTime(), event: "gtm.js" });
var f = d.getElementsByTagName(s)[0],
j = d.createElement(s),
dl = l !== "dataLayer" ? "&l=" + l : "";
j.async = true;
j.src = "https://www.googletagmanager.com/gtm.js?id=" + i + dl;
f.parentNode.insertBefore(j, f);
})(window, document, "script", "dataLayer", window.GTM_CONTAINER_ID);
</script>
</head>
<body>
<noscript>
<iframe
src="https://www.googletagmanager.com/ns.html?id=GTM-5SLM67QF"
height="0"
width="0"
style="display:none;visibility:hidden"
></iframe>
</noscript>
<a class="skip-link" href="#top">Aller au contenu</a>
<header class="site-header">
<div class="container nav-wrap">
<a class="brand" href="#top">L'électron rare</a>
<nav aria-label="Navigation principale">
<ul>
<li><a href="#a-propos">À propos</a></li>
<li><a href="#projets">Projets</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</nav>
</div>
</header>
<main id="top" class="container">
<section class="hero" aria-labelledby="hero-title">
<div class="hero-grid">
<div>
<p class="eyebrow">Performance • Noise • Audiovisuel</p>
<h1 id="hero-title">Clément Saillant, alias L'électron rare.</h1>
<p class="hero-text">
Projet artistique orienté expérimentations sonores et visuelles:
performances live, textures noise et collaborations entre scène et
image.
</p>
<ul class="hero-metrics" aria-label="Axes artistiques">
<li>Live sets</li>
<li>Textures bruitistes</li>
<li>Collab vidéo</li>
</ul>
<p class="hero-actions">
<a class="btn btn-primary" href="#projets" data-track="cta_hero_projets" data-destination="#projets">Voir mes projets</a>
<a class="btn btn-secondary" href="#contact" data-track="cta_hero_contact" data-destination="#contact">Me contacter</a>
</p>
</div>
<aside class="hero-card" aria-label="Carte artiste">
<p class="hero-card-kicker">Carte rapide</p>
<h2>L'électron rare</h2>
<p>
Performances et création sonore expérimentale, entre énergie live
et matière visuelle.
</p>
<a href="#a-propos" data-track="cta_hero_profile" data-destination="#a-propos">Explorer le profil</a>
</aside>
</div>
</section>
<section id="a-propos" class="panel" aria-labelledby="about-title">
<h2 id="about-title">À propos</h2>
<p>
Je suis <strong>Clément Saillant</strong>, artiste et créateur sous le
nom <strong>L'électron rare</strong>. J'explore les liens entre matière
sonore, récit visuel et énergie live, avec une approche artisanale et
directe.
</p>
</section>
<section id="projets" class="panel" aria-labelledby="projects-title">
<h2 id="projects-title">Projets</h2>
<div class="cards">
<article class="card">
<h3>L'électron rare</h3>
<p>
Univers principal: performances et créations expérimentales entre
musique électronique et visuel.
</p>
<p>
<a
href="https://fr.linkedin.com/in/electron-rare"
target="_blank"
rel="noopener noreferrer"
data-track="outbound_linkedin_project"
data-destination="linkedin.com"
>Profil LinkedIn</a
>
</p>
</article>
<article class="card">
<h3>Collaborations vidéo</h3>
<p>
Travail autour du VJing et des formats hybrides live, notamment avec
des collaborations type VJ Clearscreen.
</p>
</article>
<article class="card">
<h3>Électron Fou (noise)</h3>
<p>
Projet noise affilié à la scène expérimentale, avec sorties publiées
et présence sur plateformes audio.
</p>
<p>
<a
href="https://lelectron-fou.bandcamp.com/"
target="_blank"
rel="noopener noreferrer"
data-track="outbound_bandcamp_project"
data-destination="bandcamp.com"
>Bandcamp</a
>
</p>
</article>
</div>
</section>
<section id="contact" class="panel" aria-labelledby="contact-title">
<h2 id="contact-title">Contact</h2>
<p class="contact-intro">
Pour booking, collaboration artistique ou production audiovisuelle,
contactez-moi via les plateformes ci-dessous.
</p>
<ul class="contact-list">
<li>
<a
href="https://fr.linkedin.com/in/electron-rare"
target="_blank"
rel="noopener noreferrer"
data-track="outbound_linkedin_contact"
data-destination="linkedin.com"
>LinkedIn</a
>
</li>
<li>
<a
href="https://www.malt.com/profile/clementsaillant"
target="_blank"
rel="noopener noreferrer"
data-track="outbound_malt_contact"
data-destination="malt.com"
>Malt</a
>
</li>
<li>
<a
href="https://lelectron-fou.bandcamp.com/"
target="_blank"
rel="noopener noreferrer"
data-track="outbound_bandcamp_contact"
data-destination="bandcamp.com"
>Bandcamp (Électron Fou)</a
>
</li>
</ul>
<ul class="trust-points" aria-label="Repères de confiance">
<li>Profil professionnel vérifiable sur LinkedIn et Malt</li>
<li>Projets artistiques publiés sur plateformes publiques</li>
<li>Contact direct pour booking et collaborations</li>
</ul>
</section>
</main>
<footer class="site-footer">
<div class="container">
<small>© 2026 Clément Saillant — L'électron rare.</small>
</div>
</footer>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Clément Saillant",
"alternateName": "L'électron rare",
"url": "https://electron-rare.github.io/",
"jobTitle": "Artiste audiovisuel et créateur sonore expérimental",
"description": "Performances live, création noise et collaborations vidéo sous l'identité L'électron rare.",
"sameAs": [
"https://fr.linkedin.com/in/electron-rare",
"https://www.malt.com/profile/clementsaillant",
"https://lelectron-fou.bandcamp.com/"
]
}
</script>
<script>
(function () {
var links = document.querySelectorAll("[data-track]");
for (var i = 0; i < links.length; i += 1) {
links[i].addEventListener("click", function () {
var eventName = this.getAttribute("data-track");
var destination =
this.getAttribute("data-destination") || this.getAttribute("href") || "unknown";
window.dataLayer.push({
event: eventName,
event_category: "engagement",
event_label: eventName,
destination: destination
});
});
}
})();
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://electron-rare.github.io/sitemap.xml
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://electron-rare.github.io/</loc>
<lastmod>2026-03-01</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
</urlset>
@@ -0,0 +1,424 @@
:root {
--bg: #f7f4ec;
--surface: #fffdf7;
--surface-soft: #f2eee3;
--text: #171614;
--muted: #625d50;
--primary: #d74b2a;
--primary-strong: #b83c1f;
--border: #d8d1c1;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: "Avenir Next", "Segoe UI", "Helvetica Neue", sans-serif;
line-height: 1.6;
background:
radial-gradient(circle at 10% 10%, #ffddbf 0%, rgba(255, 221, 191, 0) 40%),
radial-gradient(circle at 90% 0%, #f0d8bf 0%, rgba(240, 216, 191, 0) 35%),
var(--bg);
color: var(--text);
text-rendering: optimizeLegibility;
}
.skip-link {
position: absolute;
left: -9999px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.skip-link:focus {
left: 0.8rem;
top: 0.8rem;
width: auto;
height: auto;
z-index: 40;
background: #111;
color: #fff;
padding: 0.45rem 0.7rem;
border-radius: 0.45rem;
}
.container {
width: min(1000px, 92vw);
margin: 0 auto;
}
section[id] {
scroll-margin-top: 6.2rem;
}
.site-header {
position: sticky;
top: 0;
z-index: 20;
backdrop-filter: blur(8px);
background: rgba(247, 244, 236, 0.86);
border-bottom: 1px solid var(--border);
}
.nav-wrap {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.8rem 0;
}
.brand {
color: var(--text);
text-decoration: none;
font-weight: 800;
letter-spacing: 0.01em;
}
nav ul {
list-style: none;
display: flex;
gap: 1rem;
margin: 0;
padding: 0;
}
nav a {
display: inline-flex;
align-items: center;
min-height: 2rem;
color: var(--muted);
text-decoration: none;
font-weight: 500;
padding: 0.35rem 0.55rem;
border-radius: 0.45rem;
}
nav a:hover,
nav a:focus-visible {
color: var(--text);
background: rgba(255, 255, 255, 0.66);
}
.hero {
padding: clamp(3.2rem, 7vw, 4.8rem) 0 2.2rem;
}
.hero-grid {
display: grid;
grid-template-columns: 1.25fr 0.8fr;
gap: 1.1rem;
align-items: end;
}
.eyebrow {
color: var(--primary);
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
margin: 0;
font-size: 0.8rem;
}
h1 {
font-size: clamp(2rem, 5vw, 3.2rem);
line-height: 1.12;
margin: 0.6rem 0 1rem;
max-width: 18ch;
text-wrap: balance;
}
.hero-text {
max-width: 64ch;
margin: 0 0 1.4rem;
color: var(--muted);
}
.hero-metrics {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
margin: 0 0 1.3rem;
padding: 0;
}
.hero-metrics li {
background: rgba(255, 255, 255, 0.76);
border: 1px solid var(--border);
border-radius: 999px;
padding: 0.3rem 0.65rem;
font-size: 0.92rem;
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 0.7rem;
}
.btn {
min-height: 2.75rem;
display: inline-block;
padding: 0.7rem 1.02rem;
border-radius: 0.7rem;
text-decoration: none;
font-weight: 600;
transition: transform 140ms ease, box-shadow 140ms ease, background-color 140ms ease;
}
.btn-primary {
background: var(--primary);
color: #fff7f1;
}
.btn-primary:hover,
.btn-primary:focus-visible {
background: var(--primary-strong);
transform: translateY(-1px);
box-shadow: 0 8px 18px rgba(183, 60, 31, 0.22);
}
.btn-secondary {
border: 1px solid var(--border);
color: var(--text);
background: rgba(255, 255, 255, 0.65);
}
.btn-secondary:hover,
.btn-secondary:focus-visible {
transform: translateY(-1px);
}
.hero-card {
background: linear-gradient(180deg, #fff6eb 0%, #f3e6d4 100%);
border: 1px solid #ddc8ab;
border-radius: 14px;
padding: 1rem 1rem 1.1rem;
box-shadow: 0 12px 26px rgba(30, 18, 6, 0.11);
}
.hero-card-kicker {
margin: 0;
color: #90633d;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.hero-card h2 {
margin: 0.35rem 0 0.45rem;
font-size: 1.4rem;
}
.hero-card p {
margin: 0 0 0.7rem;
color: #4f4639;
}
.hero-card a {
color: var(--primary-strong);
font-weight: 700;
text-decoration: none;
}
.hero-card a:hover,
.hero-card a:focus-visible {
text-decoration: underline;
}
.panel {
background: linear-gradient(180deg, var(--surface) 0%, var(--surface-soft) 100%);
border: 1px solid var(--border);
border-radius: 14px;
padding: 1.1rem 1.2rem 1.25rem;
margin: 1rem 0;
box-shadow: 0 10px 24px rgba(30, 18, 6, 0.07);
content-visibility: auto;
contain-intrinsic-size: 1px 500px;
}
h2 {
margin-top: 0.2rem;
text-wrap: balance;
}
.cards {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 0.9rem;
}
.card {
grid-column: span 4;
background: rgba(255, 252, 246, 0.86);
border: 1px solid var(--border);
border-radius: 10px;
padding: 0.9rem;
transition: transform 140ms ease, box-shadow 140ms ease;
}
.card h3 {
margin-top: 0.1rem;
margin-bottom: 0.45rem;
}
.card p {
margin-top: 0;
}
.card a {
color: var(--primary);
font-weight: 600;
text-decoration: none;
}
.card a:hover,
.card a:focus-visible {
text-decoration: underline;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 18px rgba(30, 18, 6, 0.08);
}
.contact-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.contact-list a {
display: inline-flex;
align-items: center;
min-height: 2rem;
color: var(--primary);
text-decoration: none;
font-weight: 600;
}
.contact-list a:hover,
.contact-list a:focus-visible {
text-decoration: underline;
}
.contact-intro {
margin-top: 0;
color: var(--muted);
max-width: 62ch;
}
.trust-points {
margin: 0.95rem 0 0;
padding-left: 1.1rem;
color: var(--muted);
}
.trust-points li + li {
margin-top: 0.3rem;
}
.site-footer {
border-top: 1px solid var(--border);
color: var(--muted);
padding: 1.2rem 0 2.2rem;
margin-top: 1.6rem;
}
a:focus-visible,
button:focus-visible {
outline: 3px solid var(--primary);
outline-offset: 2px;
border-radius: 4px;
}
@media (max-width: 960px) {
.hero-grid {
grid-template-columns: 1fr;
}
.hero-card {
max-width: 38rem;
}
.cards {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.card {
grid-column: auto;
}
}
@media (max-width: 760px) {
.nav-wrap {
flex-direction: column;
gap: 0.7rem;
}
nav ul {
flex-wrap: wrap;
justify-content: center;
gap: 0.45rem;
}
nav a {
display: inline-block;
padding: 0.42rem 0.62rem;
}
.cards {
grid-template-columns: 1fr;
}
.hero {
padding-top: 2.4rem;
}
}
@media (max-width: 390px) {
.container {
width: min(1000px, 94vw);
}
h1 {
font-size: clamp(1.72rem, 9vw, 2.25rem);
}
.hero-actions {
gap: 0.55rem;
}
.btn {
width: 100%;
text-align: center;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 1ms !important;
animation-iteration-count: 1 !important;
transition-duration: 1ms !important;
scroll-behavior: auto !important;
}
}
@media (prefers-contrast: more) {
:root {
--muted: #453f33;
--border: #8b816d;
}
}
@@ -0,0 +1,5 @@
# G0 Spec Freeze Evidence (v2)
- Spécification orientée « site personnel » finalisée.
- AC mis à jour avec navigation ancrée et contenu vitrine.
- Non-goals, risques et TODO de personnalisation documentés.
@@ -0,0 +1,6 @@
# G0 Spec Freeze Evidence
- Spec créée: `specs/site-github-pages-spec.md`
- AC définis: AC1..AC4
- Non-goals et risques documentés
- TODO ajoutés pour informations manquantes
@@ -0,0 +1,6 @@
# G2 Implement Evidence (v2)
Implémentation livrée:
- `index.html` transformé en template de site personnel complet.
- `assets/styles.css` enrichi (navigation sticky, sections, cartes projets, responsive).
- Documentation de déploiement mise à jour.
@@ -0,0 +1,39 @@
# G2 Implement Evidence (v3) - SEO + Acquisition
Date: 2026-03-01
## Portée implémentée
- SEO technique: canonical, robots, sitemap, OG/Twitter image.
- Tracking contract: événements CTA/outbound avec `event_category`, `event_label`, `destination`.
- Bootstrap GTM prêt à brancher (`GTM-REPLACE_ME`).
- Conversion UX: section contact enrichie + trust points.
- Accessibilité/réactivité: skip-link déjà présent + réglage ultra-mobile (`<=390px`).
## Fichiers modifiés
- `index.html`
- `assets/styles.css`
- `docs/github-pages-deploy.md`
- `robots.txt` (nouveau)
- `sitemap.xml` (nouveau)
- `assets/og-cover.jpg` (nouveau)
## Mapping tracking (event -> destination)
| Event | Type | Destination | Usage |
|---|---|---|---|
| `cta_hero_projets` | CTA interne | `#projets` | Navigation vers section projets |
| `cta_hero_contact` | CTA interne | `#contact` | Navigation vers section contact |
| `cta_hero_profile` | CTA interne | `#a-propos` | Navigation vers section profil |
| `outbound_linkedin_project` | Sortant | `linkedin.com` | Preuve sociale projet |
| `outbound_linkedin_contact` | Sortant | `linkedin.com` | Conversion primaire |
| `outbound_malt_contact` | Sortant | `malt.com` | Conversion secondaire |
| `outbound_bandcamp_project` | Sortant | `bandcamp.com` | Projet musical |
| `outbound_bandcamp_contact` | Sortant | `bandcamp.com` | Contact alternatif |
Paramètres communs poussés:
- `event_category=engagement`
- `event_label=<event_name>`
- `destination=<domain|anchor>`
## Notes
- Pour activer GTM en production: remplacer `GTM-REPLACE_ME` dans le script head et le bloc noscript.
- URL canonique actuelle: `https://electron-rare.github.io/`.
@@ -0,0 +1,38 @@
# G2 Implement Evidence (v4) - Astro Studio Migration Foundation
Date: 2026-03-01
## Scope implémenté
- Mise en place stack primaire:
- Astro + TypeScript strict
- React integration
- Tailwind 4 (via plugin Vite)
- Motion
- Radix/shadcn-like base (`Button` with Radix Slot + CVA)
- Storybook React-Vite
- Création architecture `src/` composantisée:
- `src/layouts/BaseLayout.astro`
- `src/pages/index.astro`
- sections React (`Hero`, `About`, `Projects`, `Contact`)
- composants UI (`Button`, `Card`)
- Conservation contrat SEO/analytics:
- metadata canonical/OG/Twitter
- JSON-LD Person
- contrat `dataLayer` (`event_category`, `event_label`, `destination`)
- GTM ID `GTM-5SLM67QF` via `PUBLIC_GTM_CONTAINER_ID` fallback
- Préparation déploiement Pages via GitHub Actions:
- `.github/workflows/deploy-pages.yml`
- Storybook full coverage initiale des composants/sections clés.
## Fichiers clés ajoutés/modifiés
- `package.json`, `package-lock.json`
- `astro.config.mjs`, `tsconfig.json`
- `src/**` (layouts/components/styles/pages)
- `.storybook/main.ts`, `.storybook/preview.ts`
- `.github/workflows/deploy-pages.yml`
- `public/robots.txt`, `public/sitemap.xml`, `public/assets/og-cover.jpg`
- `docs/github-pages-deploy.md`
- `specs/site-github-pages-spec.md`
## Note
- L'ancien socle statique (`index.html`, `assets/styles.css`) est conservé temporairement dans le repo pour transition contrôlée.
@@ -0,0 +1,7 @@
# G3 Verification Evidence (v2)
Vérifications exécutées:
- `python3 -m http.server 4173`
- `curl -I http://127.0.0.1:4173/` -> `HTTP/1.0 200 OK`
- `rg 'href="/' index.html` (attendu: aucune occurrence)
- Capture Playwright: `artifacts/qa-test/2026-03-01/site-home-v2.png`
@@ -0,0 +1,28 @@
# G3 Verification Evidence (v3) - SEO + Tracking
Date: 2026-03-01
## Vérifications exécutées
- `rg -n "canonical|og:image|twitter:image|application/ld\+json|GTM_CONTAINER_ID|event_category|event_label|destination" index.html`
- `rg -n "data-track=|data-destination=" index.html`
- `sips -g pixelWidth -g pixelHeight assets/og-cover.jpg`
- `cat robots.txt`
- `cat sitemap.xml`
- `python3 -m http.server 4173` puis:
- `curl -I http://127.0.0.1:4173/`
- `curl -s http://127.0.0.1:4173/robots.txt`
- `curl -s http://127.0.0.1:4173/sitemap.xml`
## Résultats attendus/observés
- Canonical présent et fixé sur `https://electron-rare.github.io/`.
- OG/Twitter image renseignée vers `assets/og-cover.jpg` (URL absolue).
- JSON-LD `Person` enrichi (`jobTitle`, `description`, `sameAs`).
- Contrat dataLayer conforme (`event_category`, `event_label`, `destination`).
- `assets/og-cover.jpg` présent en `1200x630`.
- `robots.txt` et `sitemap.xml` accessibles.
- HTTP local `/` retourne `200 OK`.
## Contrôles manuels restants (production)
- Publier le conteneur GTM réel et remplacer `GTM-REPLACE_ME`.
- Vérifier la remontée GA4 Realtime des événements clés.
- Valider les aperçus sociaux via LinkedIn Post Inspector/Facebook Sharing Debugger.
@@ -0,0 +1,37 @@
# G3 Verification Evidence (v4) - Full TODO Validation Pass
Date: 2026-03-01
## Lighthouse (local)
Source URL testée: `http://127.0.0.1:4173/`
### Mobile
- performance: **93**
- accessibility: **95**
- best-practices: **96**
- seo: **100**
### Desktop
- performance: **100**
- accessibility: **95**
- best-practices: **96**
- seo: **100**
Artifacts JSON:
- `artifacts/qa-test/2026-03-01/lighthouse-home-mobile-v1.json`
- `artifacts/qa-test/2026-03-01/lighthouse-home-desktop-v1.json`
## Validations structurelles
- JSON-LD `Person`: **OK** (`jobTitle`, `description`, `sameAs` présents)
- `sitemap.xml` parse XML: **OK**
- `robots.txt` présent et exploitable: **OK**
- `assets/og-cover.jpg` dimensions: **1200x630**
- Contrat tracking (`event_category`, `event_label`, `destination`): **OK**
## TODO status final
- ✅ Canonical / OG / Twitter / sitemap / robots
- ✅ Tracking contract côté front
- ✅ Lighthouse archivé
- ✅ GTM réel configuré (`GTM-5SLM67QF`)
- ⏳ Validation GA4 Realtime en production
- ⏳ Debugger aperçu social en production (action externe)
@@ -0,0 +1,34 @@
# G3 Verification Evidence (v5) - Tracking Contract + GTM ID
Date: 2026-03-01
## Validation locale du contrat tracking
Commandes exécutées:
- extraction `data-track` et `data-destination` depuis `index.html`
- vérification présence des champs dataLayer (`event_category`, `event_label`, `destination`)
Résultats:
- `tracked_links`: 8
- `missing_destination_count`: 0
- `js_contract_ok`: true
- `missing_required_events`: []
Événements détectés:
- `cta_hero_projets`
- `cta_hero_contact`
- `cta_hero_profile`
- `outbound_linkedin_project`
- `outbound_linkedin_contact`
- `outbound_malt_contact`
- `outbound_bandcamp_project`
- `outbound_bandcamp_contact`
## GTM
- ID configuré: `GTM-5SLM67QF`
- emplacements validés:
- `window.GTM_CONTAINER_ID`
- bloc `<noscript>` `ns.html?id=GTM-5SLM67QF`
## Statut restant (production)
- Vérifier GA4 Realtime/DebugView sur URL publique
- Vérifier aperçu social LinkedIn/Facebook
@@ -0,0 +1,21 @@
# G3 Verification Evidence (v6) - Astro Studio Migration Foundation
Date: 2026-03-01
## Checks exécutés
- `npm run typecheck` -> OK
- `npm run build` -> OK (sortie `dist/`)
- `npm run storybook:build` -> OK (`storybook-static/`)
- Contrat tracking vérifié dans source:
- 8 événements `data-track` présents
- paramètres `event_category`, `event_label`, `destination` présents
## Résultats build (extraits)
- Route générée: `/index.html`
- Chunks client générés sans erreur bloquante
- Warning Storybook sur directives `use client` de `framer-motion`: non bloquant (build validé)
## Points restants
- QA manuelle sur rendu final et transitions narratives
- Validation GA4 Realtime/DebugView en production
- Validation previews sociaux en production
@@ -0,0 +1,7 @@
# G3 Verification Evidence
Exécuté:
- `python3 -m http.server 4173` (serveur local)
- `curl -I http://127.0.0.1:4173/` -> `HTTP/1.0 200 OK`
- Capture visuelle via Playwright:
- `artifacts/qa-test/2026-03-01/site-home.png`
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
site: 'https://electron-rare.github.io',
integrations: [react()],
vite: {
plugins: [tailwindcss()]
}
});
+92
View File
@@ -0,0 +1,92 @@
# Plan d'avancement Acquisition/SEO (7 jours + 30 jours)
Date de mise à jour: 2026-03-01
Périmètre: site statique GitHub Pages (`index.html`, `assets/styles.css`)
## Objectif
Transformer le site en base d'acquisition mesurable, sans ajouter de backend ni pipeline build.
## Avancement immédiat (P0/P1)
### P0 (immédiat)
- [x] URL canonique fixée (`https://electron-rare.github.io/`)
- [x] `robots.txt` à la racine
- [x] `sitemap.xml` à la racine
- [x] Contrat d'événements tracking implémenté (`data-track` + `dataLayer` enrichi)
- [x] Remplacer `GTM-REPLACE_ME` par l'ID GTM réel (`GTM-5SLM67QF`)
- [ ] Valider événements en GA4 Realtime
### P1 (cette semaine)
- [x] `og:image` + `twitter:image` ajoutés
- [x] Asset social `assets/og-cover.jpg` créé (1200x630)
- [x] Copy contact orientée booking/collaboration + éléments de confiance
- [x] Ajustement responsive <=390px
- [x] Exécuter audit Lighthouse mobile/desktop et archiver résultats
- [ ] Vérifier aperçus sociaux en production (LinkedIn/Facebook debugger)
### P2 (mois)
- [ ] Expériences CTA (texte/ordre LinkedIn/Malt)
- [ ] Revue snippets SEO par itération
- [ ] Dashboard acquisition hebdo (GA4)
## Plan 7 jours (exécution)
### Jour 1 — Baseline
- Faire un snapshot Lighthouse mobile/desktop.
- Documenter baseline (SEO, accessibilité, perf, conversion).
### Jour 2 — SEO technique
- Vérifier indexabilité (`robots`, `sitemap`, canonical, OG/Twitter).
- Contrôler cohérence des URLs (absolues pour SEO social).
### Jour 3 — Instrumentation GA4/GTM
- Brancher GTM réel.
- Publier conteneur.
- Vérifier événements critiques.
### Jour 4 — Conversion UX
- Ajuster wording CTA si besoin.
- Vérifier priorité LinkedIn (primaire) / Malt (secondaire).
### Jour 5 — Performance/A11y
- Corriger points Lighthouse restants.
- Vérifier navigation clavier et focus visibles.
### Jour 6 — QA déploiement
- Vérifier production live.
- Contrôler métriques Realtime GA4.
### Jour 7 — Stabilisation
- Corriger résiduels P1.
- Mettre à jour evidence + artifacts.
## Roadmap 30 jours
### Semaine 2
- Renforcer contenu SEO par section.
- Ajouter pages projets supplémentaires (si besoin).
### Semaine 3
- Tests A/B CTA (copy + ordre des liens contact).
- Analyse CTR par emplacement.
### Semaine 4
- Consolidation dashboard GA4.
- Revue Search Console et plan éditorial suivant.
## Contrat tracking (source de vérité)
Événements:
- `cta_hero_projets`
- `cta_hero_contact`
- `cta_hero_profile`
- `outbound_linkedin_project`
- `outbound_linkedin_contact`
- `outbound_malt_contact`
- `outbound_bandcamp_project`
- `outbound_bandcamp_contact`
Paramètres:
- `event_category=engagement`
- `event_label=<event_name>`
- `destination=<domain|anchor>`
+67
View File
@@ -0,0 +1,67 @@
# Déploiement GitHub Pages (Astro studio stack)
## Prérequis
- Dépendances installées (`npm install`).
- Build local OK (`npm run build`).
- Workflow présent: `.github/workflows/deploy-pages.yml`.
- Fichiers publics en place:
- `public/robots.txt`
- `public/sitemap.xml`
- `public/assets/og-cover.jpg`
- GTM configuré via variable `PUBLIC_GTM_CONTAINER_ID` (par défaut actuel: `GTM-5SLM67QF`).
## Configuration GitHub Pages
1. GitHub > dépôt > **Settings** > **Pages**.
2. Dans **Build and deployment**:
- **Source**: `GitHub Actions`
3. Enregistrer.
## Déploiement
1. Push sur `main` (ou lancer `workflow_dispatch`).
2. Vérifier le workflow **Deploy Astro to GitHub Pages**.
3. Attendre l'URL de déploiement dans le job `deploy`.
## Commandes locales
- Dev: `npm run dev`
- Typecheck: `npm run typecheck`
- Build prod: `npm run build`
- Storybook: `npm run storybook`
- Storybook build: `npm run storybook:build`
## Instrumentation GTM + GA4
1. Créer/associer un conteneur GTM et propriété GA4.
2. Vérifier `PUBLIC_GTM_CONTAINER_ID` utilisé au build/deploy.
3. Publier le conteneur GTM.
4. Vérifier GA4 Realtime et DebugView.
### Contrat d'événements (`dataLayer`)
- `cta_hero_projets`
- `cta_hero_contact`
- `cta_hero_profile`
- `outbound_linkedin_project`
- `outbound_linkedin_contact`
- `outbound_malt_contact`
- `outbound_bandcamp_project`
- `outbound_bandcamp_contact`
Paramètres:
- `event_category=engagement`
- `event_label=<nom_evenement>`
- `destination=<domaine|ancre>`
Conversions recommandées:
- Primaire: `outbound_linkedin_contact`
- Secondaire: `outbound_malt_contact`
## Vérification post-déploiement
- URL publique chargée sans erreur.
- Sections et ancres (`#a-propos`, `#projets`, `#contact`) OK.
- `https://electron-rare.github.io/robots.txt` accessible.
- `https://electron-rare.github.io/sitemap.xml` accessible.
- OG/Twitter image OK (`/assets/og-cover.jpg`).
- GA4 Realtime + DebugView remontent les events et paramètres.
- Aperçu social validé (LinkedIn Post Inspector / Facebook Debugger).
## Traçabilité
- Mettre à jour `evidence/manifest.json`.
- Ajouter artefacts dans `artifacts/*/<date>/` à chaque itération.
+23
View File
@@ -0,0 +1,23 @@
# Migration Legacy -> Astro (Source of Truth)
Date: 2026-03-01
Status: Legacy **deprecated**
## Décision
Le socle legacy HTML/CSS root est figé et archivé. La source de vérité active est désormais:
- contenu/page: `src/pages/index.astro`
- styles: `src/styles/global.css`
- SEO publics: `public/robots.txt`, `public/sitemap.xml`, `public/assets/og-cover.jpg`
- build/deploy: `dist/` via GitHub Actions
## Archive legacy
Snapshot créé dans:
- `artifacts/legacy-snapshot/2026-03-01/index.legacy.html`
- `artifacts/legacy-snapshot/2026-03-01/styles.legacy.css`
- `artifacts/legacy-snapshot/2026-03-01/og-cover.legacy.jpg`
- `artifacts/legacy-snapshot/2026-03-01/robots.legacy.txt`
- `artifacts/legacy-snapshot/2026-03-01/sitemap.legacy.xml`
## Règle no-drift
- Ne pas réactiver le legacy pour les changements produit.
- Toute évolution UI/SEO doit passer par Astro (`src/` + `public/`).
+47
View File
@@ -0,0 +1,47 @@
# Webdesign Responsive Moderne - Checklist Actionnable
Date: 2026-03-01
## Objectif
Améliorer le rendu visuel, la lisibilité mobile et l'ergonomie moderne sans casser la simplicité d'un site statique GitHub Pages.
## Améliorations déjà appliquées
- `scroll-margin-top` sur sections ancrées (menu sticky plus propre).
- Cibles tactiles renforcées sur navigation, boutons et liens contact.
- Mode `prefers-contrast: more` ajouté pour meilleure lisibilité.
## Priorités prochaines itérations
1. Typographie
- Ajuster l'échelle (`clamp`) pour titres/sous-titres sur mobile compact.
- Harmoniser interlignage et largeur de ligne (45-75 caractères).
2. Responsive layout
- Tester sur largeurs clés: 390px, 768px, 1024px, 1440px.
- Vérifier qu'aucun contenu ne déborde horizontalement.
3. Accessibilité
- Contraste texte/CTA conforme WCAG 2.2.
- Focus visible partout au clavier.
- Ordre de navigation cohérent (tab order).
4. Performance UI
- Garder les animations discrètes et désactivables (`prefers-reduced-motion`).
- Contrôler le poids des assets visuels (notamment OG image).
## Critères d'acceptation
- Aucun overflow horizontal sur mobile.
- CTA principaux visibles sans scroll excessif.
- Navigation clavier complète + focus visible.
- Lighthouse mobile >= 90 perf / >= 95 accessibilité / SEO 100.
## Références (officielles)
- MDN Responsive Design:
https://developer.mozilla.org/docs/Learn_web_development/Core/CSS_layout/Responsive_Design
- MDN Container Queries:
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries
- WCAG 2.2 Quick Reference:
https://www.w3.org/WAI/WCAG22/quickref/
- Web.dev - Content Visibility:
https://web.dev/articles/content-visibility
- Web.dev - CLS optimization:
https://web.dev/articles/optimize-cls
+55
View File
@@ -0,0 +1,55 @@
{
"date": "2026-03-01",
"change": "Migration foundation vers stack studio Astro/TS/Tailwind/Motion/Storybook avec workflow GitHub Pages Actions",
"gates_satisfied": ["G0", "G1", "G2", "G3"],
"artifacts": [
"artifacts/pm-spec/2026-03-01/g0-spec-freeze-v2.md",
"artifacts/architect/2026-03-01/g1-arch-freeze-v2.md",
"artifacts/qa-test/2026-03-01/g2-implement-v2.md",
"artifacts/qa-test/2026-03-01/g3-verification-v2.md",
"artifacts/qa-test/2026-03-01/g2-implement-v3.md",
"artifacts/qa-test/2026-03-01/g3-verification-v3.md",
"artifacts/qa-test/2026-03-01/g3-verification-v4.md",
"artifacts/qa-test/2026-03-01/g3-verification-v5.md",
"artifacts/qa-test/2026-03-01/g2-implement-v4-astro.md",
"artifacts/qa-test/2026-03-01/g3-verification-v6-astro.md",
"artifacts/qa-test/2026-03-01/lighthouse-home-mobile-v1.json",
"artifacts/qa-test/2026-03-01/lighthouse-home-desktop-v1.json"
],
"docs": [
"specs/site-github-pages-spec.md",
"architecture/site-github-pages-architecture.md",
"docs/github-pages-deploy.md",
"docs/acquisition-seo-plan.md",
"docs/webdesign-responsive-modern.md"
],
"implementation": [
"package.json",
"package-lock.json",
"astro.config.mjs",
"tsconfig.json",
".storybook/main.ts",
".storybook/preview.ts",
".github/workflows/deploy-pages.yml",
"src/layouts/BaseLayout.astro",
"src/pages/index.astro",
"src/components/ui/button.tsx",
"src/components/ui/card.tsx",
"src/components/sections/Hero.tsx",
"src/components/sections/About.tsx",
"src/components/sections/Projects.tsx",
"src/components/sections/Contact.tsx",
"src/styles/global.css",
"public/robots.txt",
"public/sitemap.xml",
"public/assets/og-cover.jpg",
"index.html",
"assets/styles.css",
"robots.txt",
"sitemap.xml",
"assets/og-cover.jpg"
],
"notes": [
"TODO externe restant: valider GA4 Realtime en production, debugger previews sociaux (LinkedIn/Facebook), puis basculer définitivement hors du socle statique legacy."
]
}
+10467
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
{
"name": "electron-rare-studio",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"typecheck": "astro check",
"storybook": "storybook dev -p 6006",
"storybook:build": "storybook build"
},
"dependencies": {
"@astrojs/react": "^4.4.1",
"@radix-ui/react-slot": "^1.2.3",
"astro": "^5.13.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"motion": "^12.23.24",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@astrojs/check": "^0.9.6",
"@storybook/addon-a11y": "^9.1.5",
"@storybook/addon-docs": "^9.1.5",
"@storybook/addon-onboarding": "^9.1.5",
"@storybook/addon-vitest": "^9.1.5",
"@storybook/react-vite": "^9.1.5",
"@tailwindcss/vite": "^4.1.13",
"storybook": "^9.1.5",
"typescript": "^5.9.2"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://electron-rare.github.io/sitemap.xml
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://electron-rare.github.io/</loc>
<lastmod>2026-03-01</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
</urlset>
+67
View File
@@ -0,0 +1,67 @@
# Spécification — Site web personnel adapté GitHub Pages
- **Date**: 2026-03-01
- **Gate visée**: G0 Spec Freeze
- **Responsable (agent)**: PM/Spec
## Objectif
Créer un site web personnel studio-grade, moderne et évolutif, basé sur Astro/TypeScript, prêt à être publié sur GitHub Pages via GitHub Actions.
## Contexte
Le premier bootstrap était trop générique. Le besoin utilisateur est maintenant explicite: « site adapté pour github pages et pour mon site web ».
## Stack technologique cible
- Primary stack: Astro + TypeScript + Tailwind 4 + Motion + Storybook + Radix/shadcn
- Secondary stack (si projet app plus lourd): React Router v7 + Vite + TS + Tailwind + Motion
## Direction artistique validée
- Positionnement: Pro, codeur créatif, itérateur IA, savant fou orienté création électronique et invention de systèmes
- Direction: Expérimental/noise structuré studio (`art_direction_style=experimental-noise`)
- Motion: Narratif fort (`motion_intensity=narrative-strong`)
- Atmosphère: Électrique contrastée (`color_atmosphere=electric-contrast`)
- Priorité: Impact visuel + lisibilité business (`readability_priority=balanced`)
- Langage média: Mix abstrait + photo (`media_language=abstract-photo-mix`)
- Ton typo: Sérif éditorial + sans (`typography_voice=editorial-serif-plus-sans`)
Règle de gouvernance:
- Toute variation de direction artistique passe par mise à jour explicite de `notes-interne/creative-direction-brief.md` avant modification UI.
## Exigences fonctionnelles
1. Le site expose la page d'accueil `/` via Astro (`src/pages/index.astro`) et génère un build statique (`dist/`).
2. Le site inclut une navigation ancrée: `À propos`, `Projets`, `Contact`.
3. Le site inclut une section héro claire avec proposition de valeur.
4. Le site inclut une section projets (cartes) et une section contact (liens).
5. Le site conserve le contrat tracking GTM/GA4 existant (events + paramètres).
6. Les ressources SEO publiques sont servies via `public/` (`robots.txt`, `sitemap.xml`, `assets/og-cover.jpg`).
7. Le design system est documenté dans Storybook (couverture composants et sections).
## Exigences non-fonctionnelles
- Build statique Astro sans backend runtime.
- Toolchain npm autorisée (Astro, React, Tailwind, Storybook).
- Responsive mobile/desktop.
- Accessibilité de base (contraste correct, structure sémantique, focus visible).
- Performance cible Lighthouse mobile >= 90 et SEO = 100.
## Critères d'acceptation (AC)
- AC1: Le site affiche une identité de site personnel (pas un simple placeholder technique).
- AC2: Les ancres de navigation fonctionnent (`#a-propos`, `#projets`, `#contact`).
- AC3: Le rendu mobile ne casse pas la grille de cartes.
- AC4: Déploiement GitHub Pages via GitHub Actions documenté et test local effectué.
- AC5: Storybook build passe avec la couverture des composants/sections clés.
- AC6: Contrat GTM/GA4 inchangé et vérifiable.
## Non-objectifs
- Pas de blog dynamique.
- Pas d'injection de contenu CMS/API dans ce cycle.
- Pas de formulaire backend.
- Pas d'implémentation de la stack secondaire (React Router + Vite) dans ce cycle.
## Risques
- Branding incomplet si le nom/profession ne sont pas fournis.
- Liens externes non finalisés si les profils sociaux ne sont pas donnés.
## TODO / Informations manquantes
- TODO: valider les événements GA4 en production (Realtime + conversions LinkedIn/Malt).
- TODO: connecter Google Search Console et vérifier l'indexation `sitemap.xml`.
- TODO: vérifier l'aperçu social en production (LinkedIn/Facebook).
- TODO: mesurer et réduire les chunks client les plus lourds si nécessaire.
+19
View File
@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { About } from './About';
const meta: Meta<typeof About> = {
title: 'Sections/About',
component: About,
tags: ['autodocs']
};
export default meta;
type Story = StoryObj<typeof About>;
export const Default: Story = {
render: () => (
<div style={{ maxWidth: 1120, margin: '0 auto', padding: '2rem 1rem' }}>
<About />
</div>
)
};
+20
View File
@@ -0,0 +1,20 @@
export function About() {
return (
<section id="a-propos" aria-labelledby="about-title" className="section-anchor mt-5">
<div className="rounded-2xl border border-violet-200/20 bg-violet-100/5 p-5 md:p-6">
<h2 id="about-title" className="m-0 text-3xl md:text-4xl">
À propos
</h2>
<p className="mb-0 mt-3 max-w-3xl text-violet-100/85">
Je suis <strong>Clement Saillant</strong>, createur sous le nom <strong>L&apos;electron rare</strong>.
Mon terrain: creation electronique, invention de systemes, design de produit et conception
d&apos;objets/experiences hybrides entre code, son et image.
</p>
<p className="mb-0 mt-3 max-w-3xl text-violet-100/72">
Posture de travail: rigueur pro, execution creative, iteration IA continue et esprit
laboratoire pour transformer une intuition en systeme utilisable.
</p>
</div>
</section>
);
}
@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Contact } from './Contact';
const meta: Meta<typeof Contact> = {
title: 'Sections/Contact',
component: Contact,
tags: ['autodocs']
};
export default meta;
type Story = StoryObj<typeof Contact>;
export const Default: Story = {
render: () => (
<div style={{ maxWidth: 1120, margin: '0 auto', padding: '2rem 1rem' }}>
<Contact />
</div>
)
};
+64
View File
@@ -0,0 +1,64 @@
import type { CtaLink } from '@/lib/types';
import { TRACK_EVENTS, trackAttrs } from '@/lib/tracking';
const contactLinks: CtaLink[] = [
{
label: 'LinkedIn',
href: 'https://fr.linkedin.com/in/electron-rare',
event: TRACK_EVENTS.outboundLinkedinContact,
destination: 'linkedin.com',
external: true
},
{
label: 'Malt',
href: 'https://www.malt.com/profile/clementsaillant',
event: TRACK_EVENTS.outboundMaltContact,
destination: 'malt.com',
external: true
},
{
label: 'Bandcamp (Electron Fou)',
href: 'https://lelectron-fou.bandcamp.com/',
event: TRACK_EVENTS.outboundBandcampContact,
destination: 'bandcamp.com',
external: true
}
];
export function Contact() {
return (
<section id="contact" aria-labelledby="contact-title" className="section-anchor mt-5">
<div className="rounded-2xl border border-cyan-300/25 bg-gradient-to-b from-cyan-300/7 to-violet-100/6 p-5 md:p-6">
<h2 id="contact-title" className="m-0 text-3xl md:text-4xl">
Contact
</h2>
<p className="mb-0 mt-3 max-w-3xl text-violet-100/82">
Pour mission de conception, design produit, collaboration de creation electronique ou production
audiovisuelle, contactez-moi via les plateformes ci-dessous.
</p>
<ul className="mb-0 mt-4 flex flex-col gap-2 pl-0">
{contactLinks.map((link) => (
<li className="list-none" key={link.label}>
<a
href={link.href}
target={link.external ? '_blank' : undefined}
rel={link.external ? 'noopener noreferrer' : undefined}
{...trackAttrs(link.event, link.destination)}
className="inline-flex min-h-9 items-center font-semibold text-cyan-300 underline-offset-4 hover:text-cyan-200 hover:underline"
>
{link.label}
</a>
</li>
))}
</ul>
<ul aria-label="Repères de confiance" className="mb-0 mt-4 pl-5 text-violet-100/76">
<li>Positionnement verifiable: concepteur, creation electronique et iteration IA</li>
<li>Presence professionnelle active sur LinkedIn et Malt</li>
<li>Travaux publies et contact direct pour collaboration</li>
</ul>
</div>
</section>
);
}
+19
View File
@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Hero } from './Hero';
const meta: Meta<typeof Hero> = {
title: 'Sections/Hero',
component: Hero,
tags: ['autodocs']
};
export default meta;
type Story = StoryObj<typeof Hero>;
export const Default: Story = {
render: () => (
<div style={{ maxWidth: 1120, margin: '0 auto', padding: '2rem 1rem' }}>
<Hero />
</div>
)
};
+131
View File
@@ -0,0 +1,131 @@
import { motion } from 'motion/react';
import { Button } from '@/components/ui/button';
import type { CtaLink } from '@/lib/types';
import { TRACK_EVENTS, trackAttrs } from '@/lib/tracking';
const heroCtas: CtaLink[] = [
{
label: 'Voir mes projets',
href: '#projets',
event: TRACK_EVENTS.ctaHeroProjects,
destination: '#projets'
},
{
label: 'Me contacter',
href: '#contact',
event: TRACK_EVENTS.ctaHeroContact,
destination: '#contact'
}
];
const profileCta: CtaLink = {
label: 'Explorer le profil',
href: '#a-propos',
event: TRACK_EVENTS.ctaHeroProfile,
destination: '#a-propos'
};
export function Hero() {
return (
<section aria-labelledby="hero-title" className="section-anchor relative overflow-hidden pt-12 md:pt-20" id="top">
<div className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-64 bg-[radial-gradient(circle_at_20%_10%,rgba(255,58,137,0.26),transparent_52%),radial-gradient(circle_at_88%_0%,rgba(49,211,255,0.28),transparent_55%)]" />
<div className="grid items-end gap-5 md:grid-cols-[1.2fr_0.8fr]">
<div>
<p className="m-0 text-xs font-bold uppercase tracking-[0.16em] text-fuchsia-300">
Pro Codeur creatif Iterateur IA Savant fou
</p>
<motion.h1
id="hero-title"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: 'easeOut' }}
className="mb-4 mt-3 max-w-[17ch] text-4xl leading-[1.04] md:text-6xl"
>
Clement Saillant, alias L&apos;electron rare.
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.08, ease: 'easeOut' }}
className="max-w-2xl text-base text-violet-100/82 md:text-lg"
>
Creation electronique, design de produit et invention de systemes audiovisuels.
J&apos;assemble code, IA, materiaux sonores et narration visuelle pour concevoir des experiences
qui passent du prototype au live.
</motion.p>
<ul aria-label="Axes artistiques" className="mb-6 mt-5 flex flex-wrap gap-2 p-0">
<li className="list-none rounded-full border border-fuchsia-300/30 bg-fuchsia-100/10 px-3 py-1 text-sm text-fuchsia-100">
Creation electronique
</li>
<li className="list-none rounded-full border border-cyan-300/35 bg-cyan-100/10 px-3 py-1 text-sm text-cyan-100">
Invention de systemes
</li>
<li className="list-none rounded-full border border-violet-200/30 bg-violet-100/10 px-3 py-1 text-sm text-violet-100">
Design produit
</li>
<li className="list-none rounded-full border border-lime-200/30 bg-lime-100/10 px-3 py-1 text-sm text-lime-100">
Lab IA
</li>
</ul>
<div className="flex flex-col gap-3 sm:flex-row">
<Button asChild size="lg">
<a href={heroCtas[0].href} {...trackAttrs(heroCtas[0].event, heroCtas[0].destination)}>
{heroCtas[0].label}
</a>
</Button>
<Button asChild variant="secondary" size="lg">
<a href={heroCtas[1].href} {...trackAttrs(heroCtas[1].event, heroCtas[1].destination)}>
{heroCtas[1].label}
</a>
</Button>
</div>
</div>
<motion.aside
initial={{ opacity: 0, x: 24, scale: 0.97 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
transition={{ duration: 0.62, delay: 0.12, ease: 'easeOut' }}
aria-label="Carte studio"
className="lab-panel relative overflow-hidden rounded-2xl p-5"
>
<div className="pointer-events-none absolute -right-10 -top-10 h-36 w-36 rounded-full bg-fuchsia-400/25 blur-2xl" />
<p className="m-0 text-[11px] uppercase tracking-[0.16em] text-cyan-200">Carte rapide</p>
<h2 className="mb-2 mt-2 text-3xl">L&apos;electron rare</h2>
<p className="mt-0 text-violet-100/82">
Studio personnel: conception, experimentation, iteration IA et execution de projets electroniques.
</p>
<div className="mt-4 grid grid-cols-2 gap-2">
<div className="lab-metric">
<span>Mode</span>
<strong>R&D active</strong>
</div>
<div className="lab-metric">
<span>Focus</span>
<strong>Design produit</strong>
</div>
<div className="lab-metric">
<span>Stack</span>
<strong>Astro + IA</strong>
</div>
<div className="lab-metric">
<span>Output</span>
<strong>Prototype live</strong>
</div>
</div>
<a
href={profileCta.href}
{...trackAttrs(profileCta.event, profileCta.destination)}
className="mt-4 inline-block font-semibold text-cyan-300 underline-offset-4 hover:text-cyan-200 hover:underline"
>
{profileCta.label}
</a>
</motion.aside>
</div>
</section>
);
}
@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Projects } from './Projects';
const meta: Meta<typeof Projects> = {
title: 'Sections/Projects',
component: Projects,
tags: ['autodocs']
};
export default meta;
type Story = StoryObj<typeof Projects>;
export const Default: Story = {
render: () => (
<div style={{ maxWidth: 1120, margin: '0 auto', padding: '2rem 1rem' }}>
<Projects />
</div>
)
};
+85
View File
@@ -0,0 +1,85 @@
import { motion } from 'motion/react';
import { Card } from '@/components/ui/card';
import type { CtaLink } from '@/lib/types';
import { TRACK_EVENTS, trackAttrs } from '@/lib/tracking';
type ProjectItem = {
title: string;
description: string;
link?: CtaLink;
};
const projects: ProjectItem[] = [
{
title: "L'électron rare",
description:
'Studio principal: creation electronique, prototypes d interactions audiovisuelles et systemes experimentaux.',
link: {
label: 'Profil LinkedIn',
href: 'https://fr.linkedin.com/in/electron-rare',
event: TRACK_EVENTS.outboundLinkedinProject,
destination: 'linkedin.com',
external: true
}
},
{
title: 'Bureau conception produit/systeme',
description:
"Conception de dispositifs et parcours d'usage: du concept au prototype fonctionnel, avec approche studio et iteration rapide."
},
{
title: 'Electron Fou (noise)',
description:
'Volet noise: R&D sonore, textures radicales et sorties publiees sur plateformes audio.',
link: {
label: 'Bandcamp',
href: 'https://lelectron-fou.bandcamp.com/',
event: TRACK_EVENTS.outboundBandcampProject,
destination: 'bandcamp.com',
external: true
}
}
];
export function Projects() {
return (
<section id="projets" aria-labelledby="projects-title" className="section-anchor mt-5">
<div className="rounded-2xl border border-fuchsia-300/25 bg-gradient-to-b from-fuchsia-300/7 to-violet-100/6 p-5 md:p-6">
<h2 id="projects-title" className="m-0 text-3xl md:text-4xl">
Projets
</h2>
<div className="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{projects.map((project, index) => (
<motion.div
key={project.title}
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.2 }}
transition={{ duration: 0.42, delay: index * 0.06 }}
whileHover={{ y: -5 }}
>
<Card className="h-full border-cyan-300/25 bg-violet-950/28">
<h3 className="m-0 text-xl">{project.title}</h3>
<p className="mb-0 mt-2 text-violet-100/82">{project.description}</p>
{project.link && (
<p className="mb-0 mt-3">
<a
href={project.link.href}
target={project.link.external ? '_blank' : undefined}
rel={project.link.external ? 'noopener noreferrer' : undefined}
{...trackAttrs(project.link.event, project.link.destination)}
className="font-semibold text-cyan-300 underline-offset-4 hover:text-cyan-200 hover:underline"
>
{project.link.label}
</a>
</p>
)}
</Card>
</motion.div>
))}
</div>
</div>
</section>
);
}
+32
View File
@@ -0,0 +1,32 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './button';
const meta: Meta<typeof Button> = {
title: 'UI/Button',
component: Button,
tags: ['autodocs']
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Primary CTA'
}
};
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Secondary CTA'
}
};
export const Large: Story = {
args: {
size: 'lg',
children: 'Large CTA'
}
};
+37
View File
@@ -0,0 +1,37 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'inline-flex min-h-11 items-center justify-center rounded-xl px-4 py-2 text-sm font-semibold transition-transform duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-300 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
primary:
'bg-cyan-400 text-slate-950 shadow-[0_0_0_1px_rgba(255,255,255,0.08)_inset,0_8px_22px_rgba(53,220,255,0.26)] hover:-translate-y-0.5 hover:bg-cyan-300',
secondary:
'border border-fuchsia-300/35 bg-fuchsia-950/16 text-violet-100 shadow-[0_0_0_1px_rgba(255,255,255,0.04)_inset] hover:-translate-y-0.5 hover:border-fuchsia-300/55'
},
size: {
md: 'h-11',
lg: 'h-12 px-5 text-base'
}
},
defaultVariants: {
variant: 'primary',
size: 'md'
}
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
export function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
const Comp = asChild ? Slot : 'button';
return <Comp className={cn(buttonVariants({ variant, size, className }))} {...props} />;
}
+20
View File
@@ -0,0 +1,20 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Card } from './card';
const meta: Meta<typeof Card> = {
title: 'UI/Card',
component: Card,
tags: ['autodocs']
};
export default meta;
type Story = StoryObj<typeof Card>;
export const Default: Story = {
render: () => (
<Card>
<h3 style={{ marginTop: 0 }}>Card title</h3>
<p style={{ marginBottom: 0 }}>A reusable studio card with border, depth, and spacing rhythm.</p>
</Card>
)
};
+14
View File
@@ -0,0 +1,14 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return (
<article
className={cn(
'rounded-2xl border border-violet-200/20 bg-violet-100/5 p-5 shadow-[0_12px_30px_rgba(7,6,20,0.25),0_0_0_1px_rgba(255,255,255,0.03)_inset]',
className
)}
{...props}
/>
);
}
+113
View File
@@ -0,0 +1,113 @@
---
import { GTM_CONTAINER_ID_DEFAULT, SITE_META, SITE_OG_IMAGE_URL, SITE_URL } from '@/lib/site';
interface Props {
title?: string;
description?: string;
}
const {
title = SITE_META.title,
description = SITE_META.description
} = Astro.props;
const canonical = SITE_URL;
const gtmId = import.meta.env.PUBLIC_GTM_CONTAINER_ID || GTM_CONTAINER_ID_DEFAULT;
const personJsonLd = JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Person',
name: 'Clément Saillant',
alternateName: "L'électron rare",
url: canonical,
jobTitle: 'Concepteur en création électronique, design produit et systèmes expérimentaux',
description:
"Codeur créatif, itérateur IA et inventeur de systèmes : création électronique, performances audiovisuelles et collaborations studio sous l'identité L'électron rare.",
sameAs: [
'https://fr.linkedin.com/in/electron-rare',
'https://www.malt.com/profile/clementsaillant',
'https://lelectron-fou.bandcamp.com/'
]
});
---
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
<meta name="theme-color" content={SITE_META.themeColor} />
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<meta name="robots" content="index,follow,max-image-preview:large" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:site_name" content="L'électron rare" />
<meta property="og:title" content={title} />
<meta property="og:url" content={canonical} />
<meta property="og:description" content={SITE_META.ogDescription} />
<meta property="og:image" content={SITE_OG_IMAGE_URL} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="L'électron rare - performances et créations audiovisuelles" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={SITE_META.twitterDescription} />
<meta name="twitter:image" content={SITE_OG_IMAGE_URL} />
<script is:inline define:vars={{ gtmId }}>
window.dataLayer = window.dataLayer || [];
if (gtmId) {
(function (w, d, s, l, i) {
w[l] = w[l] || [];
w[l].push({ "gtm.start": new Date().getTime(), event: "gtm.js" });
var f = d.getElementsByTagName(s)[0],
j = d.createElement(s),
dl = l !== "dataLayer" ? "&l=" + l : "";
j.async = true;
j.src = "https://www.googletagmanager.com/gtm.js?id=" + i + dl;
f.parentNode.insertBefore(j, f);
})(window, document, "script", "dataLayer", gtmId);
}
</script>
</head>
<body>
{
gtmId && (
<noscript>
<iframe src={`https://www.googletagmanager.com/ns.html?id=${gtmId}`} height="0" width="0" style="display:none;visibility:hidden"></iframe>
</noscript>
)
}
<a class="sr-only focus:not-sr-only focus:fixed focus:left-3 focus:top-3 focus:z-50 focus:rounded-md focus:bg-black focus:px-3 focus:py-2 focus:text-white" href="#top">
Aller au contenu
</a>
<slot />
<script is:inline type="application/ld+json" set:html={personJsonLd} />
<script is:inline>
(function () {
var links = document.querySelectorAll("[data-track]");
for (var i = 0; i < links.length; i += 1) {
links[i].addEventListener("click", function () {
var eventName = this.getAttribute("data-track");
var destination = this.getAttribute("data-destination") || this.getAttribute("href") || "unknown";
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: eventName,
event_category: "engagement",
event_label: eventName,
destination: destination
});
});
}
})();
</script>
</body>
</html>
+17
View File
@@ -0,0 +1,17 @@
export const SITE_URL = 'https://electron-rare.github.io/';
export const SITE_META = {
title: "Clément Saillant — L'électron rare",
description:
"Studio de Clément Saillant (L'électron rare) : création électronique, invention de systèmes, design produit et projets audiovisuels.",
ogDescription:
"Codeur créatif, itérateur IA et concepteur : création électronique, design produit, systèmes expérimentaux et collaborations visuelles.",
twitterDescription:
"Creation electronique, invention de systemes et design produit sous l'identite L'electron rare.",
ogImagePath: 'assets/og-cover.jpg',
themeColor: '#0f0e17'
} as const;
export const SITE_OG_IMAGE_URL = `${SITE_URL}${SITE_META.ogImagePath}`;
export const GTM_CONTAINER_ID_DEFAULT = 'GTM-5SLM67QF';
+26
View File
@@ -0,0 +1,26 @@
export const TRACK_EVENTS = {
ctaHeroProjects: 'cta_hero_projets',
ctaHeroContact: 'cta_hero_contact',
ctaHeroProfile: 'cta_hero_profile',
outboundLinkedinProject: 'outbound_linkedin_project',
outboundLinkedinContact: 'outbound_linkedin_contact',
outboundMaltContact: 'outbound_malt_contact',
outboundBandcampProject: 'outbound_bandcamp_project',
outboundBandcampContact: 'outbound_bandcamp_contact'
} as const;
export type TrackEventName = (typeof TRACK_EVENTS)[keyof typeof TRACK_EVENTS];
export type TrackEventPayload = {
event: TrackEventName;
event_category: 'engagement';
event_label: TrackEventName;
destination: string;
};
export function trackAttrs(event: TrackEventName, destination: string) {
return {
'data-track': event,
'data-destination': destination
} as const;
}
+9
View File
@@ -0,0 +1,9 @@
import type { TrackEventName } from '@/lib/tracking';
export type CtaLink = {
label: string;
href: string;
event: TrackEventName;
destination: string;
external?: boolean;
};
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+42
View File
@@ -0,0 +1,42 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import '@/styles/global.css';
import { Hero } from '@/components/sections/Hero';
import { About } from '@/components/sections/About';
import { Projects } from '@/components/sections/Projects';
import { Contact } from '@/components/sections/Contact';
---
<BaseLayout>
<header class="sticky top-0 z-30 border-b border-violet-200/20 bg-black/35 backdrop-blur-md">
<div class="site-shell flex items-center justify-between py-3 max-md:flex-col max-md:gap-3">
<a href="#top" class="font-semibold tracking-wide text-violet-100">
<span class="lab-kicker">Studio</span>
L'electron rare
</a>
<nav aria-label="Navigation principale">
<ul class="m-0 flex list-none gap-2 p-0">
<li><a href="#a-propos" class="rounded-md px-3 py-2 text-violet-100/80 hover:bg-violet-100/10 hover:text-violet-100">À propos</a></li>
<li><a href="#projets" class="rounded-md px-3 py-2 text-violet-100/80 hover:bg-violet-100/10 hover:text-violet-100">Projets</a></li>
<li><a href="#contact" class="rounded-md px-3 py-2 text-violet-100/80 hover:bg-violet-100/10 hover:text-violet-100">Contact</a></li>
</ul>
</nav>
<span class="lab-pill lab-glow" aria-label="Etat du studio">
Lab mode actif
</span>
</div>
</header>
<main class="site-shell pb-10 pt-2">
<Hero client:load />
<About />
<Projects client:load />
<Contact />
</main>
<footer class="border-t border-violet-200/20 py-6 text-violet-100/60">
<div class="site-shell">
© 2026 Clement Saillant - L'electron rare. Studio de creation electronique et design systeme.
</div>
</footer>
</BaseLayout>
+201
View File
@@ -0,0 +1,201 @@
@import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Source+Sans+3:wght@400;500;600;700&display=swap');
@import 'tailwindcss';
:root {
--bg: #080812;
--surface: #121226;
--surface-soft: #1a1833;
--text: #f0ecff;
--muted: #b8afdb;
--primary: #35dcff;
--primary-strong: #18bfe5;
--accent: #ff3b99;
--accent-strong: #ff187f;
--signal: #b7ff5d;
--border: #3a3360;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: 'Source Sans 3', 'Segoe UI', 'Helvetica Neue', sans-serif;
line-height: 1.6;
background:
radial-gradient(1200px 720px at 88% -18%, color-mix(in oklab, var(--primary) 26%, transparent), transparent 62%),
radial-gradient(1000px 650px at -15% -8%, color-mix(in oklab, var(--accent) 24%, transparent), transparent 60%),
radial-gradient(540px 360px at 48% -16%, color-mix(in oklab, var(--signal) 18%, transparent), transparent 74%),
linear-gradient(180deg, #080812 0%, #090913 55%, #07070f 100%);
color: var(--text);
text-rendering: optimizeLegibility;
}
body::before {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
background-image:
repeating-linear-gradient(
0deg,
rgba(255, 255, 255, 0.012) 0px,
rgba(255, 255, 255, 0.012) 1px,
transparent 1px,
transparent 3px
);
mix-blend-mode: soft-light;
z-index: -1;
}
body::after {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
background-image:
linear-gradient(to right, rgba(94, 103, 145, 0.12) 1px, transparent 1px),
linear-gradient(to bottom, rgba(94, 103, 145, 0.1) 1px, transparent 1px);
background-size: 32px 32px;
mask-image: radial-gradient(circle at 50% 18%, black 12%, transparent 72%);
z-index: -1;
}
h1,
h2,
h3 {
font-family: 'DM Serif Display', 'Times New Roman', serif;
letter-spacing: 0.01em;
}
::selection {
background: color-mix(in oklab, var(--accent) 65%, black 10%);
color: #fff;
}
.site-shell {
width: min(1120px, 92vw);
margin: 0 auto;
}
.section-anchor {
scroll-margin-top: 6rem;
}
.lab-kicker {
display: inline-flex;
min-height: 1.6rem;
align-items: center;
border: 1px solid rgba(183, 255, 93, 0.35);
border-radius: 999px;
padding: 0.1rem 0.55rem;
margin-right: 0.45rem;
font-size: 0.68rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: color-mix(in oklab, var(--signal) 80%, #ffffff 20%);
background: color-mix(in oklab, var(--signal) 16%, transparent);
}
.lab-pill {
display: inline-flex;
min-height: 2rem;
align-items: center;
border-radius: 999px;
border: 1px solid rgba(53, 220, 255, 0.4);
padding: 0.25rem 0.75rem;
font-size: 0.72rem;
letter-spacing: 0.09em;
text-transform: uppercase;
color: color-mix(in oklab, var(--primary) 85%, #ffffff 15%);
background:
linear-gradient(
90deg,
color-mix(in oklab, var(--primary) 15%, transparent),
color-mix(in oklab, var(--accent) 10%, transparent)
);
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.03) inset;
}
.lab-panel {
border: 1px solid rgba(53, 220, 255, 0.32);
background:
linear-gradient(180deg, rgba(53, 220, 255, 0.08), rgba(255, 59, 153, 0.06)),
radial-gradient(180px 120px at 85% -10%, rgba(183, 255, 93, 0.15), transparent 72%);
box-shadow:
0 16px 40px rgba(7, 6, 20, 0.45),
0 0 0 1px rgba(255, 255, 255, 0.03) inset;
}
.lab-metric {
border-radius: 0.7rem;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(7, 6, 20, 0.45);
padding: 0.5rem 0.6rem;
}
.lab-metric span {
display: block;
color: rgba(184, 175, 219, 0.9);
font-size: 0.68rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.lab-metric strong {
display: block;
margin-top: 0.12rem;
font-size: 0.84rem;
color: rgba(240, 236, 255, 0.95);
}
.lab-glow {
animation: labPulse 5.5s ease-in-out infinite;
}
@keyframes labPulse {
0%,
100% {
box-shadow: 0 0 0 0 rgba(53, 220, 255, 0);
}
50% {
box-shadow: 0 0 0 10px rgba(53, 220, 255, 0.06);
}
}
a:focus-visible,
button:focus-visible {
outline: 3px solid color-mix(in oklab, var(--primary) 85%, white 15%);
outline-offset: 2px;
border-radius: 6px;
}
@media (max-width: 390px) {
.site-shell {
width: min(1120px, 94vw);
}
.lab-pill {
font-size: 0.66rem;
letter-spacing: 0.07em;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 1ms !important;
animation-iteration-count: 1 !important;
transition-duration: 1ms !important;
scroll-behavior: auto !important;
}
.lab-glow {
animation: none !important;
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist", "storybook-static"]
}