feat: apply pro creative-lab art direction and validate astro pipeline
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
# Internal private notes
|
||||
notes-interne/
|
||||
|
||||
# Node / tooling
|
||||
node_modules/
|
||||
dist/
|
||||
storybook-static/
|
||||
.astro/
|
||||
.DS_Store
|
||||
@@ -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;
|
||||
@@ -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
@@ -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()]
|
||||
}
|
||||
});
|
||||
@@ -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>`
|
||||
@@ -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.
|
||||
@@ -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/`).
|
||||
@@ -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
|
||||
@@ -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."
|
||||
]
|
||||
}
|
||||
Generated
+10467
File diff suppressed because it is too large
Load Diff
@@ -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 |
@@ -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,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.
|
||||
@@ -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>
|
||||
)
|
||||
};
|
||||
@@ -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'electron rare</strong>.
|
||||
Mon terrain: creation electronique, invention de systemes, design de produit et conception
|
||||
d'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>
|
||||
)
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
};
|
||||
@@ -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'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'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'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>
|
||||
)
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
};
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { TrackEventName } from '@/lib/tracking';
|
||||
|
||||
export type CtaLink = {
|
||||
label: string;
|
||||
href: string;
|
||||
event: TrackEventName;
|
||||
destination: string;
|
||||
external?: boolean;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "storybook-static"]
|
||||
}
|
||||
Reference in New Issue
Block a user