diff --git a/AGENTS.md b/AGENTS.md
index f26bbe9..d0bdc3d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,7 +2,7 @@
Consignes pour les agents de code (et les humains) qui travaillent sur ce dépôt. Le
fonctionnement du site pour son propriétaire est dans [README.md](README.md) ; les thèmes
-saisonniers dans [THEMES.md](THEMES.md).
+(saisons, fêtes, événements) dans [THEMES.md](THEMES.md).
## Le projet
@@ -42,10 +42,11 @@ ordinateur **et** à ~390 px de large (pas de défilement horizontal).
| `src/pages/` | Routes seulement : `/`, `/en/`, `/projets/[slug]/`, `/en/projets/[slug]/`, 404. Elles rendent une vue. |
| `src/components/sections/` | Une section de l'accueil par fichier. |
| `src/layouts/Base.astro` | `
`, scripts d'avant-rendu (thème, habillage), haut de page, pied de page. |
-| `src/scripts/ui.ts` | Tout le JS navigateur : thème, menu des langues, replis « Voir plus », bulles, liens externes. |
+| `src/scripts/ui.ts` | Tout le JS navigateur : thème, menu des langues, replis « Voir plus », bulles, liens externes, feux d'artifice du Nouvel An. |
| `src/styles/tokens.css` | Jetons de design (couleurs clair/sombre, verre, rayons). |
| `src/styles/global.css` | Mise en page et composants. |
-| `src/styles/habillages/`, `src/components/habillages/` | Thèmes saisonniers (Halloween…). |
+| `src/styles/habillages/`, `src/components/habillages/` | Habillages (saisons, fêtes) : un CSS et un composant chacun, `commun.css`, `Etoiles` et `Flocons` partagés. |
+| `src/lib/decor.ts` | Positions pseudo-aléatoires (graine fixe) des éléments de décor. |
| `public/logos/` | Logos d'organisations, carrés (400 × 400 de préférence). |
## Conventions
@@ -89,9 +90,12 @@ Deux axes indépendants, détaillés dans [THEMES.md](THEMES.md) :
- **Apparence** clair/sombre : système par défaut, choix au bouton gardé pour la visite
(`sessionStorage`).
-- **Habillage** saisonnier (`src/data/themes.json`) : choisi dans le navigateur selon la date
- du visiteur ; chaque habillage a une version jour (clair) et nuit (sombre).
- Prévisualisation : `?theme=halloween` ou `?theme=aucun`.
+- **Habillage** (`src/data/themes.json`) : choisi dans le navigateur selon la date du
+ visiteur, par priorité **spéciaux > récurrents > saisons** (une saison est toujours active).
+ Chaque habillage a une version jour (clair) et nuit (sombre), à concevoir séparément.
+ Prévisualisation : `?theme=` ou `?theme=aucun`.
+- Tout décor est `aria-hidden`, passe derrière le contenu, s'allège sur téléphone (`is-wide`)
+ et s'immobilise si l'appareil demande moins de mouvement.
## Git
diff --git a/README.md b/README.md
index 58ff687..d620a22 100644
--- a/README.md
+++ b/README.md
@@ -1,164 +1,224 @@
-# Portfolio · orelink.github.io
+
-Site statique (Astro), une seule page qui défile, pensée autant pour l'ordinateur que
-pour le téléphone. Publié sur GitHub Pages à chaque merge dans `main` (voir
-« Branches et mise en ligne »).
+# oreLINK · Portfolio
-```bash
-npm install
-npm run dev # http://localhost:4321
-npm run build # génère dist/ (et vérifie tous les réglages)
-npm run preview # sert dist/
-```
-
-## Branches et mise en ligne
+**Portfolio of Aurélien Bertrand, Data Engineer based in Bordeaux, France.**
+A single-page static site in French and English, driven entirely by data files.
-| Branche | Rôle | Qui peut y écrire |
-|---|---|---|
-| `dev` | développement : on y pousse son travail | tout le monde (push direct) |
-| `main` | production : ce qui est en ligne | personne en direct, uniquement par PR depuis `dev` |
-| `gh-pages` | le site construit (`dist/`), servi par GitHub Pages | uniquement l'action de déploiement |
+[**orelink.github.io**](https://orelink.github.io) · [English version](https://orelink.github.io/en/)
-Le circuit :
+[](https://github.com/oreLINK/orelink.github.io/actions/workflows/deploy.yml)
+[](https://github.com/oreLINK/orelink.github.io/actions/workflows/ci.yml)
+
+
-1. Travailler sur `dev`, pousser : la CI construit le site à chaque push.
-2. Ouvrir une PR `dev` → `main`. Deux vérifications doivent passer pour pouvoir merger :
- **Build** (le site se construit) et **Source dev** (la PR part bien de `dev`).
-3. Merger la PR : `.github/workflows/deploy.yml` construit `main` et publie `dist/` dans
- `gh-pages`. Le site est en ligne une à deux minutes plus tard.
+
-Les règles sont des *rulesets* GitHub (Settings → Rules → Rulesets) :
+---
-- **main** : PR obligatoire, vérifications Build et Source dev obligatoires, ni force push ni
- suppression. Personne ne les contourne, administrateur compris.
-- **gh-pages** : écriture réservée à la clé de déploiement « gh-pages deploy (GitHub
- Actions) » (Settings → Deploy keys), dont la partie privée est le secret
- `GH_PAGES_DEPLOY_KEY` de l'action. Ni force push ni suppression.
+## Highlights
+
+- **Data-driven**: profile, experience, education, certifications, volunteering, labels and
+ themes all live in JSON files. No content is hard-coded.
+- **Validated at build time**: every file is checked against a schema (zod). A malformed
+ date, an invalid color or a broken translation blocks deployment with a precise error.
+- **Bilingual**: French at `/`, English at `/en/`, on the same page. Translation files hold
+ only the text; everything else comes from the French source.
+- **Light and dark**: follows the device setting, with a toggle to switch.
+- **Seasonal skins**: one season is always active (spring, summer, autumn, winter), with
+ holidays layered on top (Halloween, Christmas, New Year, Fête de la musique), each with a
+ day and a night version. See [THEMES.md](THEMES.md).
+- **Lightweight**: HTML and CSS generated at build time, a single small browser script,
+ self-hosted fonts, no third-party calls.
+- **Accessible**: keyboard navigation, screen-reader labels, checked contrast in both
+ themes, animations disabled when the device asks for reduced motion, readable without
+ JavaScript.
+- **Mobile-first**, as much as desktop.
+
+## Tech stack
+
+| Purpose | Choice |
+|---|---|
+| Generator | [Astro 5](https://astro.build), fully static output |
+| Language | TypeScript (strict) |
+| Data | JSON and Markdown, validated with zod (Astro content collections) |
+| Styling | Plain CSS: design tokens, frosted glass, View Transitions |
+| Fonts | Plus Jakarta Sans, JetBrains Mono (`@fontsource`, self-hosted) |
+| Hosting | GitHub Pages, deployed with GitHub Actions |
-GitHub Pages sert la branche `gh-pages`, dossier `/` (Settings → Pages). Pour relancer une
-mise en ligne sans changement : Actions → Deploy to gh-pages → Run workflow.
+## Getting started
-## Modifier le site : les fichiers de réglage
+Requirements: **Node.js 22** and npm.
-Tout ce qui s'affiche vient de ces fichiers. On n'a jamais besoin de toucher au code.
+```bash
+npm install
+npm run dev # dev server: http://localhost:4321
+npm run build # builds dist/ and validates all data
+npm run preview # serves dist/ to check the final output
+```
-| Fichier | Ce qu'il règle |
-|---|---|
-| `src/data/profil.json` | Prénom, nom, photo, situation, disponibilité, lieu, e-mail, liens, « À propos » |
-| `src/data/site.json` | Titre de l'onglet, description, couleur d'accent, **ordre et titres des sections**, tous les libellés (boutons, mentions…) |
-| `src/data/experiences.json` | Expérience : une entreprise, un ou plusieurs postes |
-| `src/data/formations.json` | Formation |
-| `src/data/certifications.json` | Certifications |
-| `src/data/benevolat.json` | Bénévolat |
-| `src/content/projets/*.md` | Projets : un fichier par projet (tuile de l'accueil + page de doc) |
-| `src/data/en/*.json` | Version anglaise (voir « Langues » plus bas) |
-| `src/data/themes.json` | Thèmes d'habillage saisonniers (Halloween…) : voir [THEMES.md](THEMES.md) |
+`npm run build` doubles as the test suite: if it passes, the content is valid and the site
+is ready to ship.
-Pour ajouter une entrée, ajouter un objet dans le tableau JSON concerné, avec un `id`
-unique. L'ordre se calcule tout seul, du plus récent au plus ancien.
+## Editing the content
-### `site.json`
+Everything on the page comes from these files; the code never needs to change.
-- `sections` : l'ordre du tableau est l'ordre sur la page. `"afficher": false` masque une
- section ; `titre` change son titre. Une section vide disparaît d'elle-même.
-- `couleurs.accentClair` / `couleurs.accentSombre` : la couleur des boutons et des liens,
- en thème clair et en thème sombre (`#RRGGBB`).
-- `textes` : chaque libellé du site (« Me contacter », « Comment je l'ai fait », « En
- ligne »…).
+| File | Content |
+|---|---|
+| `src/data/profil.json` | Name, photo, job, location, spoken languages, email, links, "About" |
+| `src/data/site.json` | Title and description, accent color, **section order and titles**, every UI label |
+| `src/data/experiences.json` | Experience: one company, one or more positions |
+| `src/data/formations.json` | Education |
+| `src/data/certifications.json` | Certifications |
+| `src/data/benevolat.json` | Volunteering |
+| `src/content/projets/*.md` | Projects: one file per project (home page tile and detail page) |
+| `src/data/en/*.json` | English translation (text only) |
+| `src/data/themes.json` | Seasonal and holiday skins, see [THEMES.md](THEMES.md) |
-### `profil.json`
+To add an entry, add an object with a unique `id` to the relevant array. Display order is
+computed automatically, from most recent to oldest.
-- `photo` : déposer l'image dans `public/` (ex. `public/photo.jpg`) et écrire
- `"photo": "photo.jpg"`. Vide, ce sont les initiales qui s'affichent.
-- `statut.ton` : couleur de la pastille de disponibilité, `ok` (vert), `soon` (orange)
- ou `none` (gris). Vider `statut.texte` masque la pastille.
-- `liens.cv` : déposer `public/cv.pdf` et écrire `"cv": "cv.pdf"`. Un lien vide masque
- son bouton.
-- `langues` : un drapeau par langue à côté du lieu, avec `nom` et `niveau` dans une bulle
- au survol ou au toucher. `code` vaut `fr`, `en` ou `es` (drapeaux dessinés dans
- `src/components/Drapeau.astro`). Un tableau vide masque les drapeaux.
+
+site.json settings
-### Langues
+- `sections`: array order is page order. `"afficher": false` hides a section; `titre`
+ changes its heading. An empty section disappears on its own.
+- `hauteurSectionRepliee`: above this height (in pixels), a section collapses behind a
+ "Show all" button.
+- `couleurs.accentClair` / `couleurs.accentSombre`: button and link color in light and dark
+ themes (`#RRGGBB`), used when no skin is active.
+- `textes`: every label on the site ("Contact me", "See more"…).
-Le site existe en français (à `/`, par défaut) et en anglais (à `/en/`) ; le bouton à
-gauche du thème passe de l'un à l'autre en restant sur la même page.
+
-Les fichiers de `src/data/en/` ne contiennent **que les textes à traduire** : le reste
-(dates, logos, liens…) vient du fichier français du même nom. Une entrée se retrouve par
-son `id` (expériences, formations, bénévolat, sections de `site.json`) ; les postes d'une
-même entreprise et les langues de `profil.json` se suivent dans le même ordre qu'en
-français. Un champ absent reste en français ; un champ ou un `id` inconnu fait échouer le
-build avec un message qui le désigne. Les pages de projets (`src/content/projets/`) ne sont
-écrites qu'en français.
+
+profil.json settings
-### Dates
+- `photo`: put the image in `public/` (e.g. `public/img/photo.jpg`) and set
+ `"photo": "img/photo.jpg"`. Leave it empty to show initials instead.
+- `statut`: availability badge; `ton` is `ok` (green), `soon` (orange) or `none` (grey).
+ An empty `statut.texte` hides the badge.
+- `langues`: one flag per language next to the location, with `nom` and `niveau` shown in a
+ tooltip. `code` is `fr`, `en` or `es` (flags drawn in `src/components/Drapeau.astro`).
+- `liens.cv`: put `public/cv.pdf` in place and set `"cv": "cv.pdf"`. An empty link hides its
+ button.
-Format `"AAAA-MM"` (ex. `"2024-03"`), ou `"AAAA"` seul pour une formation. Pour un poste
-ou un bénévolat en cours, mettre `"fin": null` : la page affiche « aujourd'hui », et la
-durée (« 2 ans 8 mois ») se met à jour toute seule.
+
-### Logos d'organisations
+
+Dates, logos and projects
-Champ `logo` facultatif (entreprise, école, organisme) : un chemin dans `public/`
-(ex. `"logos/acme.png"`) ou une URL. Sans logo, les initiales s'affichent.
+**Dates**: `"YYYY-MM"` format (e.g. `"2024-03"`), or `"YYYY"` alone for education. For an
+ongoing position or commitment, use `"fin": null`: the page shows "Present" and the duration
+("2 yrs 8 mos") updates itself.
-### Projets
+**Logos**: optional `logo` field, a path inside `public/` (e.g. `"logos/acme.png"`) or a URL.
+Square images work best (400 × 400). Without a logo, initials are shown.
-Un fichier `src/content/projets/.md` = une tuile sur l'accueil **et** une page
-`/projets//` « Comment je l'ai réalisé » :
+**Projects**: a `src/content/projets/.md` file creates a tile on the home page **and**
+a `/projets//` "How I built it" page.
```yaml
---
titre: Terminus
-resume: Une ou deux phrases.
-date: 2026-09 # début du projet
-statut: en-ligne # en-ligne | en-cours | archive
-role: Projet personnel
-lien: https://… # le projet en ligne (facultatif)
-depot: https://github.com/… # le code (facultatif)
+resume: One or two sentences.
+date: 2026-09 # project start
+statut: en-ligne # en-ligne | en-cours | archive
+role: Side project
+lien: https://… # live project (optional)
+depot: https://github.com/… # source code (optional)
stack: [HTML, CSS]
-ordre: 1 # facultatif : force l'ordre (le plus petit d'abord)
-couleur: "#E8962E" # facultatif : couleur de la pastille du projet
+ordre: 1 # optional: forces the order (lowest first)
+couleur: "#E8962E" # optional: badge color
---
-## Le besoin
+## The need
…
```
-Le corps est du Markdown classique. Les titres `##` forment le sommaire de la page.
+The body is plain Markdown; `##` headings build the page's table of contents.
+
+
-### Vérification
+
+English translation
-Tous ces fichiers sont vérifiés au build. Une date mal écrite, une couleur invalide ou
-un champ manquant fait échouer `npm run build` (donc la mise en ligne), avec un message
-qui désigne le fichier et le champ.
+Files in `src/data/en/` contain **only the text to translate**: everything else (dates,
+logos, links…) comes from the French file with the same name.
-## Design
+- Entries are matched by `id` (experience, education, certifications, volunteering,
+ `site.json` sections).
+- Lists without an `id` (positions within a company, profile languages) follow the French
+ order; plain text lists (skills) are replaced as a whole.
+- A missing field stays in French; an unknown field or `id` fails the build.
+- Project pages are written in French only; the surrounding interface is translated.
-- Police : Plus Jakarta Sans, hébergée avec le site (`@fontsource-variable`), sans appel
- à un service tiers.
-- `src/styles/tokens.css` : couleurs (gris neutres, clair et sombre), verre, formes.
-- `src/styles/global.css` : mise en page et composants.
-- Le bouton en haut à droite passe du thème clair au sombre ; le choix vaut pour la visite
- en cours. Par défaut, et à chaque nouvelle visite, le site suit le réglage du système.
-- Les animations s'arrêtent si l'appareil demande moins de mouvement.
-- Thèmes d'habillage saisonniers (Halloween en octobre) : voir [THEMES.md](THEMES.md).
+
-## Structure
+## Architecture
```
src/
-├── data/ # réglages et contenu (JSON)
-├── content/projets/ # un .md par projet
-├── content.config.ts # vérification du contenu
-├── data/en/ # traductions anglaises (seulement les textes)
-├── lib/reglages.ts # vérification de site.json et profil.json, par langue
-├── lib/i18n.ts # langues, et pose des traductions sur le français
-├── layouts/Base.astro # , haut de page, pied de page
-├── components/sections/ # une section par fichier
-├── components/ # tuile projet, logo d'organisation, étiquettes, icônes
-├── vues/ # accueil et page de projet, communes aux deux langues
-├── pages/ # adresses : / et /en/, projets, 404
-├── scripts/ui.ts # thème, apparition, durées
-└── styles/ # jetons et composants
+├── data/ # content and settings (JSON); data/en/: translations
+├── content/projets/ # one Markdown file per project
+├── content.config.ts # data schemas
+├── lib/ # settings per language, translations, dates, links, decor
+├── layouts/Base.astro # , pre-render scripts, header and footer
+├── vues/ # home and project page, shared by both languages
+├── pages/ # routes: /, /en/, /projets/…, /en/projets/…, 404
+├── components/
+│ ├── sections/ # one home page section per file
+│ └── habillages/ # decor for the seasonal and holiday skins
+├── scripts/ui.ts # all browser-side JavaScript
+└── styles/
+ ├── tokens.css # design tokens (light / dark)
+ ├── global.css # layout and components
+ └── habillages/ # one file per skin
```
+
+The codebase (file names, identifiers, comments) is written in French; see
+[AGENTS.md](AGENTS.md) for conventions.
+
+## Deployment
+
+| Branch | Role | Write access |
+|---|---|---|
+| `dev` | Development | Direct push |
+| `main` | Production, what is live | Pull requests from `dev` only |
+| `gh-pages` | Built site (`dist/`), served by GitHub Pages | Deployment workflow only |
+
+1. Work happens on `dev`; CI builds the site on every push.
+2. A `dev` → `main` pull request must pass two checks: **Build** (the site builds) and
+ **Source dev** (the PR comes from `dev`).
+3. On merge, [`deploy.yml`](.github/workflows/deploy.yml) builds `main` and publishes `dist/`
+ to `gh-pages`. The site is live one to two minutes later.
+
+
+Repository protection
+
+Rules are defined as GitHub *rulesets* (Settings → Rules → Rulesets):
+
+- **main**: pull request required, Build and Source dev checks required, no force push or
+ deletion. Nobody can bypass them, admins included.
+- **gh-pages**: writes restricted to the "gh-pages deploy (GitHub Actions)" deploy key
+ (Settings → Deploy keys), whose private key is the workflow's `GH_PAGES_DEPLOY_KEY`
+ secret. No force push or deletion.
+
+GitHub Pages serves the `gh-pages` branch, `/` folder (Settings → Pages). To redeploy
+without changes: Actions → Deploy to gh-pages → Run workflow.
+
+
+
+## Documentation
+
+| Document | Audience |
+|---|---|
+| [README.md](README.md) | Project overview and content editing |
+| [THEMES.md](THEMES.md) | Light / dark appearance and skins: calendar, settings, adding a theme |
+| [AGENTS.md](AGENTS.md) | Code conventions, for contributors and coding agents |
+
+## Author
+
+**Aurélien Bertrand** (oreLINK) · Data Engineer at Betclic Group, Bordeaux
+[LinkedIn](https://www.linkedin.com/in/aurelienbertrand/) · [GitHub](https://github.com/oreLINK)
diff --git a/THEMES.md b/THEMES.md
index 7566cea..fa165e7 100644
--- a/THEMES.md
+++ b/THEMES.md
@@ -5,35 +5,62 @@ Le site a deux réglages visuels indépendants :
| | Quoi | Qui décide |
|---|---|---|
| **Apparence** | Clair ou sombre | Le visiteur (bouton en haut à droite), sinon le réglage de son appareil |
-| **Thème d'habillage** | Une ambiance saisonnière posée sur le site (Halloween…) | Le calendrier, d'après `src/data/themes.json` |
+| **Thème d'habillage** | Une ambiance posée sur le site : saison, fête, grand événement | Le calendrier, d'après `src/data/themes.json` |
Les deux se combinent : chaque thème d'habillage existe en **deux versions**, une version
**jour** (apparence claire) et une version **nuit** (apparence sombre), chacune avec son décor
et ses animations. Quand le visiteur passe du clair au sombre, le décor change avec un fondu.
-## Le thème par défaut
+## Trois niveaux de priorité
-C'est le site tel qu'il est le reste de l'année : gris neutres, accent bleu (réglé dans
-`src/data/site.json` → `couleurs`). Il s'affiche dès qu'aucun thème d'habillage n'est en
-période, ou quand ils sont tous désactivés. Il n'a rien à régler ici.
+| Niveau | Rôle | Dates | Priorité |
+|---|---|---|---|
+| **Spéciaux** | Grands événements ponctuels (Coupe du monde, Roland-Garros…) | Avec l'année : `AAAA-MM-JJ` | 1, l'emporte toujours |
+| **Récurrents** | Fêtes qui reviennent chaque année | Chaque année : `MM-JJ` | 2 |
+| **Saisons** | Fond permanent : les 4 couvrent toute l'année | Chaque année : `MM-JJ` | 3 |
+| *Défaut* | Le site sans habillage | Seulement si aucun thème n'est en période (saisons désactivées) | dernier recours |
-## Halloween
+À une date donnée, le site prend **le premier thème actif en période**, en partant du haut. Par
+exemple le 21 juin, la Fête de la musique passe avant l'Été ; le 15 octobre, Halloween passe
+avant l'Automne. À l'intérieur d'un même niveau, le premier de la liste l'emporte.
-Actif **chaque année du 1er octobre à 0 h au 31 octobre à 23 h 59**. Sobre, pour rester pro.
+Le **thème par défaut** (gris neutres, accent bleu réglé dans `src/data/site.json` →
+`couleurs`) ne s'affiche donc plus que si les saisons sont désactivées, ou avec
+`"forcer": "aucun"`.
-Dans les deux versions :
+## Les thèmes
-- l'accent passe au **orange citrouille** (boutons, liens, le « _ » du logo) ;
-- le fond se réchauffe : un halo orange en haut à gauche, violet en bas à droite.
+Le contenu, la mise en page et les textes ne changent jamais : le décor passe derrière. Les
+animations s'arrêtent si l'appareil demande moins de mouvement (le décor reste affiché,
+immobile), et les décors sont allégés sur téléphone.
-| | Version jour (apparence claire) | Version nuit (apparence sombre) |
-|---|---|---|
-| Décor | Une **toile d'araignée** très pâle en haut à gauche, avec son **araignée** au bout d'un fil | Une **lune** en croissant en haut à droite, quelques **étoiles**, des **chauves-souris** |
-| Animations | L'araignée monte et descend en se balançant ; des **feuilles d'automne** tombent lentement en tournoyant | La lune luit doucement, les étoiles scintillent, deux chauves-souris planent près de la lune en battant des ailes et une troisième **traverse le ciel** de temps en temps |
-| Sur téléphone | 4 feuilles au lieu de 6 | 5 étoiles au lieu de 7, une chauve-souris qui plane au lieu de deux |
+### Saisons (dates astronomiques)
+
+| Thème | Période | Jour (clair) | Nuit (sombre) | Accent |
+|---|---|---|---|---|
+| **Printemps** | 20 mars – 20 juin | Pétales de cerisier qui volent ; fond vert et rose | Lucioles qui clignotent, quelques étoiles | Vert |
+| **Été** | 21 juin – 22 sept. | Halo de soleil, dunes de sable en bas | Voie lactée en diagonale, lueur chaude sur l'horizon, étoiles filantes | Turquoise |
+| **Automne** | 23 sept. – 20 déc. | Feuilles qui tombent en tournoyant | Lune rousse, brume qui dérive, quelques feuilles sombres | Bordeaux |
+| **Hiver** | 21 déc. – 19 mars | Neige et congères en bas | Aurore boréale qui ondule, étoiles, neige et congères | Bleu glacier |
+
+### Récurrents
+
+| Thème | Période | Jour (clair) | Nuit (sombre) | Accent |
+|---|---|---|---|---|
+| **Halloween** | 1er – 31 oct. | Toile d'araignée et son araignée au bout d'un fil, feuilles qui tombent | Lune en croissant, étoiles, chauves-souris (dont une qui traverse le ciel) | Orange citrouille |
+| **Noël** | 1er – 26 déc. | Neige qui tombe sur un fond rouge et vert | Neige et ciel étoilé | Rouge |
+| **Nouvel An** | 31 déc. – 2 janv. | Ballons dorés aux chiffres de la nouvelle année, confettis | Feux d'artifice : fusées, gerbes (pivoine, saule, anneau, crépitement) qui retombent | Doré |
+| **Fête de la musique** | 21 juin | Notes qui s'envolent, égaliseur en bas | Notes et égaliseur en néon, deux projecteurs qui balaient la scène | Violet |
+
+L'année des ballons est calculée dans le navigateur : de juillet à décembre, c'est l'année
+suivante ; de janvier à juin, l'année en cours.
+
+### Spéciaux
-Les animations s'arrêtent si l'appareil demande moins de mouvement : le décor reste affiché,
-immobile. Le contenu, la mise en page et les textes ne changent pas ; le décor passe derrière.
+Aucun pour l'instant. Prévus : un habillage **Football** (Coupe du monde, Euro), **Rugby**
+(Coupe du monde de rugby) et **Tennis** (Roland-Garros). Chaque événement ne sera qu'une
+ligne dans `themes.json`, qui réutilise l'habillage de son sport (voir « Ajouter un
+événement spécial »).
## Les réglages : `src/data/themes.json`
@@ -41,50 +68,55 @@ immobile. Le contenu, la mise en page et les textes ne changent pas ; le décor
{
"forcer": "",
- "themes": [
- {
- "id": "halloween",
- "nom": "Halloween",
- "actif": true,
- "debut": "10-01",
- "fin": "10-31",
- "accentClair": "#C2410C",
- "accentSombre": "#FB923C"
- }
+ "speciaux": [],
+
+ "recurrents": [
+ { "id": "halloween", "nom": "Halloween", "actif": true, "debut": "10-01", "fin": "10-31", "accentClair": "#C2410C", "accentSombre": "#FB923C" }
+ ],
+
+ "saisons": [
+ { "id": "printemps", "nom": "Printemps", "actif": true, "debut": "03-20", "fin": "06-20", "accentClair": "#2E7D46", "accentSombre": "#86D19C" }
]
}
```
+(Extrait : le fichier contient les 4 récurrents et les 4 saisons.)
+
| Champ | Rôle |
|---|---|
-| `forcer` | `""` : chaque thème suit ses dates (normal). `"halloween"` : ce thème toute l'année. `"aucun"` : jamais de thème, le site reste au thème par défaut. |
-| `id` | Le thème dessiné dans le code (aujourd'hui : `halloween`). Ne pas changer. |
+| `forcer` | `""` : chaque thème suit ses dates (normal). L'`id` d'un thème (ex. `"halloween"`) : ce thème toute l'année. `"aucun"` : jamais d'habillage, le site reste au thème par défaut. |
+| `id` | Saisons et récurrents : le thème dessiné dans le code, à ne pas changer. Spéciaux : un nom libre pour l'événement (ex. `"coupe-du-monde-2026"`). |
| `nom` | Son nom, pour s'y retrouver. |
+| `habillage` | Spéciaux seulement : le dessin à utiliser (ex. `"football"`). |
| `actif` | `true` : il s'affiche pendant sa période. `false` : il est désactivé. |
-| `debut` | Premier jour, au format `MM-JJ` (`"10-01"` = 1er octobre), **dès minuit**. |
-| `fin` | Dernier jour, au format `MM-JJ`, **jusqu'à 23 h 59** inclus. |
+| `debut` | Premier jour, **dès minuit**. `MM-JJ` (saisons, récurrents) ou `AAAA-MM-JJ` (spéciaux). |
+| `fin` | Dernier jour, **jusqu'à 23 h 59** inclus, même format. |
| `accentClair` / `accentSombre` | Couleur d'accent du thème, en apparence claire et sombre (`#RRGGBB`). |
Ce fichier est vérifié au build : une date mal écrite (`"1-10"`, `"10-32"`), une couleur
-invalide ou un `id` inconnu fait échouer `npm run build` avec un message qui désigne le champ.
+invalide, un `id` en double ou un `forcer` inconnu fait échouer `npm run build` avec un
+message qui désigne le champ.
### Exemples
-**Désactiver Halloween** :
+**Désactiver un thème** (ex. la Fête de la musique) : dans son entrée,
```json
"actif": false
```
-**Changer la période** (ex. seulement la dernière semaine d'octobre) :
+**Changer une période** (ex. Halloween seulement la dernière semaine d'octobre) :
```json
"debut": "10-25",
"fin": "10-31"
```
-Une période peut passer le 31 décembre : `"debut": "12-15"`, `"fin": "01-05"` couvre du
-15 décembre au 5 janvier.
+Une période annuelle peut passer le 31 décembre : l'Hiver (`"12-21"` → `"03-19"`) et le
+Nouvel An (`"12-31"` → `"01-02"`) le font déjà.
+
+**Revenir au site sans habillage le reste de l'année** : désactiver les 4 saisons. Les fêtes
+restent actives pendant leur période.
**Désactiver tous les thèmes d'un coup**, sans toucher à chacun :
@@ -92,59 +124,76 @@ Une période peut passer le 31 décembre : `"debut": "12-15"`, `"fin": "01-05"`
"forcer": "aucun"
```
-**Garder Halloween en ligne hors saison** (ex. pour une démonstration) :
+**Garder un thème en ligne hors période** (ex. pour une démonstration) :
```json
-"forcer": "halloween"
+"forcer": "nouvel-an"
```
## Comment la date est choisie
Le site est statique : le thème se décide **dans le navigateur du visiteur**, à l'ouverture
-de la page, avec **sa date et son heure locales**. Il n'y a donc rien à republier le
-1er octobre ni le 1er novembre : le site bascule tout seul. Le choix se fait avant
-l'affichage, sans flash du thème par défaut.
+de la page, avec **sa date et son heure locales**. Il n'y a donc rien à republier quand une
+période commence ou se termine : le site bascule tout seul. Le choix se fait avant
+l'affichage, sans flash d'un autre thème.
## Prévisualiser un thème
Ajouter `?theme=` à l'adresse, à n'importe quelle date et sans rien modifier :
-- `https://orelink.github.io/?theme=halloween` : le site en habillage Halloween ;
-- `https://orelink.github.io/?theme=aucun` : le site au thème par défaut, même en octobre.
+- `https://orelink.github.io/?theme=noel` : le site en habillage Noël (marche avec tous les
+ `id` : `printemps`, `ete`, `automne`, `hiver`, `halloween`, `noel`, `nouvel-an`,
+ `fete-musique`, ou l'`id` d'un événement spécial) ;
+- `https://orelink.github.io/?theme=aucun` : le site au thème par défaut.
-Fonctionne aussi en local (`npm run dev`, puis `http://localhost:4321/?theme=halloween`) et
-sur les pages anglaises (`/en/?theme=halloween`). Cela ne vaut que pour la page ouverte.
+Fonctionne aussi en local (`npm run dev`, puis `http://localhost:4321/?theme=noel`) et sur les
+pages anglaises (`/en/?theme=noel`). Cela ne vaut que pour la page ouverte. Pour voir l'autre
+version (jour ou nuit), utiliser le bouton d'apparence en haut à droite.
-## Ajouter un thème (pour le code)
+## Ajouter un événement spécial
-Un thème d'habillage, c'est quatre morceaux. Exemple pour un thème `noel` :
+Quand l'habillage de son sport existe déjà, une ligne suffit dans `speciaux` :
-1. **Le déclarer** dans `src/lib/reglages.ts` : ajouter `'noel'` à `HABILLAGES`.
-2. **Son décor** dans `src/components/habillages/Noel.astro`, sur le modèle de
- `Halloween.astro` : un bloc `decor decor-noel` qui contient deux parties,
- `decor-jour` (affichée en apparence claire) et `decor-nuit` (en apparence sombre). Le
- site choisit tout seul la bonne partie ; le décor entier reste caché tant que le thème
- n'est pas actif. L'ajouter dans le `.backdrop` de `src/layouts/Base.astro`, à côté de
- ``. Penser à ce qui a du sens à chaque moment : pas de chauves-souris en
- plein jour, pas de soleil la nuit…
-3. **Ses couleurs et animations** dans `src/styles/habillages/noel.css`, sur le modèle de
- `halloween.css` : les jetons du fond (`--light`, `--shade-1`, `--shade-2`) et du décor
- (`--decor`) en clair et en sombre sous `:root[data-habillage="noel"]`, la règle
- `[data-habillage="noel"] .decor-noel { display: block; }`, puis le style des éléments.
- Mettre les animations dans `@media (prefers-reduced-motion: no-preference)`. L'importer
- en tête de `src/styles/global.css`, à côté de `halloween.css`.
-4. **Sa période** dans `src/data/themes.json` : une entrée avec `"id": "noel"`.
+```json
+{ "id": "coupe-du-monde-2026", "nom": "Coupe du monde 2026", "habillage": "football", "actif": true, "debut": "2026-06-11", "fin": "2026-07-19", "accentClair": "#15803D", "accentSombre": "#86EFAC" }
+```
-Si deux thèmes actifs se chevauchent, le premier de la liste l'emporte.
+Sinon, il faut d'abord dessiner l'habillage (section suivante).
+
+## Ajouter un habillage (pour le code)
+
+Un habillage, c'est quatre morceaux. Exemple pour `football` :
+
+1. **Le déclarer** dans `src/lib/reglages.ts` : ajouter `'football'` à `HABILLAGES`.
+2. **Son décor** dans `src/components/habillages/Football.astro`, sur le modèle des autres :
+ un bloc `decor decor-football` qui contient deux parties, `decor-jour` (affichée en
+ apparence claire) et `decor-nuit` (en apparence sombre). Le site choisit tout seul la
+ bonne partie ; le décor entier reste caché tant que l'habillage n'est pas actif. L'ajouter
+ dans le `.backdrop` de `src/layouts/Base.astro`, avec les autres. Penser à ce qui a du sens
+ à chaque moment : pas de chauves-souris en plein jour, pas de soleil la nuit…
+ - Positions « au hasard » mais fixes : `hasard(graine)` de `src/lib/decor.ts`.
+ - Étoiles et neige toutes faites : `` et ``.
+ - Moins d'éléments sur téléphone : la classe `is-wide` les masque sous 36rem.
+3. **Ses couleurs et animations** dans `src/styles/habillages/football.css`, sur le modèle des
+ autres : les jetons du fond (`--light`, `--shade-1`, `--shade-2`) en clair et en sombre
+ sous `:root[data-habillage="football"]`, la règle
+ `[data-habillage="football"] .decor-football { display: block; }`, puis le style des
+ éléments. Mettre les animations dans `@media (prefers-reduced-motion: no-preference)`.
+ L'importer en tête de `src/styles/global.css`, avec les autres. Les pièces communes
+ (scintillement, chutes, neige) sont dans `commun.css`.
+4. **Sa période** dans `src/data/themes.json` : une entrée de saison ou de récurrent avec
+ `"id": "football"`, ou un événement spécial avec `"habillage": "football"`.
## Fichiers
| Fichier | Contenu |
|---|---|
-| `src/data/themes.json` | Activation, dates et couleurs d'accent des thèmes |
-| `src/lib/reglages.ts` | Liste des thèmes existants (`HABILLAGES`) et vérification de `themes.json` |
-| `src/layouts/Base.astro` | Le petit script qui choisit le thème selon la date (dans ``) |
-| `src/styles/habillages/halloween.css` | Fond, décor et animations d'Halloween |
-| `src/components/habillages/Halloween.astro` | Décor jour (toile, araignée, feuilles) et nuit (lune, étoiles, chauves-souris) ; nombre, taille et rythme des feuilles et des étoiles en tête du fichier |
+| `src/data/themes.json` | Activation, dates et couleurs d'accent de tous les thèmes |
+| `src/lib/reglages.ts` | Liste des habillages existants (`HABILLAGES`) et vérification de `themes.json` |
+| `src/layouts/Base.astro` | Le petit script qui choisit le thème selon la date et les priorités (dans ``) |
+| `src/components/habillages/*.astro` | Le décor jour et nuit de chaque habillage, plus `Etoiles` et `Flocons` (partagés) |
+| `src/styles/habillages/*.css` | Fond, décor et animations de chaque habillage ; `commun.css` pour les pièces partagées |
+| `src/lib/decor.ts` | Positions pseudo-aléatoires (graine fixe) des éléments de décor |
+| `src/scripts/ui.ts` | Feux d'artifice et année des ballons du Nouvel An |
| `src/styles/global.css` | Bascule jour / nuit des décors (règles `.decor-jour` / `.decor-nuit`) |
| `src/styles/tokens.css` | Le thème par défaut, en clair et en sombre |
diff --git a/src/components/habillages/Automne.astro b/src/components/habillages/Automne.astro
new file mode 100644
index 0000000..393bd4e
--- /dev/null
+++ b/src/components/habillages/Automne.astro
@@ -0,0 +1,41 @@
+---
+// Décor Automne (voir THEMES.md), derrière le contenu et caché hors saison.
+// Jour : feuilles qui tombent en tournoyant. Nuit : lune rousse, brume qui dérive, quelques
+// feuilles sombres.
+import { hasard, vars } from '../../lib/decor';
+
+const h = hasard(5);
+const feuilles = (nombre: number, larges: number, couleurs: string[]) =>
+ Array.from({ length: nombre }, (_, i) => ({
+ style: `${vars({
+ x: `${h.entre(3, 95)}%`,
+ top: `${h.entre(5, 90)}vh`,
+ dur: `${h.entre(17, 26)}s`,
+ delay: `${h.entre(-26, 0)}s`,
+ drift: `${h.entre(-60, 60, 0)}px`,
+ spin: `${h.parmi([-1, 1]) * h.entre(240, 340, 0)}deg`,
+ })}; color: ${couleurs[i % couleurs.length]}`,
+ taille: h.entre(18, 30, 0),
+ large: i >= larges,
+ }));
+const jour = feuilles(7, 4, ['#B45309', '#9F1239', '#9A3412', '#CA8A04']);
+const nuit = feuilles(4, 3, ['#C2410C', '#BE123C']);
+---
+
+
+
+
+ {jour.map((f) => )}
+
+
+
+
+
+ {nuit.map((f) => )}
+
+
diff --git a/src/components/habillages/Ete.astro b/src/components/habillages/Ete.astro
new file mode 100644
index 0000000..0e78cd1
--- /dev/null
+++ b/src/components/habillages/Ete.astro
@@ -0,0 +1,36 @@
+---
+// Décor Été (voir THEMES.md), derrière le contenu et caché hors saison.
+// Jour : halo de soleil et dunes de sable. Nuit : Voie lactée en diagonale, lueur chaude sur
+// l'horizon, étoiles et étoiles filantes.
+import Etoiles from './Etoiles.astro';
+import { hasard, vars } from '../../lib/decor';
+
+// La Voie lactée : de minuscules étoiles serrées autour d'une bande (une seule boîte, des ombres).
+const h = hasard(3);
+const poussiere = Array.from(
+ { length: 110 },
+ () => `${h.entre(-62, 62)}vw ${h.normale(4.5).toFixed(1)}vh 0 ${h.parmi([0, 0, 0.4, 0.8])}px rgb(255 255 255 / ${h.entre(0.25, 0.8, 2)})`,
+).join(', ');
+const filantes = [
+ { x: '12%', y: '9%', dur: '11s', delay: '-3s' },
+ { x: '34%', y: '17%', dur: '16s', delay: '-9s' },
+ { x: '58%', y: '6%', dur: '21s', delay: '-15s' },
+];
+---
+
+
diff --git a/src/components/habillages/Etoiles.astro b/src/components/habillages/Etoiles.astro
new file mode 100644
index 0000000..cac7545
--- /dev/null
+++ b/src/components/habillages/Etoiles.astro
@@ -0,0 +1,30 @@
+---
+// Étoiles qui scintillent, pour les versions nuit des habillages. Au-delà de `larges`, elles ne
+// s'affichent que sur grand écran ; `hauteur` : jusqu'où elles descendent (en % de l'écran).
+import { hasard, vars } from '../../lib/decor';
+
+interface Props {
+ nombre: number;
+ larges: number;
+ hauteur: number;
+ graine: number;
+}
+const { nombre, larges, hauteur, graine } = Astro.props;
+const h = hasard(graine);
+const etoiles = Array.from({ length: nombre }, () => ({
+ x: h.entre(3, 97),
+ y: h.entre(3, hauteur),
+ taille: h.parmi([1.5, 2, 2.5]),
+ dur: h.entre(3, 6.5),
+ delay: h.entre(-6, 0),
+}));
+---
+
+{
+ etoiles.map((e, i) => (
+ = larges }]}
+ style={`left: ${e.x}%; top: ${e.y}%; width: ${e.taille}px; ${vars({ dur: `${e.dur}s`, delay: `${e.delay}s` })}`}
+ />
+ ))
+}
diff --git a/src/components/habillages/FeteMusique.astro b/src/components/habillages/FeteMusique.astro
new file mode 100644
index 0000000..acc7a26
--- /dev/null
+++ b/src/components/habillages/FeteMusique.astro
@@ -0,0 +1,40 @@
+---
+// Décor Fête de la musique (voir THEMES.md), derrière le contenu et caché hors période.
+// Jour : notes qui s'envolent, égaliseur en bas. Nuit : les mêmes en néon, et deux projecteurs
+// qui balaient la scène.
+import { hasard, vars } from '../../lib/decor';
+
+const h = hasard(14);
+const notes = () =>
+ Array.from({ length: 10 }, (_, i) => ({
+ signe: '♪♫♬♩'[i % 4],
+ large: i >= 6,
+ style: vars({
+ x: `${h.entre(3, 95)}%`,
+ top: `${h.entre(15, 80)}%`,
+ s: `${h.entre(1.1, 2)}rem`,
+ dur: `${h.entre(12, 19)}s`,
+ delay: `${h.entre(-19, 0)}s`,
+ drift: `${h.entre(-40, 40, 0)}px`,
+ }),
+ }));
+const barres = Array.from({ length: 36 }, (_, i) => ({
+ large: i % 2 === 1,
+ style: vars({ h: `${h.entre(35, 100, 0)}%`, dur: `${h.entre(0.45, 1.1, 2)}s`, delay: `${h.entre(-1, 0, 2)}s` }),
+}));
+const jour = notes();
+const nuit = notes();
+---
+
+
+
+ {jour.map((n) => {n.signe})}
+
{barres.map((b) => )}
+
+
+
+
+ {nuit.map((n) => {n.signe})}
+
{barres.map((b) => )}
+
+
diff --git a/src/components/habillages/Flocons.astro b/src/components/habillages/Flocons.astro
new file mode 100644
index 0000000..9a2c916
--- /dev/null
+++ b/src/components/habillages/Flocons.astro
@@ -0,0 +1,30 @@
+---
+// Neige qui tombe (Hiver, Noël) ; la couleur des flocons (--flake) vient de l'habillage. Au-delà
+// de `larges`, ils ne s'affichent que sur grand écran.
+import { hasard, vars } from '../../lib/decor';
+
+interface Props {
+ nombre: number;
+ larges: number;
+ graine: number;
+}
+const { nombre, larges, graine } = Astro.props;
+const h = hasard(graine);
+const flocons = Array.from({ length: nombre }, () => ({
+ x: h.entre(2, 98),
+ top: h.entre(0, 95),
+ s: h.parmi([3, 4, 4, 5, 6]),
+ dur: h.entre(13, 24),
+ delay: h.entre(-24, 0),
+ drift: h.entre(-24, 24, 0),
+}));
+---
+
+{
+ flocons.map((f, i) => (
+ = larges }]}
+ style={vars({ x: `${f.x}%`, top: `${f.top}vh`, s: `${f.s}px`, dur: `${f.dur}s`, delay: `${f.delay}s`, drift: `${f.drift}px` })}
+ />
+ ))
+}
diff --git a/src/components/habillages/Hiver.astro b/src/components/habillages/Hiver.astro
new file mode 100644
index 0000000..f9296cf
--- /dev/null
+++ b/src/components/habillages/Hiver.astro
@@ -0,0 +1,25 @@
+---
+// Décor Hiver (voir THEMES.md), derrière le contenu et caché hors saison.
+// Jour : neige et congères. Nuit : aurore boréale, étoiles, neige et congères.
+import Etoiles from './Etoiles.astro';
+import Flocons from './Flocons.astro';
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/habillages/Noel.astro b/src/components/habillages/Noel.astro
new file mode 100644
index 0000000..7e36b85
--- /dev/null
+++ b/src/components/habillages/Noel.astro
@@ -0,0 +1,16 @@
+---
+// Décor Noël (voir THEMES.md), derrière le contenu et caché hors période.
+// Jour : neige qui tombe sur un fond rouge et vert. Nuit : neige et ciel étoilé.
+import Etoiles from './Etoiles.astro';
+import Flocons from './Flocons.astro';
+---
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/habillages/NouvelAn.astro b/src/components/habillages/NouvelAn.astro
new file mode 100644
index 0000000..2dfabb1
--- /dev/null
+++ b/src/components/habillages/NouvelAn.astro
@@ -0,0 +1,53 @@
+---
+// Décor Nouvel An (voir THEMES.md), derrière le contenu et caché hors période.
+// Jour : confettis qui tombent et ballons dorés aux chiffres de la nouvelle année (posés au build,
+// puis tenus à jour par src/scripts/ui.ts). Nuit : étoiles et feux d'artifice (canvas, ui.ts).
+import Etoiles from './Etoiles.astro';
+import { hasard, vars } from '../../lib/decor';
+import { anneeQuiCommence } from '../../lib/dates';
+
+const h = hasard(12);
+const couleurs = ['#EAB308', '#CBD5E1', '#F472B6', '#60A5FA', '#34D399'];
+const confettis = Array.from({ length: 18 }, (_, i) => ({
+ rond: i % 4 === 3,
+ large: i >= 10,
+ style: vars({
+ x: `${h.entre(2, 98)}%`,
+ top: `${h.entre(0, 95)}vh`,
+ dur: `${h.entre(14, 22)}s`,
+ delay: `${h.entre(-22, 0)}s`,
+ drift: `${h.entre(-50, 50, 0)}px`,
+ spin: `${h.parmi([-1, 1]) * h.entre(160, 320, 0)}deg`,
+ c: couleurs[i % couleurs.length],
+ flip: `${h.entre(1.2, 2.6)}s`,
+ }),
+}));
+const chiffres = [...String(anneeQuiCommence())].map((c) => ({ c, style: vars({ dur: `${h.entre(2.6, 3.6)}s`, delay: `${h.entre(-3, 0)}s` }) }));
+---
+
+
+
+ {confettis.map((c) => )}
+
+ {
+ chiffres.map((ch, i) => (
+
+ ))
+ }
+
+
+
+
+
+
+
diff --git a/src/components/habillages/Printemps.astro b/src/components/habillages/Printemps.astro
new file mode 100644
index 0000000..7678648
--- /dev/null
+++ b/src/components/habillages/Printemps.astro
@@ -0,0 +1,35 @@
+---
+// Décor Printemps (voir THEMES.md), derrière le contenu et caché hors saison.
+// Jour : pétales de cerisier qui volent. Nuit : lucioles qui clignotent et quelques étoiles.
+import Etoiles from './Etoiles.astro';
+import { hasard, vars } from '../../lib/decor';
+
+const h = hasard(1);
+const petales = Array.from({ length: 8 }, () => ({
+ x: h.entre(2, 85),
+ top: h.entre(5, 90),
+ s: h.entre(8, 13, 0),
+ dur: h.entre(19, 27),
+ delay: h.entre(-27, 0),
+ drift: h.entre(40, 90, 0),
+ spin: h.parmi([-1, 1]) * h.entre(200, 320, 0),
+}));
+const lucioles = Array.from({ length: 12 }, () => ({ x: h.entre(4, 96), y: h.entre(45, 92), dur: h.entre(10, 16), delay: h.entre(-10, 0) }));
+---
+
+