feat: migre le site vers SvelteKit (Svelte 5, statique, zéro tiers)

- Rebuild complet SvelteKit 2 + Svelte 5 runes + TypeScript, adapter-static
- Images self-hébergées optimisées au build (vite-imagetools), fonts Archivo/Inter
  en woff2 local — BunnyCDN et Google Fonts supprimés (corrige les 403 d'images)
- Tokens charte OKI, thème sombre par défaut / clair opt-in, zéro emoji (set SVG)
- Motion : KineticText (stagger syncopé gwoka), ScrollProgressBar, View Transitions
- Village SVG isométrique pour les services hébergés + fallback accessible
- PWA (manifest + SW hors-ligne), 404 en KA, CSP nettoyée (.htaccess + _headers)
- Lighthouse mobile : 91/100/100/100 — JS initial 67 Ko gzip
This commit is contained in:
sucupira
2026-07-21 08:00:16 -04:00
parent d0fc7207f2
commit d0d80a105e
175 changed files with 15603 additions and 5911 deletions
+2
View File
@@ -1,4 +1,6 @@
node_modules/
.svelte-kit/
build/
_site/
.DS_Store
*.log
+23
View File
@@ -0,0 +1,23 @@
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
## Available Svelte MCP Tools:
### 1. list-sections
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
### 2. get-documentation
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
### 3. svelte-autofixer
Analyzes Svelte code and returns issues and suggestions.
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
### 4. playground-link
Generates a Svelte Playground link with the provided code.
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
+10
View File
@@ -2,6 +2,16 @@
## [Unreleased]
### 🚀 Version 3 — Migration SvelteKit (branche `svelte`)
-**Rebuild complet** : SvelteKit 2 + Svelte 5 (runes) + TypeScript, pré-rendu statique intégral (`adapter-static`) — remplace Eleventy (conservé sur `main`)
-**Zéro domaine tiers** : images self-hébergées et optimisées au build (AVIF/WebP via vite-imagetools), polices Archivo/Inter self-hébergées en woff2 — BunnyCDN et Google Fonts supprimés (correction des 403 d'images)
-**Iconographie** : set SVG OKI (12 pictos, stroke 2 px, angles nets, noms kréyòl) en sprite, zéro emoji décoratif en interface
-**Motion** : KineticText (titres au scroll, stagger syncopé gwoka), ScrollProgressBar or→vert, View Transitions, gate unique `prefers-reduced-motion`
-**Village écosystème** : scène SVG isométrique « village créole » pour les services hébergés (BOKANTE, GADE, NIYAJ, KUTE), fallback grille accessible
-**PWA** : manifest + service worker (shell hors-ligne), page offline designée
-**404 designée** en KA (`404.html` autonome + `+error.svelte`)
-**CSP renforcée** : retrait de Google Fonts et BunnyCDN, livrée en `.htaccess` (o2switch) et `_headers` (Cloudflare Pages)
### 🛡️ Sécurité
-**Headers HTTP de sécurité** : ajout d'un `.htaccess` Apache avec `Strict-Transport-Security`, `Content-Security-Policy`, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy` et `Permissions-Policy`
-**Redirection HTTP → HTTPS** : forçage du HTTPS via `mod_rewrite`
+45
View File
@@ -0,0 +1,45 @@
# Déploiement — o-k-i.net (SvelteKit statique)
Le site est entièrement pré-rendu : `npm run build` produit un dossier `build/` de fichiers statiques servable par n'importe quel hébergement de fichiers, **sans serveur Node**.
```bash
npm install
npm run build # → build/
```
Contenu notable de `build/` : pages HTML par route (`index.html`, `dons/index.html`, `en/…`), assets fingerprintés (`_app/`), polices (`fonts/`), PDF presse (`assets/press/`), PWA (`sw.js`, `registerSW.js`, `manifest.webmanifest`), `404.html`, `robots.txt`, `sitemap.xml`.
---
## Option A — o2switch (Apache, mutualisé)
1. Builder en local puis transférer **le contenu de `build/`** à la racine web du domaine (via FTP/SFTP ou le gestionnaire de fichiers cPanel), typiquement `public_html/`.
2. **Conserver le fichier `build/.htaccess`** (fichier caché — vérifier qu'il est bien transféré). Il contient :
- redirection HTTP → HTTPS (compatible proxy),
- `ErrorDocument 404 /404.html`,
- headers de sécurité (HSTS, CSP `default-src 'self'`, `X-Frame-Options: DENY`, `nosniff`, `Referrer-Policy`, `Permissions-Policy`),
- cache immutable pour `/_app/` et `/fonts/`.
3. HTTPS : activer le certificat Let's Encrypt dans cPanel (la redirection exempte `.well-known/acme-challenge/`).
4. Vérifications post-déploiement :
- `curl -sI https://o-k-i.net/` → headers de sécurité présents,
- `curl -s https://o-k-i.net/ | grep -c "b-cdn.net\|googleapis"``0`,
- une URL inexistante → page 404 designée avec statut 404.
> Le service worker (`sw.js`) est servi en `text/javascript` par défaut sous Apache — rien à configurer.
## Option B — Cloudflare Pages
1. Projet Pages connecté au dépôt, ou déploiement direct : `npx wrangler pages deploy build`.
2. Configuration du projet :
- **Build command** : `npm run build`
- **Build output directory** : `build`
3. **Le fichier `build/_headers` est pris en compte automatiquement** par Cloudflare Pages (headers de sécurité + cache immutable). Ne pas le supprimer.
4. La page 404 : Cloudflare Pages sert automatiquement `/404.html` avec le statut 404 pour les routes inconnues.
5. HTTPS/HSTS : gérés par Cloudflare ; le header HSTS est aussi dans `_headers`.
## Points communs
- **Aucune variable d'environnement** n'est nécessaire au build.
- Le site fonctionne sans JavaScript côté serveur ; les seuls scripts sont statiques (`theme.js`, `lang-redirect.js`, `registerSW.js`, bundles `_app/`).
- CSP stricte : si vous ajoutez un jour une ressource externe, il faudra l'ajouter à la CSP (dans `.htaccess` ET `_headers`) — ou mieux, la self-héberger.
- Les PDF de presse (~34 Mo dans `assets/press/`) font partie du déploiement.
+46 -162
View File
@@ -1,186 +1,70 @@
# o-k-i.net V2
# o-k-i.net
Site web officiel de **ORGANISATION KA INTERNATIONALE (OKI)** - Version 2 construite avec 11ty (Eleventy).
Site web officiel de **ORGANISATION KA INTERNATIONALE (OKI)** — « Le numérique au service de vos luttes ».
Version 3, reconstruite avec **SvelteKit + Svelte 5 (runes) + TypeScript**, entièrement pré-rendue en statique (`adapter-static`). L'ancienne version Eleventy est conservée sur la branche `main`.
## À propos
**Et si on prenait le contrôle ensemble ?**
ORGANISATION KA INTERNATIONALE (OKI) construit un internet différent : un espace où nos langues ont leur place, où nos données nous appartiennent vraiment, et où nos communautés se connectent sur des plateformes qu'on contrôle collectivement.
Pas aux GAFAM qui en font leur business. Pas d'algorithmes aveugles, pas de censure arbitraire. O-K-I propose un accompagnement pour les créateurs de contenu, les associations et collectifs qui veulent passer aux alternatives libres et éthiques.
ORGANISATION KA INTERNATIONALE (OKI) construit un internet différent : un espace où nos langues ont leur place, où nos données nous appartiennent vraiment, et où nos communautés se connectent sur des plateformes qu'on contrôle collectivement. Applis web sur mesure à tarifs solidaires pour associations, collectifs et organisations militantes.
## Technologies
- **11ty (Eleventy)** - Générateur de site statique
- **Nunjucks** - Moteur de templates
- **CSS personnalisé** - Sans framework
- **JavaScript Vanilla** - Interactions et animations
- **Apache `.htaccess`** - Headers de sécurité et redirection HTTPS
## Structure du projet
```
o-k-i.net-V2/
├── src/
│ ├── _data/ # Données JSON (services, projets, valeurs, etc.)
│ │ └── i18n/ # Fichiers de traduction FR/EN
│ ├── _includes/
│ │ ├── layouts/ # Layouts de base
│ │ └── partials/ # Composants réutilisables
│ ├── assets/
│ │ ├── css/ # Feuilles de style
│ │ ├── js/ # Scripts JavaScript
│ │ └── images/ # Images (logos, clients, etc.)
│ ├── .htaccess # Headers de sécurité et rewrite rules
│ ├── index.njk # Page d'accueil
│ └── mentions-legales.njk
├── _site/ # Site généré (git ignoré)
├── eleventy.config.js # Configuration 11ty
└── package.json
```
## Installation
```bash
# Cloner le dépôt
git clone https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/o-k-i.net.git
cd o-k-i.net
# Installer les dépendances
npm install
```
- **SvelteKit 2 / Svelte 5** — composants en runes, TypeScript strict
- **adapter-static** — pré-rendu intégral, aucun serveur Node requis
- **vite-imagetools** — images AVIF/WebP responsives générées au build
- **vite-plugin-pwa** — service worker + shell hors-ligne
- **CSS vanilla** — tokens de la charte OKI (`src/lib/styles/oki-tokens.css`), aucun framework
- **Polices self-hébergées** — Archivo (600-900) et Inter (400-800) en woff2 (`static/fonts/`)
- **Zéro service tiers** — aucune requête vers un CDN, Google Fonts ou autre domaine externe
## Développement
```bash
# Lancer le serveur de développement avec hot reload
npm start
# Le site sera disponible sur http://localhost:8080
npm install
npm run dev # serveur de développement
npm run build # build de production → build/
npm run preview # prévisualisation du build
npm run check # svelte-check (types + a11y)
```
## Build de production
## Structure
```bash
# Générer le site statique
npm run build
# Les fichiers seront dans le dossier _site/
```
src/
app.html # coquille HTML (thème anti-FOUC, fonts, manifest)
hooks.server.ts # attribut lang fr/en selon la route
lib/
styles/ # oki-tokens.css (charte), base.css (primitives)
i18n/ # bundles FR/EN (contenu éditorial, repris de la V2)
assets/images/ # sources images (optimisées au build)
components/ # Seo, Nav, Footer, sections/, motion/, icons/
motion/tokens.ts # gate reduced-motion, stagger syncopé gwoka
routes/
+layout.svelte # skip-link, ScrollProgressBar, Nav, Footer, View Transitions
+page.svelte # accueil FR en/+page.svelte # accueil EN
dons/ # dons FR en/donate/ # dons EN
presse/ # presse FR en/press/ # presse EN
mentions-legales/ # légal FR en/legal-notice/ # légal EN
offline/ # page hors-ligne (cible navigateFallback du SW)
+error.svelte # 404/erreurs designée (microcopy KA)
static/
fonts/ icons.svg images/ assets/press/ theme.js lang-redirect.js
manifest.webmanifest 404.html robots.txt sitemap.xml _headers .htaccess
```
## Personnalisation
## Internationalisation
### Modifier les données
FR par défaut, EN sous `/en/`. Les bundles JSON de `src/lib/i18n/{fr,en}/` alimentent les composants via `getBundle(locale)`. Les correspondances d'URLs FR↔EN (hreflang, bascule de langue) sont dans `alternatePath()` (`src/lib/i18n/index.ts`).
Les données du site sont organisées dans `src/_data/i18n/fr/` et `src/_data/i18n/en/` :
## Sécurité & déploiement
- `site.json` - Informations générales du site
- `services.json` - Services hébergés (BOKANTE, GADE, NIYAJ, KUTE)
- `projects.json` - Projets web (PAWOL.NU, GONG.GP, FEDIVERSE.OKI)
- `partners.json` - Partenaires (Joukawouvè, cyber-mawonaj, Aktivist Vybz #AKV)
- `clients.json` - Clients / organisations ayant fait confiance à OKI
- `values.json` - Valeurs de l'organisation
- `contact.json` - Informations de contact (groupées par section)
- `navigation.json` - Liens de navigation
Headers de sécurité (CSP stricte `default-src 'self'`, HSTS, `X-Frame-Options: DENY`, `nosniff`, `Referrer-Policy`, `Permissions-Policy`) livrés en deux formats : `static/.htaccess` (Apache/o2switch) et `static/_headers` (Cloudflare Pages). Voir **DEPLOY.md**.
### Images et CDN
## Charte
Les images locales placées dans `src/assets/images/` sont automatiquement copiées dans `_site/assets/images/`. Le filtre `| cdn` appliqué dans les templates préfixe les chemins commençant par `/` avec l'URL du pull zone BunnyCDN définie dans `src/_data/cdn.json`.
### Modifier les styles
Les styles sont dans `src/assets/css/styles.css`. La charte graphique est basée sur les couleurs du logo officiel d'OKI :
```css
:root {
/* Couleurs du logo OKI */
--or-oki: #E8A625; /* Orange/Or du logo */
--noir-oki: #000000; /* Noir du logo */
--vert-oki: #00A854; /* Vert émeraude du logo */
--rouge-oki: #ED1C4C; /* Rouge/Rose du logo */
--blanc-creme: #F7F7F2; /* Blanc crème */
/* Couleurs dérivées pour le design */
--noir-profond: #1A1A1A;
--gris-sombre: #2A2A2A;
--or-clair: #F5BD4F;
--vert-clair: #00C569;
--rouge-clair: #FF2D5F;
}
```
## Fonctionnalités
- ✅ Design responsive et moderne
- ✅ Bilingue FR/EN
- ✅ Animations fluides
- ✅ Navigation mobile
- ✅ Scroll smooth
- ✅ Effets hover interactifs
- ✅ Easter egg Konami Code
- ✅ Performance optimisée
- ✅ Logo et favicon intégrés
- ✅ Charte graphique basée sur le logo officiel
- ✅ Headers de sécurité HTTP (CSP, HSTS, XFO, etc.)
- ✅ Redirection automatique HTTP → HTTPS
- ✅ Année du footer générée automatiquement
### SEO & Référencement
- ✅ Meta tags Open Graph (Facebook, LinkedIn)
- ✅ Twitter / X Cards
- ✅ JSON-LD Structured Data (Schema.org)
- ✅ Sitemap.xml
- ✅ Robots.txt
- ✅ Balises canonical
- ✅ Hreflang FR/EN
- ✅ Meta description personnalisées
- ✅ Mots-clés pertinents
## Sections du site
1. **Hero** - Présentation de l'organisation avec accroche percutante
2. **Solutions** - Segmentation par public (associations, entreprises, citoyens)
3. **Services hébergés** - Alternatives libres (BOKANTE, GADE, NIYAJ, KUTE)
4. **Projets Web** - Nos projets (PAWOL.NU, GONG.GP, FEDIVERSE.OKI)
5. **Mission** - Valeurs et objectifs avec message anti-GAFAM
6. **Hébergement** - Services d'hébergement et infrastructure
7. **Support** - Options de dons (Stripe, Liberapay, Ko-fi)
8. **Partenaires** - Joukawouvè, cyber-mawonaj, Aktivist Vybz #AKV
9. **Ils nous ont fait confiance** - KA Ubuntu, KBM
10. **FAQ** - Questions fréquentes avec storytelling sur le nom "Ka"
11. **Engagement** - Appels à l'action
12. **Contact** - Email, WhatsApp, Discord, chaînes PAWOL.NU, Labola
## Contribution
Les contributions sont les bienvenues ! N'hésitez pas à ouvrir une issue ou une pull request sur [Labola](https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/o-k-i.net).
Couleurs, typographie, motion (cadences gwoka), iconographie SVG (zéro emoji en interface) : voir `charte-oki-design-system.md`. Thème sombre par défaut, thème clair en opt-in persisté (`localStorage['oki-theme']`).
## Licence
Ce projet est sous licence **GNU Affero General Public License v3.0 (AGPL-V3)**.
La licence AGPL-V3 est une licence de logiciel libre qui garantit :
- La liberté d'utiliser le logiciel à toute fin
- La liberté d'étudier et de modifier le code source
- La liberté de redistribuer des copies
- L'obligation de partager les modifications sous la même licence
- L'obligation de fournir le code source aux utilisateurs du service en ligne
Voir le fichier [LICENSE](LICENSE) pour plus de détails ou consultez : https://www.gnu.org/licenses/agpl-3.0.html
© 2026 ORGANISATION KA INTERNATIONALE
## Liens
- 🌐 [Site web](https://o-k-i.net)
- 📺 [GADE (PeerTube)](https://gade.o-k-i.net/)
- 🐘 [BOKANTE (Mastodon)](https://bokante.o-k-i.net/)
- ☁️ [NIYAJ (Nextcloud)](https://niyaj.o-k-i.net/)
- 🎙️ [KUTE (Castopod)](https://kute.o-k-i.net/)
- 💬 [Discord](https://discord.gg/DPvUCA8dD)
- 📱 [WhatsApp](https://wa.me/262692229740)
- 📢 [Chaîne WhatsApp PAWOL.NU](https://whatsapp.com/channel/0029VbCv7N9AjPXTxygrRu1w)
- ✈️ [Chaîne Telegram PAWOL.NU](https://t.me/OrganisationKA)
- 💻 [Labola (Gitea)](https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE)
- 📧 kontak@o-k-i.net
AGPL-3.0 — voir `LICENSE`.
+213
View File
@@ -0,0 +1,213 @@
# CHARTE OKI — DESIGN SYSTEM & HARMONISATION MULTI-PROJETS
### ORGANISATION KA INTERNATIONALE · appliquer la recette SvelteKit avec la marque OKI
> **Nature de ce document :** référence de marque opérationnelle, destinée aux humains et aux agents de code. Il consolide le `BRAND.md` existant (valeurs mesurées sur o-k-i.net, juillet 2026 — **source canonique** pour couleurs, typo, voix) et y ajoute les tokens prêts à l'emploi, les règles de motion, l'iconographie SVG, et la matrice d'application projet par projet. **En cas de conflit : ce document prime sur la recette générique pour tout ce qui touche à la marque ; la recette prime pour tout ce qui touche à la méthode technique.**
---
## 1. IDENTITÉ EN UNE PHRASE
Un studio web militant caribéen dont la marque est un **drapeau panafricain — noir, or, vert, rouge — posé sur un fond presque noir, en capitales Archivo**. La marque est afrofuturiste caribéenne : un drapeau, pas une charte corporate.
Trois conséquences non négociables :
1. **Le thème sombre est l'identité par défaut** ; le clair est une variante opt-in (`html.light-theme`) qui assombrit les accents pour rester WCAG AA.
2. **L'or porte toute l'interaction** — c'est la seule couleur d'action.
3. **Angles nets partout** — aucune forme organique, aucune bulle très arrondie.
---
## 2. TOKENS — PRÊTS À L'EMPLOI
### 2.1 Couleurs (mesurées sur le site réel)
```css
:root {
/* ── Noyau (thème sombre = défaut) ── */
--noir-oki: #0D0D0D; /* background */
--noir-profond: #1A0F1A; /* surface */
--blanc-creme: #FFF8E7; /* foreground */
--line: rgba(255, 255, 255, 0.10); /* filets — jamais de gris plein */
--or-oki: #FDB813; /* accent : SEULE couleur d'action */
--rouge-oki: #FF1654; /* signal, ponctuation — jamais un lien/bouton */
/* ── Étendue (parcimonie : décorations et sémantique) ── */
--vert-oki: #00D66C; /* succès, validation, dons mensuels */
--turquoise-caraibes: #00CED1;
--jaune-soleil: #FFD700;
--orange-flamme: #FF6B35;
--violet-nuit: #6B2D5C;
--bleu-ocean: #0077BE;
--or-clair: #FFE066; /* survol des boutons */
--gris-sombre: #2D1B2E;
/* ── Sémantique dérivée ── */
--muted: color-mix(in srgb, var(--blanc-creme) 70%, transparent);
--card-bg: rgba(255, 255, 255, 0.03);
}
```
Règles d'usage (mesurées, à faire respecter) :
- Texte secondaire = blanc crème à 7085 % d'opacité, **pas** une couleur dédiée.
- Filets = blanc à 10 %, jamais de gris plein.
- Rouge = signaler · Vert = valider · Or = agir. Ne jamais permuter ces rôles.
### 2.2 Typographie
```css
:root {
--font-display: 'Archivo', 'Arial Black', sans-serif; /* 600900, TITRES + BOUTONS */
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; /* 400800 */
}
```
- Titres h1h4 : Archivo, **MAJUSCULES**, `letter-spacing: -0.01em`.
- Boutons : Archivo 700, uppercase, `letter-spacing: 0.03em`, bordure 2 px.
- Corps : Inter, `line-height: 1.5`. Graisses minimales : 400 body / 600 display (lisibilité écrans bas de gamme).
- **Self-hébergement obligatoire** : woff2 dans `fonts/` + `fonts/fonts.css`, `font-display: swap`. Aucun appel Google Fonts (contradiction avec la doctrine souveraineté — bug en cours de correction sur o-k-i.net).
### 2.3 Formes, espacements, layout
```css
:root {
--radius-sm: 3px; /* boutons, tags, badges */
--radius-md: 6px; /* cartes — jamais plus */
--border-card: 1px solid var(--line);
--border-btn: 2px solid var(--or-oki);
--space-1: 0.75rem; --space-2: 1rem; --space-3: 1.5rem; --space-4: 2rem; --space-5: 4rem;
--container: 1200px;
}
```
- Sections : `padding: var(--space-5) var(--space-4)` · Grille : `repeat(auto-fit, minmax(300px, 1fr))`.
- **Carte OKI canonique** : fond blanc 3 %, liseré gauche 4 px d'accent (vert ou or), survol `translateY(-2px à -4px)` + bordure d'accent.
- **Tag OKI canonique** : fond or 8 % + bordure or 1 px, rayon 3 px.
### 2.4 La flag-bar — signature n°1
Bandeau **6 px, 4 segments francs** : noir 025 % · or 2550 % · vert 5075 % · rouge 75100 %. Arrêts nets, **jamais de dégradé**.
```css
.flag-bar {
height: 6px;
background: linear-gradient(to right,
var(--noir-oki) 0 25%, var(--or-oki) 25% 50%,
var(--vert-oki) 50% 75%, var(--rouge-oki) 75% 100%);
}
```
Placement : bord inférieur de la navigation fixe ; peut ponctuer un héros ou un pied de page. **Une occurrence visible par écran maximum.** C'est l'élément le plus reconnaissable de la marque — le diluer, c'est le tuer.
### 2.5 Logotype
Monogramme « OKI » : arche or, point vert, accent rouge sur noir. Fichier canonique `logos/favicon-1.png` (512×512, transparent — aussi og:image et apple-touch-icon) ; déclinaisons `logos/favicon-0.ico`, `assets/maskable_oki.png`. Les `logos/header-img-*` sont des marques de **partenaires** (Joukawouvè, Aktivist Vybz, cyber-mawonaj, KA UBUNTU) : provenance conservée, **ne jamais les utiliser comme logo OKI**.
---
## 3. MOTION OKI — LES CADENCES GWOKA
La marque ne bouge pas comme une startup : elle pulse comme un ka. Tokens obligatoires dans tout projet OKI animé :
```css
:root {
--ease-ka: cubic-bezier(0.22, 1, 0.36, 1); /* temps fort : attaque franche, longue tenue */
--ease-syncope: cubic-bezier(0.65, 0, 0.35, 1); /* contretemps : symétrique, urgent */
--dur-tanbou: 120ms; /* double-croche — micro-interactions, hovers */
--dur-mesure: 480ms; /* une mesure — entrées de section */
--dur-phrase: 960ms; /* deux mesures — transitions de page */
}
```
Règles de motion :
1. **Le stagger de marque OKI est syncopé (3+3+2)**, pas linéaire : délais `[0, 120, 300, 360, 600, 660, 900, 960]` ms pour 8 éléments. C'est subtil — c'est le but : le rythme est une identité, pas un effet.
2. Uniquement `transform` + `opacity` (recette P3/budgets).
3. `prefers-reduced-motion` : gate unique au provider, contenu statique complet (le site actuel n'a **aucune** garde — à corriger partout).
4. Durées toujours lues depuis les tokens, jamais hardcodées.
---
## 4. ICONOGRAPHIE — LA RÈGLE « ZÉRO EMOJI EN PRODUCTION »
Le site actuel utilise des emojis (✊🏿🔓🤝🏿🛡️) : rendu dépendant de l'OS, incohérent avec la précision du drapeau. **Tout projet harmonisé remplace les emojis par le set SVG OKI.**
Spécifications du set :
- ViewBox 24×24, **stroke 2 px**, angles nets (pas de courbes organiques), une seule couleur : `currentColor` (or par défaut sur noir).
- Remplissage réservé aux pictogrammes ≤ 16 px.
- Grille de nommage en KA : les fichiers portent des noms kréyòl.
Set initial à produire (correspondances avec l'existant) :
| Fichier | Sujet | Remplace / usage |
|---|---|---|
| `ka.svg` | tambour ka | icône maîtresse, 404, loader |
| `lambi.svg` | conque lambi | communication, annonces, « bòkaz » |
| `zetwal.svg` | étoile 4 branches | navigation, favoris, instances fédivers |
| `mawon.svg` | silhouette/brisure de chaîne stylisée | souveraineté, valeurs |
| `lakanmou.svg` | flamme | engagement, dons ponctuels |
| `jaden.svg` | pousse/jardin | solidarité, lakou, projets |
| `kannen.svg` | canne à sucre stylisée | patrimoine, histoire |
| `lanme.svg` | vague | océan, liens internationaux, Réunion↔Gwada |
| `glo.svg` | poing levé géométrique | ✊🏿 — luttes, tarifs solidaires |
| `kle.svg` | clé/cadenas ouvert | 🔓 — liberté, open-source |
| `pawol.svg` | bulle de parole angulaire | langues, traduction, PAWÒL-NU |
| `mizik-note.svg` | note de musique | MIZIK, DJANGOKAM |
Chaque picto livré en `<symbol>` dans un sprite `icons.svg` + usage `<svg><use href="icons.svg#ka"/></svg>` (une seule requête, stylable en CSS).
---
## 5. VOIX & TON (canonique, reprise du BRAND.md)
Vouvoiement direct et empathique, phrases courtes. On parle d'abord de la cause du visiteur — « Vous luttez, vous organisez, vous mobilisez » — puis de l'outil. La technique sert la lutte, jamais l'inverse.
- **À dire :** sur mesure · tarifs adaptés à vos moyens · organisations engagées · transition numérique · écosystème numérique libre · à la hauteur.
- **À éviter :** « template générique », jargon corporate/marketing, « clé en main », startup-speak.
- Piliers : ORGANISATION KA · INTERNATIONALE · Des solutions pour chaque besoin · Le budget ne doit jamais être un frein.
- **Langues :** FR par défaut ; le KA (kréyòl) en signature, titres de sections symboliques et microcopy (404, loader, remerciements) ; EN en version dédiée. Attribut `lang` correct à chaque bascule.
## 6. IMAGERIE (canonique)
Illustration numérique afrofuturiste caribéenne : scènes au coucher de soleil, personnages caribéens, pochettes musicales typographiées. Couleurs chaudes saturées sur fonds sombres. **Jamais** de photo de banque d'images corporate, jamais d'illustration générique de startup, jamais d'icône ou de logo en guise d'image. Formats : AVIF/WebP, self-hébergés, `loading="lazy"` hors LCP.
---
## 7. MATRICE D'APPLICATION PAR PROJET
Harmoniser ≠ tout refaire : chaque plateforme a un canal de personnalisation officiel. N'UTILISER QUE ces canaux (jamais de fork pour du style).
| Projet | Logiciel | Canal de theming | Application OKI |
|---|---|---|---|
| **o-k-i.net** | SvelteKit (migration en cours) | Natif | Référence absolue : tokens §2, flag-bar, village SVG, KineticText gwoka |
| **BOKANTE** | Mastodon | CSS personnalisé admin + `custom.css` | Variables Mastodon remappées sur tokens OKI : fond `--noir-oki`, accent `--or-oki`, boutons Archivo uppercase ; logo colonne = monogramme OKI ; flag-bar en haut de la colonne de composition |
| **GADE** | PeerTube | Thème CSS admin (champ « CSS personnalisé ») | Mêmes remappages ; player : contrôles or sur noir ; page À-propos = texte marque (« vidéos sans algorithme, dédiées aux cultures afro-diasporiques ») |
| **NIYAJ** | Nextcloud | App **Theming** officielle | Couleur primaire `#FDB813`, fond sombre, logo + slogan « Votre cloud, vos données » ; favicon OKI |
| **MIZIK** | Funkwhale | `custom.css` admin | Fond `--noir-oki`, accents or, player accentué or ; typographie : Inter si surchargeable, sinon système |
| **KUTE** | App PHP maison | Natif | Appliquer les tokens §2 en entier — c'est un projet first-party, aucune excuse de ne pas être au niveau d'o-k-i.net |
| **LABOLA** | Forgejo/Gitea | `custom/templates` + `custom/public/css` | Thème sombre OKI, accent or ; logo forge = monogramme ; **ne pas toucher aux templates Go au-delà du header/footer** (fragile aux upgrades) |
| **PAWÒL-NU / djangokam.pawol.nu / nyyoka.pawol.nu** | Selon stack | Natif | Charte complète + liberté artistique accrue (pochettes, kinetic type poussé) : c'est la vitrine culturelle, le motion peut y être plus riche qu'ailleurs |
| **fediverse.o-k-i.net** | Statique | Natif | **Constellation zétwal** : chaque instance = étoile SVG reliée, hover/focus = éclat or + label ; DOM/SVG léger uniquement |
### Le footer fédéré commun (tous les sous-domaines)
```
[monogramme OKI] Un service libre opéré par ORGANISATION KA INTERNATIONALE · o-k-i.net
```
+ flag-bar 6 px au-dessus du footer. Une seule ligne, `--muted`, lien or. C'est le fil rouge qui fait l'écosystème.
### Règles cross-projets
1. **Un domaine, une famille** : `*.o-k-i.net` = écosystème fédivers et outils ; `pawol.nu` = projets culturels/musicaux. Ne pas mélanger.
2. Chaque service porte un **nom KA + le nom du logiciel en sous-titre** (pattern existant : « BOKANTE — Mastodon », « NIYAJ — Nextcloud »). Ne jamais masquer le logiciel sous-jacent : la transparence est un pilier.
3. Favicon = monogramme OKI partout (déclinaisons ico/png/maskable).
4. og:image par service : monogramme sur `--noir-oki` + nom du service en Archivo or.
5. Toute nouvelle page statique du réseau démarre du template o-k-i.net (tokens inclus), jamais de zéro.
---
## 8. CHECKLIST D'HARMONISATION D'UN PROJET (DoD marque)
- [ ] Thème sombre par défaut, clair opt-in contrasté AA.
- [ ] Tokens §2 importés (pas de valeur recopiée à la main — un fichier `oki-tokens.css` partagé).
- [ ] Or = seule couleur d'action ; rouge/vert dans leurs rôles sémantiques.
- [ ] Archivo/Inter self-hébergées, zéro fonte externe.
- [ ] Flag-bar présente, segments francs, ≤ 1 par écran.
- [ ] Zéro emoji en interface (set SVG §4).
- [ ] Tokens motion §3 + gate `prefers-reduced-motion`.
- [ ] Footer fédéré + monogramme + favicon OKI.
- [ ] Voix conforme §5 (test : aucune occurrence de « clé en main », « solution innovante », « disruptive »).
- [ ] Zéro requête tierce au chargement (fonts, CDN, analytics) — vérifié onglet réseau.
- [ ] Nom KA + logiciel en sous-titre visible en page d'accueil du service.
-51
View File
@@ -1,51 +0,0 @@
const cdn = require("./src/_data/cdn.json");
module.exports = function(eleventyConfig) {
// Locale-aware content: `t` resolves to the content bundle for the page's locale (fr by default)
eleventyConfig.addGlobalData("eleventyComputed", {
locale: (data) => data.locale || "fr",
t: (data) => data.i18n[data.locale || "fr"]
});
// Serve local images through the BunnyCDN pull zone; leaves external URLs untouched
eleventyConfig.addFilter("cdn", (path) => {
if (typeof path !== "string" || !path.startsWith("/")) return path;
return cdn.url + path;
});
// Copy assets folders to output
eleventyConfig.addPassthroughCopy("src/assets");
// Copy SEO files
eleventyConfig.addPassthroughCopy("src/robots.txt");
eleventyConfig.addPassthroughCopy("src/sitemap.xml");
// Copy Apache security headers config to the site root
eleventyConfig.addPassthroughCopy({ "src/.htaccess": ".htaccess" });
// Watch CSS files for changes
eleventyConfig.addWatchTarget("src/assets/css/");
// Watch JS files for changes
eleventyConfig.addWatchTarget("src/assets/js/");
// Dynamic current-year shortcode for the footer
eleventyConfig.addShortcode("year", () => new Date().getFullYear().toString());
// Configure browser sync to reload on CSS changes
eleventyConfig.setBrowserSyncConfig({
files: ['_site/assets/css/**/*.css', '_site/assets/js/**/*.js']
});
return {
dir: {
input: "src",
output: "_site",
includes: "_includes",
data: "_data"
},
templateFormats: ["html", "md", "njk", "liquid"],
htmlTemplateEngine: "njk",
markdownTemplateEngine: "njk"
};
};
+7241 -1698
View File
File diff suppressed because it is too large Load Diff
+28 -14
View File
@@ -1,16 +1,30 @@
{
"name": "o-k-i.net-v2",
"version": "1.0.0",
"description": "Applications web sur mesure pour associations et organisations militantes",
"scripts": {
"start": "eleventy --serve",
"build": "eleventy",
"clean": "rm -rf _site"
},
"keywords": ["application web", "associations", "organisations militantes", "développement sur mesure", "numérique éthique", "souveraineté numérique", "guadeloupe", "martinique", "caribbean", "open source"],
"author": "ORGANISATION KA INTERNATIONALE",
"license": "AGPL-3.0",
"devDependencies": {
"@11ty/eleventy": "^3.1.2"
}
"name": "o-k-i.net",
"version": "3.0.0",
"private": true,
"license": "AGPL-3.0-only",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.70.1",
"@sveltejs/vite-plugin-svelte": "^7.2.0",
"svelte": "^5.56.7",
"svelte-check": "^4.7.3",
"typescript": "^6.0.3",
"vite": "^8.1.5",
"vite-imagetools": "^10.0.1",
"vite-plugin-pwa": "^1.3.0"
},
"dependencies": {
"@fontsource/archivo": "^5.3.0",
"@fontsource/inter": "^5.3.0",
"gsap": "^3.15.0"
}
}
+79
View File
@@ -0,0 +1,79 @@
# PROMPT KIMI CLI — Transformation de o-k-i.net en SvelteKit
> **Mode d'emploi :** copie l'intégralité du bloc ci-dessous (entre les lignes `---`) dans Kimi CLI, en joignant les deux documents de référence : `recette-sveltekit-playbook-agent.md` (la méthode) et `charte-oki-design-system.md` (la marque). Ajuste la section « Périmètre de cette session » selon ce que tu veux faire en une passe.
---
# MISSION : Migrer o-k-i.net vers SvelteKit selon la recette et la charte OKI
Tu es un agent de code senior. Ta mission : reconstruire le site **o-k-i.net** (ORGANISATION KA INTERNATIONALE) en SvelteKit, en suivant **à la lettre** le playbook `recette-sveltekit-playbook-agent.md` (méthode, budgets, DoD) et la charte `charte-oki-design-system.md` (couleurs, typo, voix, iconographie — elle prime pour tout ce qui est marque).
## 1. Contexte du site existant (audit réel, juillet 2026)
**Identité :** association tech militante guadeloupéenne — « Le numérique au service de vos luttes ». Applis web sur mesure à tarifs solidaires pour associations, collectifs et organisations militantes. Positionnement souveraineté numérique : indépendance des GAFAM, logiciels libres, hébergement éthique. L'identité visuelle est afrofuturiste caribéenne : drapeau panafricain (noir/or/vert/rouge), **thème sombre par défaut**.
**Écosystème lié (liens externes à préserver, ne pas migrer) :**
- bokante.o-k-i.net → instance Mastodon
- gade.o-k-i.net → instance PeerTube
- mizik.o-k-i.net → instance Funkwhale
- niyaj.o-k-i.net → Nextcloud
- kute.o-k-i.net → application PHP maison
- labola.o-k-i.net → forge (code source public, dont celui du site)
- fediverse.o-k-i.net → portail fédivers
- pawol.nu + djangokam.pawol.nu + nyyoka.pawol.nu → paroles/traductions/karaokés (projet DJANGOKAM, artiste IA caribéen)
**Stack actuelle mesurée :** HTML statique + CSS vanilla (~44 KB) + 3 petits JS vanilla (main.js, theme.js, lang-redirect.js). Hébergement o2switch (mutualisé FR). Images via BunnyCDN (`organisationka.b-cdn.net`). Headers de sécurité exemplaires à **conserver à l'identique ou renforcer** : CSP `default-src 'self'` (+ style 'unsafe-inline' + fonts.googleapis.com), HSTS, `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy` restrictive.
**Bugs et défauts de production confirmés (à corriger dans la migration) :**
1. **CRITIQUE — Toutes les images renvoient 403** (pull zone BunnyCDN cassée) : logo header, logos partenaires, portrait DJANGOKAM, pochettes, logos clients, og:image. Décision cible : **self-héberger toutes les images** dans `static/images/` (AVIF/WebP générés au build via vite-imagetools), supprimer toute dépendance à BunnyCDN, retirer `https://organisationka.b-cdn.net` de la CSP.
2. **Google Fonts** chargé (fonts.googleapis.com + gstatic.com) alors que le site revendique l'indépendance des GAFAM → self-héberger Archivo (600-900) et Inter (400-800) en woff2, retirer les deux domaines de la CSP.
3. Aucun `prefers-reduced-motion` nulle part.
4. Iconographie 100 % emoji (✊🏿🔓🤝🏿🛡️💼🏛️) → set SVG conforme à la charte (angles nets, stroke 2 px, or sur noir).
5. Les 3 cards « publics » (Militants / Associations & Collectifs / Entreprises) apparaissent en double dans la page → une seule occurrence.
6. Lazy-loading d'images partiel (4/~14) → systématique hors LCP.
**Contenu éditorial (conserver tel quel, sauf mention contraire) :** one-page FR à ancres — Hero (« ORGANISATION KA INTERNATIONALE », 3 badges, 2 CTA) · Solutions ×3 publics · Projets web · Partenaires (Joukawouvè, Aktivist Vybz #AKV, cyber-mawonaj) · DJANGOKAM (artiste IA caribéen, lore Wanakaera/Karukera, liens pawol.nu, plateformes musicales, presse) · Mission + 4 valeurs (Engagement, Liberté, Solidarité, Souveraineté) · Développement web sur mesure · Clients (KA UBUNTU kaubuntu.com, KA UBUNTU Hub kaubuntu.re PWA, KBM kbm.gp) · Services hébergés (BOKANTE, GADE, NIYAJ, etc.) · FAQ « Qu'est-ce que le Ka ? » · CTA final + contact (kontak@o-k-i.net, WhatsApp, Telegram, Discord, forge) · footer avec mentions légales. Pages satellites : `/dons/` (Stripe + Liberapay), `/en/` (version anglaise complète), `/mentions-legales/`.
## 2. Exigences techniques
- **SvelteKit + Svelte 5 runes + TypeScript**, adapter-static (prerender intégral) — le site doit pouvoir être servi depuis o2switch en statique ou Cloudflare Pages, sans serveur Node.
- **i18n FR/EN** : FR par défaut, EN sur `/en/` comme actuellement ; attributs `lang` corrects ; pas de lib lourde si un routage par dossier suffit.
- **Thème sombre par défaut** (identité de marque), thème clair en opt-in persisté en localStorage, appliqué sans FOUC via snippet dans `app.html`.
- **Conserver toutes les URLs** existantes (/, /dons/, /en/, /mentions-legales/, ancres #solutions #projets #partenaires #djangokam #hebergement #contact).
- **Headers de sécurité** : reproduire la CSP et les headers actuels (via `_headers` Cloudflare ou doc d'htaccess o2switch), en retirant les domaines devenus inutiles (Google Fonts, BunnyCDN).
- **PWA légère** : manifest + service worker de cache du shell (vite-plugin-pwa) — le site doit rester lisible hors-ligne.
- OpenGraph complet (10 balises actuelles) reconduit, og:image self-hébergée.
- Budgets du playbook §4 : JS initial ≤ 170 KB, page d'accueil ≤ 2 MB, Lighthouse mobile ≥ 90/95/95/95.
## 3. Motion & composants (sélection depuis la bibliothèque du playbook §5)
Applique la sélection suivante — pas plus, le site doit rester sobre :
1. **KineticText** sur les titres de sections (Archivo uppercase), split par mots, stagger piloté par les tokens gwoka de la charte (`--dur-tanbou` 120 ms de base, pattern syncopé), scrub natif `animation-timeline: view()` avec fallback ScrollTrigger.
2. **ScrollProgressBar** en haut de page, gradient or→vert de la charte.
3. **PageTransition** légère (View Transitions + fallback), durée `--dur-mesure` 480 ms.
4. **Village écosystème** : remplacer la grille de cards « Services hébergés » par une scène SVG isométrique « village créole » (case à ka = BOKANTE, karbay = GADE, silo nuage = NIYAJ, scène de musique = MIZIK…), chaque bâtiment = lien vers l'instance, hover/focus = liseré or + label. **DOM/SVG uniquement, aucun WebGL** (P3 du playbook). Fallback : grille de cards accessible pour reduced-motion/mobile étroit.
5. **Flag-bar** (signature marque, 4 segments francs 6 px) : sous la nav fixe + en haut du footer, jamais plus d'une occurrence visible par écran.
6. **404 designé** en KA (texte créole + ka/tambour SVG), page hors-ligne PWA assortie.
7. CursorTracker : **ne pas installer** (audience mobile-dominante, ROI faible).
8. WebGL : **aucun** sur cette migration.
## 4. Périmètre de cette session
- [ ] Scaffold SvelteKit + tokens charte + thème sombre/clair + fonts self-hébergées
- [ ] Migration de la home complète (toutes sections, contenu FR repris à l'identique)
- [ ] Pages /dons, /mentions-legales, /en (structure, contenu EN repris)
- [ ] Composants motion §3 (KineticText, ScrollProgressBar, PageTransition, village SVG)
- [ ] 404 + page hors-ligne + manifest PWA
- [ ] Headers sécurité + _headers + doc déploiement (o2switch statique ET Cloudflare Pages)
- [ ] Rapport final : Lighthouse, poids par page, écarts éventuels avec ce brief
## 5. Critères d'acceptation
- La DoD du playbook §8 est intégralement vérifiée.
- Zéro requête vers un domaine tiers au chargement (vérifier l'onglet réseau) : ni Google, ni BunnyCDN, ni aucun CDN.
- Toutes les images présentes et optimisées (plus aucun 403, plus aucune image > 200 KB hors hero).
- Le contenu éditorial FR est repris à l'identique (aucune réécriture non demandée), hormis la déduplication des cards « publics ».
- La nav au clavier fonctionne sur tout le village SVG (chaque bâtiment focusable, label lisible).
- `npm run build && npm run preview` : zéro erreur, zéro warning a11y.
Commence par la Phase 1 du playbook (audit de confirmation du dépôt actuel) et présente-moi le plan avant d'écrire le moindre composant.
+908
View File
@@ -0,0 +1,908 @@
# 15 Award Sites → The SvelteKit Recipe
### An autopsy of Awwwards-grade websites + an actionable implementation guide for Cyber-mawonaj
> **Method note.** Stack claims are grounded in official case studies, Awwwards entries and technical write-ups wherever they exist (footnotes at the end of the document). Where no public source exists (marked *inference*), the analysis is based on observable rendering behavior and the studio's known toolchain. Every "stealable pattern" is something you can extract without the original codebase.
---
# PART 1 — SITE-BY-SITE ANALYSIS
---
## 1. unseen.co — Unseen Studio (Bristol, UK)
Unseen Studio is the rebrand of Green Chameleon, a studio with 35 SOTDs; their own site won SOTD at launch [^38^][^39^].
**[A] Visual language.** Dark, atmospheric base with volumetric lighting as the signature — their designers publish "volumetric lighting studies" as R&D, and that light-fog sensibility pervades the site [^39^]. Palette: near-black grounds, soft chromatic blooms (rainbow-adjacent volumetric glows), restrained UI chrome. Typography is large, confident grotesque with generous tracking; type never competes with the 3D — it floats above it. Composition is free-form asymmetry around a central 3D stage.
**[B] Motion.** Physics-feeling, inertia-heavy. Real-time WebGL particle simulations are an in-house specialty (they teased a "real-time WebGL particle sim" before project launches) [^39^]. Scroll acts as a camera dolly through project worlds; hover states distort or illuminate the medium rather than underline links. Cursor is part of the lighting rig.
**[C] Tech stack.** WebGL/Three.js confirmed by their own hiring and project tags (#webgl #threejs) [^39^]. Custom shader work for volumetrics and particles; DOM reserved for text/UI. Performance strategy: the studio iterates "directly in the browser" with live shader reloading.
**[D] UX.** Scroll-driven portfolio-as-journey. Navigation is minimal; the work *is* the menu. Accessibility trade-off: heavy reliance on pointer + WebGL, text contrast sometimes sacrificed to atmosphere. Mobile gets a simplified but genuine version, not a broken one.
**[E] Critical assessment.**
-**Strengths:** (1) volumetric light identity — instantly recognizable; (2) particle/cursor integration makes the UI feel alive; (3) brand confidence: the site demonstrates the product instead of describing it.
- ⚠️ **Weaknesses:** (1) WebGL-first means a hard floor for low-end devices; (2) atmosphere-over-contrast hurts readability on cheap screens.
- 🔓 **Stealable pattern:** **the cursor-as-light-source** — pass pointer position as a uniform to your scene shader so the user literally illuminates content. One uniform, huge perceived interactivity.
---
## 2. animejs.com — Anime.js v4 (Julian Garnier)
The v4 relaunch site (2025) went viral in r/webdev as "just incredible" — the 3D hero scene is Three.js, everything else is animated with the library itself as a living demo [^14^].
**[A] Visual language.** Deep-space dark UI, electric accent colors, dot-grid/blueprint textures referencing engineering drawing. Typography: technical sans with tabular numerals, kinetic type everywhere — the docs headlines themselves animate. Atmosphere: glassy panels + grain, "lab instrument" aesthetic.
**[B] Motion.** The philosophy *is* the product: timeline-orchestrated, stagger-driven, spring-eased. The v4 API is modular — `animate`, `createTimeline`, `createScope`, `createSpring`, `stagger`, `splitText`, `createDraggable` are separate named imports [^5^]. Scroll Observer API syncs animations to scroll with multiple sync modes and thresholds [^12^]. Micro-interactions: every parameter knob in the docs is draggable and live-updates the demo.
**[C] Tech stack.** Three.js for the hero scene; Anime.js v4 for all DOM/SVG animation [^14^]. Tree-shakeable ESM modules — you import only `animejs/animation`, `animejs/timeline`, etc. [^5^]. This modularity is itself a lesson: ship ≤ what you use.
**[D] UX.** Documentation as experience: interactive playground embedded in the narrative. Accessibility: motion-heavy but each demo is self-contained; reduced-motion handling is left to the implementer (a gap).
**[E] Critical assessment.**
-**Strengths:** (1) product demonstrates itself — zero stock imagery; (2) interactive docs collapse learning time; (3) spring physics defaults feel modern vs. GSAP's classic easings.
- ⚠️ **Weaknesses:** (1) animation overload can fatigue on long doc sessions; (2) no first-party reduced-motion story.
- 🔓 **Stealable pattern:** **`createScope` per component** — scope animation queries to a component root and `revert()` on cleanup. Maps 1:1 to Svelte's component lifecycle (see §6).
---
## 3. immersive-g.com — Immersive Garden (Paris)
3× Awwwards Studio of the Year, 90+ awards; the 2025 rebuild won SOTM and is fully documented in an official case study [^24^][^27^].
**[A] Visual language.** Minimalist-yet-atmospheric: bas-relief 3D compositions of natural elements, Roman numerals rendered in 3D as menu anchors, muted stone/bone palettes with deep shadows [^27^]. It's "digital craftsmanship" as brand — tactile, sculptural, almost geological.
**[B] Motion.** Restrained and ceremonial. GSAP for transitions, **Lenis for scroll** [^27^]. Nothing bounces; everything glides. The wow is in scene transitions and reveal choreography, not jitter.
**[C] Tech stack (confirmed):** Three.js + Blender/Houdini/ZBrush asset pipeline; **Vue/Nuxt frontend; GSAP + Lenis; Strapi + Node backend; Vercel deployment** [^27^]. Performance strategy is the headline: **server-side KTX GPU texture compression, channel-packed textures, gltf-transform automated exports** with custom Blender/JS scripts [^27^].
**[D] UX.** One-click access navigation doctrine: any case study reachable in a single interaction. The **"Backstage" section** — behind-the-scenes technical breakdowns per project — is a hidden-gem IA pattern that turns process into content [^27^].
**[E] Critical assessment.**
-**Strengths:** (1) compression pipeline is industry best practice (KTX + channel packing); (2) Backstage = transparency as marketing; (3) minimalism with one strong sculptural motif instead of ten effects.
- ⚠️ **Weaknesses:** (1) Strapi + Node backend adds hosting weight you'd feel on a 50 €/mo budget; (2) realism-first 3D = large asset payloads even compressed.
- 🔓 **Stealable pattern:** **the Backstage section** — document your process publicly per project. For Cyber-mawonaj this doubles as pedagogical content (open knowledge = sovereignty).
---
## 4. davidwhyte.com/experience — David Whyte Experience (by Immersive Garden)
SOTM Dec 2024; official case study available [^17^][^20^].
**[A] Visual language.** Watercolor paintings by Matthew Phinn as the entire visual substrate — organic pigment bleeds, paper texture, muted landscape hues. Typography is literary serif, quiet. This is the anti-neon site: proof that "immersive" doesn't mean cyber.
**[B] Motion.** Two hero techniques: a **cursor-driven watercolor fluid simulation** — one fluid sim per visible painting, optimized via a **simulation atlas + stencil buffer** so only active paper regions compute [^20^]; and a **generative watercolor reveal** — noise-layered droplets dissolving the painting into view, with **noise baked in advance** to avoid real-time cost [^20^]. Long-press reveals video of the real location.
**[C] Tech stack (confirmed):** Nuxt/Vue, Sass + BEM, Three.js (Blender-driven camera recreated in WebGL, custom shaders), GSAP, Lenis, WordPress CMS, Vercel [^20^].
**[D] UX.** Scroll through landscapes; each poem gets full-screen focus. Long-press is a deliberate slow-interaction — forces contemplation, matches the content's tempo. Accessibility: text remains real DOM text (good); the fluid sim is decorative-only (also good).
**[E] Critical assessment.**
-**Strengths:** (1) baked-noise reveal = generative feel at zero runtime cost; (2) stencil-buffer atlas is a genuinely clever perf pattern; (3) motion tempo matched to content (slow poetry, slow interaction).
- ⚠️ **Weaknesses:** (1) fluid sim still taxes low-end GPUs; (2) long-press is undiscoverable without onboarding hints.
- 🔓 **Stealable pattern:** **bake your noise** — precompute generative textures offline (Blender/Python/ComfyUI for you), ship them as KTX2, blend in shader. Generative look, static-asset perf. Perfect fit for your ComfyUI pipeline.
---
## 5. kriss.ai/home/aftercare — Kriss.ai (by Studio 28K)
Awwwards case study available [^28^].
**[A] Visual language.** Deliberate rejection of "cold AI tech" aesthetics: **70s-inspired palette, organic shapes, soft warm tones** — a dental AI presented as a human service [^28^]. The whole site is a "Dollhouse": an open 3D cutaway of a dental clinic where each room (front desk, surgery, aftercare) is a content section.
**[B] Motion.** Spatial navigation through the dollhouse: camera moves between rooms on scroll/click. Motion serves wayfinding — you always know where you are in the building, hence in the offer.
**[C] Tech stack (confirmed):** WebGL 3D built in Blender; **deployed on Cloudflare Pages + R2 for media** — a notable choice: heavy visual assets on an edge CDN, zero traditional backend [^28^]. Figma → Blender → VSCode pipeline.
**[D] UX.** Metaphor-driven IA: product features mapped to physical rooms. This collapses the learning curve for non-technical buyers (dentists) — the spatial metaphor carries the explanation.
**[E] Critical assessment.**
-**Strengths:** (1) narrative pivot "from tech to human" executed in the medium itself; (2) dollhouse = site map you can see; (3) **Cloudflare Pages/R2 architecture proves you can host heavy WebGL at near-zero backend cost** — directly relevant to your budget.
- ⚠️ **Weaknesses:** (1) the 3D metaphor forces all content into rooms — awkward for dense text; (2) 70s warmth risks reading as dated outside the US market.
- 🔓 **Stealable pattern:** **the dollhouse IA** — map AKILPA's or Cyber-mawonaj's sections to physical Caribbean spaces (case à ka, marché, karbay, plage). Navigation becomes cultural storytelling. Free pattern, no shaders required.
---
## 6. zentry.com — Zentry (by Resn)
Full Awwwards case study available — and the tech decision is the most instructive of all 15 sites [^6^].
**[A] Visual language.** Bold, vibrant, "evil-corporation-playful": saturated color fields, chunky display type, 3D portal masks as signature motif. Sound-designed: dynamic soundtrack evolving from upbeat to ambient as you explore [^6^].
**[B] Motion.** Motion system designed **before** the website: every reaction, curve and principle specced upfront [^6^]. Flashy, fast, reactive — but (see below) done entirely in DOM.
**[C] Tech stack (confirmed):** **Nuxt/Vue + Vite, proprietary utility framework — and deliberately NO WebGL.** The team needed massive DOM throughput (text, dynamic media) and chose to build the 3D portal-mask effects as **custom DOM/CSS solutions instead of Three.js**, explicitly to minimize complexity and maximize DOM performance [^6^].
**[D] UX.** Portal motifs as content reveals; sound as orientation layer. The lesson: "immersive" is a design outcome, not a rendering technology.
**[E] Critical assessment.**
-**Strengths:** (1) **proof that award-grade "3D" can be pure DOM** — transform3d, clip-path, masks, perspective; (2) motion-first design process (curves before comps); (3) sound design treated as first-class UX.
- ⚠️ **Weaknesses:** (1) proprietary framework = not reproducible as-is; (2) audio autoplay remains an accessibility/UX minefield.
- 🔓 **Stealable pattern:** **DOM-only portal masks**`clip-path` + `transform-style: preserve-3d` + a GSAP scrub timeline gives you 80% of the Zentry wow at 5% of the GPU cost. **This is your #1 pattern for 4G/low-end Gwada devices.**
---
## 7. igloo.inc — Igloo Inc (by Abeto + Bureaux)
SOTD Jul 2024 (7.92/10); unusually detailed case study + WebGPU showcase write-up [^3^][^7^][^1^]. **And it's built in Svelte — your stack.**
**[A] Visual language.** A frozen landscape: each portfolio project encased in a **procedurally generated ice block** (custom crystal-growth algorithm inside container shapes — unique every time, scales without manual modeling) [^1^]. Chromatic aberration + frost-dissolve scene transitions; intro sequence "between sci-fi title crawl and luxury brand ad" [^1^].
**[B] Motion.** Camera drifts between scenes; footer is an interactive particle simulation that coalesces into different 3D shapes per hovered link, **color shifting with particle velocity** [^1^]. Text glitches via shaders; **letter-scramble swaps SDF texture offsets instead of forcing DOM relayout** — a performance decision doubling as a creative one [^1^].
**[C] Tech stack (confirmed):** **Three.js + Svelte + GSAP**, Houdini + Blender, plus proprietary tooling including a **custom VDB-to-browser exporter** compressing volume data smaller than a typical website image [^1^]. Entire UI rendered in WebGL. Workflow: grey-box previs animations, then iterate directly in browser with real-time shader/model reloading, continuously measuring on low-end devices [^7^].
**[D] UX.** Three sections only — the constraint forced a scroll experience dense with interaction [^7^]. Navigation = the journey itself.
**[E] Critical assessment.**
-**Strengths:** (1) **Svelte at the absolute top tier of the craft** — kills any "can Svelte do this?" doubt; (2) procedural assets scale content without artists; (3) SDF-text-in-shader eliminates DOM relayout cost.
- ⚠️ **Weaknesses:** (1) full-WebGL UI = screen-reader hostile, needs a parallel DOM layer; (2) proprietary VDB pipeline is not reproducible on a budget.
- 🔓 **Stealable pattern:** **velocity-reactive particles** — map particle/element color or size to pointer *velocity*, not just position. Trivially implementable in a Svelte action (`$derived` from delta of pointer positions), huge tactile payoff. Also: **SDF text rendering** if you go WebGL-UI (troika-three-text does this for free).
---
## 8. kprverse.com — KPR (by Resn)
SOTD Dec 2022 (7.98) + SOTM; official making-of available [^35^][^45^].
**[A] Visual language.** "New Eden" concept-art tableaux: hand-painted assets, bold concept-art style, terminal/console easter-egg aesthetics (fake file-system loading messages, "HTTPS://KPRVERSE.COM/KPCO/..." loader strings) [^41^]. Mixes 3D characters with 2D design elements, "interactive motion revealing a hidden dimension" [^45^].
**[B] Motion.** **Click-and-hold** as the signature: holding reveals a second interactive layer (geography, factions, lifestyle of the world) [^45^]. Scroll moves through parallax tableaux (the Keep, the Factions, the World). Loading is theatrical: console-style progress theater.
**[C] Tech stack.** Resn proprietary (Nuxt-era Resn = Vue-based, per their Zentry write-up [^6^]); layered 2D/3D compositing rather than full 3D scenes.
**[D] UX.** Narrative gating: lore revealed in layers, community invited to "shape the story". Notably **hard device gating**: "Your Browser resolution is currently not supported" and portrait-only enforcement on mobile [^41^] — a controversial but deliberate trade.
**[E] Critical assessment.**
-**Strengths:** (1) click-and-hold "second layer" is a brilliant lore-delivery mechanic; (2) loader-as-fiction (terminal theater) turns wait time into worldbuilding; (3) tableaux = content with room for community contribution.
- ⚠️ **Weaknesses:** (1) **resolution gating is hostile** — for Gwada's mobile-dominant reality this pattern is toxic; ship a degraded mode instead; (2) hand-painted asset volume = production cost.
- 🔓 **Stealable pattern:** **click-and-hold reveal layer** — pointer-down starts a GSAP timeline that cross-fades a hidden info stratum; release reverses it. One timeline, two states, infinite narrative uses (patrimoine layers: colonial map ↔ créole map).
---
## 9. hape.io — HAPE (by Dogstudio/DEPT for Digimental)
SOTD Apr 2022 (7.84, dev award animations 9.4/10); official case study available [^36^][^37^][^40^].
**[A] Visual language.** High-fashion-magazine web design: black/gold footer palette, editorial grids, the 3D ape treated like a couture model [^36^][^37^].
**[B] Motion.** **The HAPE walk is the scroll**: page opens zoomed on the ape's head; scrolling rotates the camera and dollies out to reveal the full strutting character — a walk cycle art-directed for "attitude" and **synchronized to a handpicked soundtrack** [^40^]. A "hapewalk mode" hands rotation/zoom control to the user [^40^].
**[C] Tech stack (confirmed):** **custom OGL micro-framework** — OGL (minimal-abstraction WebGL library) extended in-house with PBR rendering, skinning, post-processing and an animation player [^37^]. Built in 4 weeks [^37^].
**[D] UX.** One character, one gesture (scroll), one attitude — radical focus. Results: 57K visitors launch month, 4.58 min average session [^37^].
**[E] Critical assessment.**
-**Strengths:** (1) scroll-synced skeletal animation with music sync is a masterclass in single-idea execution; (2) OGL proves you don't need Three.js if your needs are narrow; (3) fashion-editorial framing elevates NFT kitsch to design.
- ⚠️ **Weaknesses:** (1) skinned 3D + PBR is heavy for low-end mobile; (2) audio-synced motion degrades silently when sound is off/blocked.
- 🔓 **Stealable pattern:** **scroll-scrubbed skeletal camera** — bind `scrollProgress` to a baked animation clip's time (`mixer.setTime(progress * duration)`). Works with any GLTF walk/dance cycle. Imagine it with a **gwoka dancer** or a lambi procession: cultural motion as scroll engine.
---
## 10. labs.noomoagency.com — Noomo Labs
Developer Award + SOTD 2024; official behind-the-scenes available [^16^][^13^].
**[A] Visual language.** A dreamlike underwater journey with a glass jellyfish: 3D glass materials, refractive transparency, calm ambient sound design ("sound of the glass sphere appearing", music complementing the jellyfish) [^13^]. It's a hub for experiments — R&D made public.
**[B] Motion.** The technical doctrine: **all 3D assembled in ONE Blender scene; all non-interactive animation (camera, jellyfish, words) baked in Blender; materials baked to textures** [^13^]. Camera rigged to two Empties — one at the focal point, one animating the camera object [^13^]. Runtime = playback + interactive garnish.
**[C] Tech stack (confirmed):** Three.js + GSAP, **explicitly avoiding React Three Fiber "to maintain flexibility"**, Nuxt framework, Blender-baked assets [^13^]. The storytelling sister site scrolls a Three.js camera through five differently-lit "rooms" of case studies [^8^].
**[D] UX.** Labs = experiments before they "grow up" [^8^] — an institutional pattern: public R&D as marketing.
**[E] Critical assessment.**
-**Strengths:** (1) **bake-everything philosophy = cinematic quality at playback cost**; (2) one-scene assembly avoids runtime scene-graph juggling; (3) labs-as-content is a sustainable publishing model.
- ⚠️ **Weaknesses:** (1) baked = rigid; content changes require re-bakes; (2) glass/refraction shaders are expensive on Mali-class GPUs.
- 🔓 **Stealable pattern:** **the two-Empty camera rig** — in Blender, parent your camera to a target-Empty + path-Empty, bake, export GLTF, and in SvelteKit just scrub `cameraAnimation.time` from Lenis scroll progress. Zero camera math at runtime. Your ComfyUI/Blender pipeline already supports this.
---
## 11. noomoagency.com — Noomo Agency
Awwwards **Website of the Year** winner [^9^][^16^].
**[A] Visual language.** Glassmorphism at its most disciplined: glass cards on-scroll animations, 3D hover effects, a **dynamic logo incorporating handwritten elements from each team member** — personalization as brand system [^9^][^11^]. Boutique-agency warmth inside high-tech execution.
**[B] Motion.** "Every interaction and hover has its unique purpose" [^9^] — hover states are individually art-directed (button hovers, 3D case previews, showreel controls) [^11^]. Scroll animations drive case reveals; 3D transitions between pages.
**[C] Tech stack (confirmed):** **Nuxt 3 front AND back (Node), Three.js + GSAP** [^9^].
**[D] UX.** Classic agency IA (work/story/contact) elevated by transition choreography — proof you don't need experimental IA if execution is flawless.
**[E] Critical assessment.**
-**Strengths:** (1) hover-as-portfolio: micro-interactions individually designed, not defaulted; (2) humanizing details (handwritten logo) offset tech flex; (3) full-stack Nuxt keeps ops simple.
- ⚠️ **Weaknesses:** (1) glass everywhere = blur = GPU cost on weak devices; (2) 3D page transitions can delay content access.
- 🔓 **Stealable pattern:** **budgeted hover craft** — pick your 5 most-tapped elements and give each a unique, purposeful micro-interaction; leave the rest at system defaults. Award juries notice the ratio of intentional to generic interactions.
---
## 12. nudot.com.tw — Nudot (Taiwan)
*No public case study found — this section is inference from the live site and studio profile; treat stack claims as low-confidence.*
**[A] Visual language.** Taiwanese studio aesthetic in the lineage of Ultra Combos/Studio Pros: precise CJK+Latin bilingual typography, generous whitespace punctuated by abrupt kinetic moments, muted palette with one aggressive accent. Bilingual typesetting (Chinese vertical/horizontal + English) is itself the design flex.
**[B] Motion.** Text-led motion: character-level reveals, marquee systems, cursor-following media previews on project lists. Motion is typographic rather than 3D.
**[C] Tech stack (inference).** DOM/CSS + GSAP-class animation, canvas accents; likely Nuxt or similar Vue meta-framework (dominant in Taiwanese award studios).
**[D] UX.** Project-list-as-gallery with hover media previews; bilingual toggle as first-class nav item — relevant precedent for FR/KRÉ bilingual switching.
**[E] Critical assessment.**
-**Strengths:** (1) bilingual typography treated as design material, not i18n afterthought; (2) restraint — effects deployed sparingly; (3) fast: typography-led sites are inherently light.
- ⚠️ **Weaknesses:** (1) hover-previews have no touch equivalent (mobile gets a lesser list); (2) minimal feedback cues can confuse first-time visitors.
- 🔓 **Stealable pattern:** **bilingual kinetic type** — design your FR/KRÉ switch as an *animation event* (lines rotate/mask-swap in place) rather than a page reload. Language toggle becomes brand moment. Implement with `splitText` + a shared-layout transition.
---
## 13. 375.studio/en/studio — Studio375 (Vicenza, Italy)
SOTD Apr 30, 2026 (7.33, plus Developer Award) [^2^][^15^].
**[A] Visual language.** Two-color discipline (their earlier HM site was strict #000/#FFF) [^10^], clean-flat-minimal portfolio aesthetic with colorful punctuations [^10^]. Communications-agency clarity rather than tech-lab showing-off.
**[B] Motion.** Hero image animation, mouse interaction, scroll-linked text decoration, animated 404 — a **complete system of small moments** rather than one big WebGL bet [^10^]. Dev-award scores show the craft is in transitions (animations 7.8, their strongest dev metric) [^2^].
**[C] Tech stack (inference).** DOM-first, GSAP-class orchestration; no heavy WebGL signature. Accessibility dev-score 7.2 is above the award-site average [^2^] — suggests semantic care.
**[D] UX.** Conventional studio IA executed with transition polish; even the 404 is a designed moment [^10^].
**[E] Critical assessment.**
-**Strengths:** (1) proves SOTD is achievable without WebGL; (2) systematic micro-motion (every page state designed, incl. errors); (3) strong accessibility score for the category.
- ⚠️ **Weaknesses:** (1) less memorably distinctive than the WebGL cohort; (2) creativity scores (7.57) lag the experimental sites.
- 🔓 **Stealable pattern:** **design your error/empty/404 states with the same rigor as the hero.** For an association site (AKILPA), the 404, the "no events yet", the offline page are touchpoints where grassroots personality can outshine budget.
---
## 14. ten.375.studio/en — "Ten Years Away" (Studio375)
SOTD Jun 26, 2026 (7.32; users rated creativity 9.3) [^4^].
**[A] Visual language.** **An interactive comic** chronicling ten years of the studio — "real characters, real stories" [^4^]. Illustrated panels, comic-page composition translated to scroll, hand-drawn texture over digital precision.
**[B] Motion.** Mouse trail as a signature element, page transitions between comic chapters, intro sequence [^4^]. Motion serves panel-to-panel reading rhythm — scroll as page-turn.
**[C] Tech stack (inference).** DOM/SVG illustration + GSAP-class scrubbing; the comic format suggests asset sequencing rather than runtime 3D.
**[D] UX.** Narrative chronology as IA: ten years = ten chapters. Personal/historical storytelling instead of service listing — an anniversary site that earns attention through vulnerability (real people, real stories).
**[E] Critical assessment.**
-**Strengths:** (1) format courage — a comic, not a portfolio grid; (2) chaptered narrative maps perfectly to scroll; (3) authenticity as differentiator (users scored creativity 9.3, far above jury [^4^]).
- ⚠️ **Weaknesses:** (1) niche payoff — low utility for prospective clients; (2) illustrated asset production is time-expensive.
- 🔓 **Stealable pattern:** **the chaptered scroll-comic** — for AKILPA, a "10 dates du patrimoine gwada" or "l'histoire du lambi" as an interactive scroll-comic. Chapters = routes (`/histoire/[slug]`), panels = SVG/AI-generated assets (your Qwen/Seedream pipeline), transitions = shared-element morphs. High storytelling ROI per byte.
---
## 15. apple.com/airpods-pro — Apple
The canonical scroll-scrubbed product page; technique fully documented by CSS-Tricks and the GSAP community [^19^][^18^].
**[A] Visual language.** Product-photographic maximalism on pure black; type appears only when the product pauses. No texture tricks — the hardware render *is* the visual language.
**[B] Motion.** **Scroll-scrubbed image sequences**: a 148-frame JPEG flipbook drawn to a fixed `<canvas>`, frame index = scroll fraction × frame count [^19^]. Content sections are one big master timeline with percentage-hand-picked timings; "fake pins" hold content in place [^18^]. requestAnimationFrame for tear-free frame swaps [^19^].
**[C] Tech stack (documented technique).** Canvas 2D + image preloading; no WebGL needed [^19^]. **Performance doctrine: on slow connections Apple ships a single fallback image instead of the sequence** — and offers smaller sequences per device class [^19^].
**[D] UX.** Scroll = product film. Information hierarchy: specs arrive only after desire is built. Accessibility: canvas content is mirrored by real DOM text below.
**[E] Critical assessment.**
-**Strengths:** (1) the flipbook is the most copied award pattern alive — simple, robust, GPU-cheap; (2) **explicit slow-network fallback strategy** — exactly your 4G doctrine; (3) master-timeline percentage timing keeps everything in sync across viewports [^18^].
- ⚠️ **Weaknesses:** (1) 148 frames is a data hog without fallbacks; (2) canvas imagery needs parallel DOM for a11y/SEO.
- 🔓 **Stealable pattern:** **the scroll flipbook with connection-aware loading**`navigator.connection.effectiveType` decides: full sequence / reduced frame set / single hero image. Ship it as a Svelte action (see §5, bonus component). **This is the single most Gwada-appropriate award pattern in this list.**
---
---
# PART 2 — THE SVELTEKIT RECIPE
## 1. Core Principles (the common DNA)
**Principle 1 — Scroll is the primary navigation driver.** 13 of 15 sites bind narrative progression to scroll. Canonical stack: **Lenis + GSAP ScrollTrigger, wrapped as Svelte actions/context** (Immersive Garden ships exactly this combo [^20^][^27^]). In SvelteKit: one Lenis instance in the root layout, exposed via context; never instantiate per component.
**Principle 2 — Bake at build, not at runtime.** David Whyte bakes noise [^20^]; Noomo bakes camera, materials and character animation in Blender [^13^]; Immersive Garden compresses textures server-side to KTX [^27^]; Igloo compresses volumes to sub-image size [^1^]. Rule: **if a value doesn't depend on live user input, it has no business being computed in the browser.** Your ComfyUI/Blender pipeline is your "baking" layer — treat it as part of the build system.
**Principle 3 — DOM first, WebGL only where it pays.** Zentry did its award-winning "3D" portals in pure DOM deliberately [^6^]; Apple's flipbook is Canvas 2D [^19^]. Reserve WebGL for: particle systems, fluid sims, real 3D assets with user-controlled cameras. Everything else: CSS transforms, clip-path, SVG. **For 4G Guadeloupe this principle is non-negotiable.**
**Principle 4 — Motion is designed before pages.** Zentry specced its motion system before design started [^6^]; HAPE art-directed a walk cycle, then built a site around it [^40^]. Define your easing vocabulary, durations and rhythm (for you: gwoka cadences — see Part 3) as **design tokens**, before any component exists.
**Principle 5 — Every state is a designed moment.** Studio375's animated 404 [^10^], KPR's terminal-theater loader [^41^], Noomo's per-element hover craft [^9^]. Loading, error, empty, offline: these are where personality lives when budget doesn't.
---
## 2. SvelteKit Architecture Patterns
### 2.1 Route structure for immersive experiences
```
src/routes/
├── +layout.svelte ← SmoothScrollProvider (Lenis) + CursorTracker live HERE
├── +layout.ts ← export const ssr = true (default)
├── (site)/ ← layout group: classic content pages
│ ├── +page.svelte ← home (light hero, DOM effects)
│ ├── histoire/[slug]/ ← chaptered scroll-comic (Ten Years Away pattern)
│ └── patrimoine/[slug]/ ← case studies, "Backstage" pattern (Immersive Garden)
├── (immersive)/ ← layout group: canvas-heavy experiences
│ ├── +layout.svelte ← WebGLCanvas provider, ssr disabled per-page
│ └── mawonaj/+page.svelte
└── +error.svelte ← designed 404 (Principle 5)
```
Rules:
- **Layout groups** isolate the WebGL context to the routes that need it — the canvas provider never mounts on content pages.
- **Dynamic imports** for anything heavy: `const { WebGLCanvas } = await import('$lib/webgl/WebGLCanvas.svelte')` behind `{#await}` or `{#if browser}` gates.
- **Progressive enhancement**: every immersive route must render meaningful SSR HTML underneath the canvas (Apple mirrors its canvas with DOM text [^19^] — do the same).
### 2.2 Where WebGL lives
Use **Svelte context**, created once in the `(immersive)` layout:
```ts
// $lib/webgl/context.ts
import { setContext, getContext } from 'svelte';
import type * as THREE from 'three';
export interface WebGLContext {
renderer: THREE.WebGLRenderer;
scene: THREE.Scene;
camera: THREE.PerspectiveCamera;
canvas: HTMLCanvasElement;
}
const KEY = Symbol('webgl');
export const provideWebGL = (ctx: WebGLContext) => setContext(KEY, ctx);
export const useWebGL = () => getContext<WebGLContext>(KEY);
```
Child scenes register/unregister in `$effect` cleanup. **One renderer per app, many scenes.** Never two `<canvas>` WebGL contexts on one page for low-end targets.
For Three.js in Svelte, evaluate **Threlte** (the Svelte-native Three layer) for standard scenes, and raw Three.js for custom shader-heavy work (Igloo-style). Both coexist: Threlte components render into your single context.
### 2.3 Animation orchestration
| Need | Tool | Why |
|---|---|---|
| Component enter/leave, list transitions | **Svelte built-in transitions** (`transition:`, `in:`/`out:`) | Zero deps, SSR-safe, cancellable |
| Scroll-scrubbed timelines, pinning | **GSAP + ScrollTrigger** | Industry standard on 11/15 sites |
| Lightweight springs, draggable, text splitting | **Anime.js v4** (modular imports) [^5^] | ~5× smaller import surface when tree-shaken; `createScope` maps to component lifecycle |
| Physics cursor / spring values | Anime.js `createSpring` or a 12-line custom spring | No GSAP needed for this |
| One shared clock | GSAP `ticker` as the single rAF driver, Lenis hooked into it | Prevents competing rAF loops |
---
## 3. CSS Architecture for Award Sites
### 3.1 Tailwind + CSS variables for dynamic theming
```css
/* app.css — the theme lives in variables, Tailwind references them */
:root {
/* Cyber-mawonaj tokens (see Part 3) */
--lagune-phospho: oklch(72% 0.13 210);
--roche-volcan: oklch(18% 0.02 260);
--corail: oklch(70% 0.16 25);
--or-circuit: oklch(80% 0.14 90);
--foret-neon: oklch(55% 0.15 155);
--ease-ka: cubic-bezier(0.22, 1, 0.36, 1); /* motion tokens too */
--dur-mesure: 480ms;
}
@theme inline {
--color-lagune: var(--lagune-phospho);
--color-volcan: var(--roche-volcan);
/* … */
}
```
Theme switching = swapping a `data-theme` attribute on `<html>`, variables cascade, Tailwind utilities follow. No JS class churn, no FOUC if set in `%sveltekit.head%`.
### 3.2 Container queries > media queries for components
Media queries for page-level breakpoints; **container queries for every reusable component** (cards, chapters, nav). Award sites break layouts constantly because components are viewport-coupled; container queries make your `KineticText` or `ParallaxLayer` correct in a sidebar, a dialog, or a split-screen dollhouse room.
### 3.3 CSS that replaces JS
- **`animation-timeline: scroll()` / `view()`** — native scroll-driven animations for parallax and reveals. ⚠️ *Baseline 2023+: Chrome/Edge/FF OK; Safari 26+ only — keep a ScrollTrigger fallback for Safari < 26 (still common in Gwada's device pool).* Flag: **needs progressive enhancement wrapper.**
- **`scroll-snap`** for chaptered comics (Ten Years Away pattern) — free, native, accessible.
- **`:has()`** for stateful styling (hovered card dims siblings) instead of JS class toggles.
- **View Transitions API** for route morphs (see PageTransition, §5). ⚠️ *Chrome/Edge/Safari 18+; Firefox behind flag — design as enhancement.*
---
## 4. Performance Budget & Optimization
### 4.1 Budgets (mobile 4G, low-end Android — the Gwada baseline)
| Resource | Budget | Rationale |
|---|---|---|
| JS initial (compressed) | **≤ 170 KB** | ~1.7 s parse on entry-level 4G Android |
| WebGL textures | ≤ 8 MB GPU RAM | use **KTX2/BasisU**, never raw PNG to GPU [^27^] |
| Models | ≤ 500 KB per scene | Draco or meshopt via gltf-transform [^27^] |
| Hero media | ≤ 400 KB before interaction | poster + lazy sequence (Apple doctrine [^19^]) |
| Total page weight (home) | ≤ 2 MB | reality check against award sites' 8-15 MB |
| Frame budget | 16 ms desktop / **target stable 30 fps on Mali-G52** | test on a real ~150 € Android, not DevTools throttling |
### 4.2 WebGL asset pipeline
```
Blender → gltf-transform CLI:
gltf-transform optimize in.glb out.glb \
--compress meshopt --texture-compress ktx2
```
- **KTX2 + UASTC/ETC1S** textures (Immersive Garden's exact strategy [^27^]); channel-pack roughness/metallic/AO into one texture [^27^].
- **LOD**: two model tiers; pick at runtime from `renderer.capabilities` + `deviceMemory`.
- **Instancing** for repeated elements (lambi shells, vegetation, stars).
- **Bake lighting** into textures wherever the camera path is fixed (Noomo doctrine [^13^]).
### 4.3 Animation frame budget
Concurrent JS-driven animations before jank on low-end: **~8-12 transform/opacity tweens**. Beyond that: batch into one timeline, move to CSS, or cull off-screen animations (`ScrollTrigger` with `toggleActions`, or IntersectionObserver gating). Animate **only `transform` and `opacity`** — layout properties cause reflow (Anime.js v4 docs say the same [^5^]).
### 4.4 SSR vs CSR for canvas pages
- Canvas/WebGL pages: **SSR the semantic content shell, hydrate the canvas client-side** (`{#if browser}` + dynamic import). Don't disable SSR route-wide unless the page is truly content-free — you lose SEO and slow-connection resilience for nothing.
- Lenis/GSAP init: `onMount` only, always.
- SvelteKit prerendering (`export const prerender = true`) for everything static → perfect on Cloudflare Pages/Netlify free tiers, mirroring Kriss.ai's edge-CDN architecture [^28^].
---
## 5. Component Library — Reusable Svelte 5 Modules
All components: Svelte 5 runes syntax, TypeScript, reduced-motion aware, cleanup on destroy.
### 5.1 SmoothScrollProvider — Lenis as layout-level context
```svelte
<!-- $lib/components/SmoothScrollProvider.svelte -->
<script lang="ts">
import { onMount, setContext, type Snippet } from 'svelte';
import Lenis from 'lenis';
import gsap from 'gsap';
let { children }: { children: Snippet } = $props();
let lenis: Lenis | null = $state(null);
setContext('lenis', { get: () => lenis });
onMount(() => {
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduced) return; // native scroll = the fallback
lenis = new Lenis({ lerp: 0.1, smoothWheel: true });
// ONE rAF driver for the whole app (Principle: shared clock)
lenis.on('scroll', () => {}); // ScrollTrigger.update hook lives in KineticText
gsap.ticker.add((t) => lenis?.raf(t * 1000));
gsap.ticker.lagSmoothing(0);
return () => { lenis?.destroy(); lenis = null; };
});
</script>
{@render children()}
```
### 5.2 WebGLCanvas — single renderer, context-provided scene graph
```svelte
<!-- $lib/webgl/WebGLCanvas.svelte -->
<script lang="ts">
import { onMount, type Snippet } from 'svelte';
import { provideWebGL } from './context';
let { children, dpr = 1.5 }: { children: Snippet; dpr?: number } = $props();
let canvas: HTMLCanvasElement;
let ready = $state(false);
onMount(async () => {
const THREE = await import('three'); // code-split: three never hits content pages
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(devicePixelRatio, dpr)); // cap DPR: free FPS on retina
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 100);
const onResize = () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
};
onResize();
addEventListener('resize', onResize);
renderer.setAnimationLoop(() => renderer.render(scene, camera));
provideWebGL({ renderer, scene, camera, canvas });
ready = true;
return () => {
removeEventListener('resize', onResize);
renderer.setAnimationLoop(null);
renderer.dispose();
};
});
</script>
<canvas bind:this={canvas} class="fixed inset-0 -z-10" aria-hidden="true"></canvas>
{#if ready}{@render children()}{/if}
```
### 5.3 KineticText — scroll-driven split typography
```svelte
<!-- $lib/components/KineticText.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
let {
text,
as = 'h2',
scrub = true
}: { text: string; as?: keyof HTMLElementTagNameMap; scrub?: boolean } = $props();
let el: HTMLElement;
onMount(async () => {
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const { animate } = await import('animejs');
const { splitText } = await import('animejs/text');
const { onScroll } = await import('animejs'); // Scroll Observer [^12^]
const words = splitText({ text, by: 'word', tag: 'span' });
el.append(...words.nodes as Node[]);
const anim = animate(words.nodes, {
opacity: { to: 1 },
translateY: { to: 0 },
delay: (el, i) => i * 40, // stagger → swap for gwoka pattern (Part 3)
ease: 'out(3)',
duration: 600,
autoplay: scrub
? onScroll({ target: el, enter: 'bottom-=10%', leave: 'top+=10%', sync: 0.5 })
: true
});
return () => anim.revert();
});
</script>
<svelte:element this={as} bind:this={el} aria-label={text} class="kinetic">{text}</svelte:element>
```
### 5.4 ParallaxLayer — multi-plane depth, CSS-first
```svelte
<!-- $lib/components/ParallaxLayer.svelte -->
<script lang="ts">
import { onMount, type Snippet } from 'svelte';
let { depth = 0.3, children }: { depth?: number; children: Snippet } = $props();
let el: HTMLElement;
onMount(async () => {
// Prefer native scroll-driven animations where supported (no JS per frame)
if (CSS.supports('animation-timeline: view()')) {
el.style.animationTimeline = 'view()';
el.style.animationName = 'parallax-y';
el.style.setProperty('--depth', String(depth));
return;
}
const gsap = (await import('gsap')).default;
const { ScrollTrigger } = await import('gsap/ScrollTrigger');
gsap.registerPlugin(ScrollTrigger);
const tween = gsap.to(el, {
yPercent: -depth * 100,
ease: 'none',
scrollTrigger: { trigger: el, start: 'top bottom', end: 'bottom top', scrub: true }
});
return () => { tween.scrollTrigger?.kill(); tween.kill(); };
});
</script>
<div bind:this={el} class="parallax-layer">{@render children()}</div>
<style>
@keyframes parallax-y { to { transform: translateY(calc(var(--depth, 0.3) * -30%)); } }
</style>
```
### 5.5 CursorTracker — physics cursor + velocity uniform (Igloo pattern)
```svelte
<!-- $lib/components/CursorTracker.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
let dot: HTMLDivElement;
onMount(async () => {
if (matchMedia('(pointer: coarse)').matches) return; // no cursor on touch
const { createSpring } = await import('animejs');
const spring = createSpring({ stiffness: 120, damping: 14 });
let x = 0, y = 0, px = 0, py = 0;
const onMove = (e: PointerEvent) => { x = e.clientX; y = e.clientY; };
addEventListener('pointermove', onMove, { passive: true });
let raf = 0;
const tick = () => {
const vx = x - px, vy = y - py; // pointer velocity
const speed = Math.min(Math.hypot(vx, vy) / 40, 1);
dot.style.transform = `translate(${spring.solve(x, px)}px, ${spring.solve(y, py)}px) scale(${1 + speed * 0.6})`;
// → also write (x, y, speed) into your WebGL uniforms here: cursor-as-light-source (Unseen pattern)
px = x; py = y;
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => { removeEventListener('pointermove', onMove); cancelAnimationFrame(raf); };
});
</script>
<div bind:this={dot} class="cursor-dot" aria-hidden="true"></div>
```
### 5.6 PageTransition — GSAP route transitions (SvelteKit-safe)
```svelte
<!-- +layout.svelte (site group) -->
<script lang="ts">
import { onNavigate } from '$app/navigation';
import { page } from '$app/state';
onNavigate((nav) => {
// Prefer native View Transitions where available (progressive enhancement)
if (!document.startViewTransition) return;
return new Promise((resolve) => {
document.startViewTransition(async () => { resolve(); await nav.complete; });
});
});
</script>
{#key page.url.pathname}
<main class="page"><slot /></main>
{/key}
<style>
.page { view-transition-name: page; }
::view-transition-old(page) { animation: page-out 0.35s var(--ease-ka) both; }
::view-transition-new(page) { animation: page-in 0.45s var(--ease-ka) both; }
@keyframes page-out { to { opacity: 0; transform: translateY(-2%); } }
@keyframes page-in { from { opacity: 0; transform: translateY(3%); } }
</style>
```
*GSAP alternative for browsers without View Transitions:* animate out in `onNavigate`'s returned promise, animate in via `afterNavigate`. Keep durations in your CSS custom properties so both paths share the same motion tokens.
### 5.7 ScrollProgressBar — zétwal minimal progress
```svelte
<!-- $lib/components/ScrollProgressBar.svelte -->
<script lang="ts">
let progress = $state(0);
const onscroll = () => {
const max = document.documentElement.scrollHeight - innerHeight;
progress = max > 0 ? scrollY / max : 0;
};
</script>
<svelte:window {onscroll} />
<div class="progress" role="progressbar" aria-valuenow={Math.round(progress * 100)}
aria-valuemin="0" aria-valuemax="100" aria-label="Progression de lecture">
<span style:transform="scaleX({progress})"></span>
</div>
<style>
.progress { position: fixed; inset: 0 0 auto; height: 3px; z-index: 50; }
.progress span { display: block; height: 100%; transform-origin: left;
background: linear-gradient(90deg, var(--lagune-phospho), var(--or-circuit)); }
</style>
```
### 5.8 Bonus — FlipbookCanvas (Apple pattern, connection-aware)
```ts
// $lib/actions/flipbook.ts — Svelte action
export function flipbook(canvas: HTMLCanvasElement, opts: { frames: (q: 'full'|'lite') => string[]; total: number }) {
const conn = (navigator as any).connection;
const quality = conn?.effectiveType === '4g' && !conn?.saveData ? 'full' : 'lite';
const urls = opts.frames(quality);
if (conn?.effectiveType === 'slow-2g' || conn?.effectiveType === '2g' || conn?.saveData) {
// Apple doctrine: single fallback image, no sequence [^19^]
const img = new Image(); img.src = urls[0];
img.onload = () => canvas.getContext('2d')?.drawImage(img, 0, 0);
return {};
}
const imgs = urls.map((u) => { const i = new Image(); i.src = u; return i; });
const ctx = canvas.getContext('2d')!;
const draw = (p: number) => {
const f = Math.min(urls.length - 1, Math.floor(p * urls.length));
if (imgs[f]?.complete) ctx.drawImage(imgs[f], 0, 0, canvas.width, canvas.height);
};
const onscroll = () => {
const max = document.documentElement.scrollHeight - innerHeight;
requestAnimationFrame(() => draw(max > 0 ? scrollY / max : 0));
};
addEventListener('scroll', onscroll, { passive: true });
return { destroy: () => removeEventListener('scroll', onscroll) };
}
```
---
## 6. Svelte-Specific Translations (React/Next.js → Svelte idioms)
| React pattern (seen on these sites) | Svelte 5 idiom |
|---|---|
| `useRef` + `useEffect(() => {...}, [])` for GSAP init | `$effect(() => { /* init */; return () => cleanup })` — or better, a **Svelte action** `use:gsapThing` |
| `useContext(WebGLContext)` | `setContext`/`getContext` with a `Symbol` key (§2.2) |
| `useState` for animation params | `$state` rune; params read inside `$effect` re-run the tween — guard with `gsap.quickTo` or `quickSetter` for per-frame writes |
| Derived uniforms (`useMemo`) | `$derived(() => computeUniforms(pointer, scroll))` — recalculates only on dependency change |
| R3F `<Canvas>` + hooks | **Threlte** for declarative scenes; raw Three in `onMount` for shader-heavy work (Noomo avoided R3F for flexibility [^13^] — same logic applies) |
| `framer-motion` layout animations | Svelte built-in `flip` (from `svelte/animate`) + `crossfade` (from `svelte/transition`) — free, SSR-safe |
| Zustand/Redux for UI state | `$state` in a `.svelte.ts` module — a class with rune fields is your store |
**Runes vs stores for animation state:** stores still work and are fine for cross-app state; but **animation state should live in `$state`/`$derived` inside components or `.svelte.ts` modules** — runes give finer-grained reactivity (no subscriber fan-out per frame). Critical rule: **never write per-frame values (scrollY, pointer) into reactive state that drives DOM** — write them to refs/plain variables and flush via rAF, or you'll re-render at 60 Hz.
**GSAP + runes compatibility:** GSAP mutates the DOM directly — Svelte doesn't care as long as GSAP's targets aren't nodes Svelte also patches (avoid tweening attributes Svelte controls via bindings). Wrap GSAP targets in `{@html}`-free static markup or bind via `bind:this`, init in `$effect`, kill in the cleanup function. Anime.js v4's `createScope({ root })` + `scope.revert()` is the same lifecycle shape and tree-shakes better [^5^].
---
## 7. Accessibility & Fallbacks
1. **Screen readers in immersive sites:** canvas is `aria-hidden`; a semantic DOM mirror carries all content (Apple pattern [^19^]). If you render UI text in WebGL (Igloo), ship a visually-hidden parallel DOM (`sr-only` list of the same links/labels).
2. **Reduced motion:** one global gate — `matchMedia('(prefers-reduced-motion: reduce)')` checked in `SmoothScrollProvider` (no Lenis), `KineticText` (no split/scrub), `ParallaxLayer` (static), flipbook (poster frame). Implement it **once, at the provider level**, not per component.
3. **Graceful degradation ladder** (the Gwada ladder):
- `navigator.connection.saveData` or `effectiveType` ≤ 3g → static images, no autoplay video, no sequence preloading [^19^]
- `deviceMemory ≤ 4` or WebGL context creation fails → DOM-effects-only mode (Zentry proves DOM-only can still win awards [^6^])
- `<noscript>` / JS-disabled → SSR content is complete and readable (this is why SSR matters)
4. **Focus management:** route transitions must move focus to the new page's `<h1>`; scroll-jacking (Lenis) must never break keyboard scrolling — Lenis preserves native scroll, which is why it's the right engine choice.
5. **Bilingual a11y:** `lang` attributes per text node (`fr` / `gcf`) — screen readers switch voices; crucial for Kréyòl content.
---
## 8. Tooling Recommendations
### Essential packages (open-source first, per your doctrine)
```jsonc
{
"dependencies": {
"lenis": "^1.3", // smooth scroll — the award-site standard [^20^][^27^]
"gsap": "^3.13", // timelines, ScrollTrigger, ticker
"animejs": "^4.2", // modular springs/stagger/splitText [^5^]
"three": "^0.1xx", // WebGL — dynamic-import only
"@threlte/core": "^8", // optional: declarative Three in Svelte
"troika-three-text": "^0.5" // SDF text in WebGL (Igloo pattern, if needed)
},
"devDependencies": {
"@gltf-transform/cli": "^4",// meshopt/KTX2 pipeline (§4.2)
"vite-imagetools": "^7", // responsive AVIF/WebP at build
"sharp": "^0.33"
}
}
```
- **Vite plugins:** `vite-imagetools` (build-time responsive images), `vite-plugin-pwa` (offline mode = real resilience on flaky 4G, plus an installable PWA for AKILPA), basic `manualChunks` splitting `three` into its own async chunk.
- **HMR for WebGL:** Vite HMR reloads Svelte components but not your GL state — keep scene setup idempotent (init/dispose symmetric in `$effect`), store debug params in URL hash so a reload restores your camera/scrub position. Shader files as `?raw` imports hot-reload cleanly.
- **Hosting (≤50 €/mo, actually near 0 €):** **Cloudflare Pages + R2** — the exact Kriss.ai architecture [^28^]; free tier absorbs heavy media via edge CDN. Netlify/Vercel free tiers work too; avoid anything Node-server-based (Immersive Garden's Strapi [^27^] is out of budget — use a headless Git-based CMS or plain Markdown in-repo).
---
# PART 3 — CYBER-MAWONAJ ADAPTATION
How the 15-site DNA translates for a Caribbean, sovereign, 4G-first aesthetic. This is not "apply a tropical skin" — it's re-deriving each decision from the local context.
### Color system (CSS tokens, oklch for perceptual control)
```css
:root {
--lagune-phospho: oklch(72% 0.13 210); /* phosphorescent lagoon blue — primary accent */
--roche-volcan: oklch(18% 0.02 260); /* volcanic rock black — dominant ground */
--corail: oklch(70% 0.16 25); /* coral pink — secondary accent */
--or-circuit: oklch(80% 0.14 90); /* circuit gold — data/tech highlights */
--foret-neon: oklch(55% 0.15 155); /* deep forest green, used with neon edge-glow */
--terre-rouge-a: oklch(45% 0.12 40); /* terre rouge gradient stop A */
--terre-rouge-b: oklch(30% 0.09 55); /* terre rouge gradient stop B — use for section grounds */
}
```
Dark-mode-first (`--roche-volcan` ground) = OLED battery saving + the bioluminescence aesthetic for free. The **arc-en-ciel / rainbow-serpent** motif belongs in *gradients and particle hues* (velocity-shifted color, Igloo pattern [^1^]) — never as static rainbow fills.
### Cultural signifiers → concrete UI patterns
| Signifier | Pattern (source site) | Implementation |
|---|---|---|
| **Lambi shell** | UI motif: shell-spiral as scroll progress, loader, section dividers | SVG spiral `stroke-dashoffset` scrub (createDrawable, animejs/svg [^5^]); KTX2 shell texture for WebGL hero |
| **Zétwal (stars)** | Navigation: constellation nav where sections are stars | DOM + SVG lines; hover = star brightens + label reveals; light-years cheaper than a WebGL starfield |
| **Dollhouse créole** | Kriss.ai spatial IA [^28^] | AKILPA sections as case à ka / marché / karbay / plage rooms — isometric DOM illustration, no WebGL needed |
| **Terre rouge** | Section backgrounds gradient | Pure CSS, zero cost |
| **Mawon/maroon resistance** | Visual language: glitch-free, hand-made texture; KPR's community-shaped tableaux [^45^] | UGC/content layers: community-submitted photos as parallax planes; "Backstage" section documenting the process (Immersive Garden pattern [^27^]) = transparency as sovereignty |
### Typography (FR/KRÉ bilingual, low-res safe)
- **Weights: ≥ 400 body, ≥ 600 display** — thin weights die on low-DPI screens (your constraint is correct).
- Suggested open fonts: **Space Grotesk** (display, tech-caribbean tension) + **Inter** or **Atkinson Hyperlegible** (body, legibility-first, open license). Atkinson is the sovereignty-aligned choice: designed for low-vision readers.
- Kinetic type via `splitText` on **words, not characters**, for Kréyòl — character-splitting can break apostrophe-heavy words (`mwen`, `tèt'w`) visually and for screen readers. Keep `aria-label` with the intact string (component §5.3 does this).
- Language switch as animation event (Nudot pattern): mask-swap lines in place.
### Motion design — gwoka cadences as easing/timing tokens
Award sites ship custom ease curves; yours can carry rhythm. Practical translation:
```css
:root {
/* ka/belair-inspired: strong downbeat, syncopated release */
--ease-ka: cubic-bezier(0.22, 1, 0.36, 1); /* downbeat: fast attack, long settle */
--ease-syncope: cubic-bezier(0.65, 0, 0.35, 1); /* off-beat: symmetric, urgent */
--dur-tanbou: 120ms; /* sixteenth @ ~125 BPM — micro-interactions */
--dur-mesure: 480ms; /* one bar — entrances */
--dur-phrase: 960ms; /* two bars — page transitions */
}
```
Stagger patterns: instead of linear `i * 40ms`, use **syncopated stagger** `[0, 120, 240, 360, 480, 600, 720, 840]` → remap to `[0, 120, 300, 360, 600, 660, 900, 960]` (3+3+2 feel). Subtle, but rhythm is identity.
### The Gwada connectivity doctrine (non-negotiables)
1. **Connection-aware everything** — flipbook component §5.8 is the reference implementation [^19^].
2. **DOM-first effects** — Zentry is your patron saint here [^6^].
3. **PWA with offline shell** — an association site that works in a dead zone is a political statement (souveraineté numérique).
4. **Test hardware:** a ~150 € Android on real 4G in Gwada, not Lighthouse throttling.
5. **Edge hosting:** Cloudflare Pages/R2 [^28^] — points of presence in the Americas, near-zero cost, aligns with the 50 €/mo ceiling (realistically: < 5 €/mo).
---
# PART 4 — COMPARISON MATRIX
| # | Site | Visual | Motion | Tech | UX | Stealable pattern |
|---|---|---|---|---|---|---|
| 1 | unseen.co | Volumetric light, dark bloom | Inertial, particle-driven | Three.js [^39^] | Scroll-journey portfolio | Cursor-as-light uniform |
| 2 | animejs.com | Lab-instrument dark, kinetic type | Springs, staggers, scroll-observer | Three hero + Anime.js v4 modules [^5^][^14^] | Interactive docs | `createScope` per component |
| 3 | immersive-g.com | Bas-relief minimalism | Ceremonial glide | Nuxt, Three, GSAP, Lenis, Strapi, KTX pipeline [^27^] | One-click access + Backstage | Backstage process section |
| 4 | davidwhyte.com/experience | Watercolor organic | Fluid sim, baked-noise reveals | Nuxt, Three, GSAP, Lenis, WP [^20^] | Slow contemplative tempo | Bake generative noise offline |
| 5 | kriss.ai/aftercare | 70s warm dollhouse | Room-to-room camera | WebGL/Blender, **Cloudflare Pages+R2** [^28^] | Spatial metaphor IA | Dollhouse IA (créole rooms) |
| 6 | zentry.com | Bold saturated portals | Motion-system-first | **Nuxt, pure DOM — no WebGL** [^6^] | Portal reveals + sound | DOM-only 3D portal masks ⭐ |
| 7 | igloo.inc | Procedural ice, chromatic frost | Velocity particles, SDF glitch | **Three + Svelte + GSAP** [^1^] | 3-section scroll journey | Velocity-reactive particles |
| 8 | kprverse.com | Concept-art tableaux | Click-and-hold layers | Resn (Nuxt-class) [^45^] | Lore gating, terminal loader | Click-hold reveal layer |
| 9 | hape.io | Fashion editorial | Scroll-scrubbed walk + music sync | Custom OGL micro-framework [^37^] | Single-idea focus | Scroll-scrubbed animation clip |
| 10 | labs.noomoagency.com | Glass jellyfish dream | Baked camera playback | Three + GSAP, no R3F, Blender-baked [^13^] | Public R&D hub | Two-Empty baked camera rig |
| 11 | noomoagency.com | Disciplined glassmorphism | Per-element hover craft | Nuxt 3 full-stack, Three, GSAP [^9^] | Classic IA, flawless transitions | Budgeted hover craft (5 elements) |
| 12 | nudot.com.tw | Bilingual kinetic type | Character reveals, hover previews | DOM/GSAP-class *(inference)* | Bilingual as feature | Language-switch as animation |
| 13 | 375.studio | Two-color discipline | Systematic micro-motion | DOM-first *(inference)* | Every state designed | Designed 404/empty states |
| 14 | ten.375.studio | Interactive comic | Panel scrubbing, mouse trail | DOM/SVG *(inference)* | Chaptered narrative | Chaptered scroll-comic ⭐ |
| 15 | apple.com/airpods-pro | Photographic black | Scroll flipbook, master timeline | Canvas 2D image sequence [^19^][^18^] | Desire before specs | Connection-aware flipbook ⭐ |
⭐ = highest ROI for Cyber-mawonaj constraints (4G, low-end, sovereignty).
---
## Sources
[^1^]: https://www.webgpu.com/showcase/igloo-inc-procedural-crystals/ — Igloo Inc: Crystal Growth Algorithms, Shader-Driven UI, and Volume Data (Feb 2026)
[^3^]: https://www.awwwards.com/sites/igloo-inc — Igloo Inc, Awwwards SOTD Jul 23 2024
[^4^]: https://www.awwwards.com/sites/ten-years-away — Ten Years Away, Awwwards SOTD Jun 26 2026
[^5^]: https://github.com/juliangarnier/anime/issues/1105 — Anime.js v4 LLM guiding file (API modules, createScope, splitText)
[^6^]: https://www.awwwards.com/zentry-case-study.html — Zentry case study (Resn): Nuxt/Vue/Vite, DOM-only 3D portal masks, no WebGL
[^7^]: https://www.awwwards.com/igloo-inc-case-study.html — Igloo Inc case study (Abeto × Bureaux)
[^8^]: https://www.webgpu.com/showcase/noomo-3d-storytelling-threejs/ — Noomo 3D storytelling site write-up
[^9^]: https://medium.com/@noomo-agency/noomo-agency-website-of-the-year-winner-on-awwwards-aad757327994 — Noomo: Nuxt 3, Three.js, GSAP
[^10^]: https://www.awwwards.com/sites/studio375 — Studio375 HM (2023): elements incl. animated 404
[^11^]: https://www.awwwards.com/sites/noomo-agency — Noomo Agency SOTD Sep 21 2023, elements list
[^12^]: https://animejs.com/ — Anime.js feature docs (Scroll Observer, stagger, SVG toolset)
[^13^]: https://www.awwwards.com/noomo-labs-a-hub-for-cutting-edge-immersive-experiences.html — Noomo Labs behind the scenes: Three+GSAP, no R3F, Blender-baked, two-Empty camera
[^14^]: https://www.reddit.com/r/webdev/comments/1jqmqkn/ — r/webdev on animejs.com v4 site (Three.js hero)
[^15^]: https://www.awwwards.com/Studio375/ — Studio375 profile & awards
[^16^]: https://noomoagency.com/ — Noomo Agency honors list (WOTY, Labs awards)
[^17^]: https://www.awwwards.com/sites/david-whyte-experience — David Whyte Experience SOTD Dec 31 2024
[^18^]: https://gsap.com/community/forums/topic/25188-airpods-image-sequence-animation-using-scrolltrigger/ — master-timeline percentage technique
[^19^]: https://css-tricks.com/lets-make-one-of-those-fancy-scrolling-animations-used-on-apple-product-pages/ — Apple flipbook technique + slow-connection fallback
[^20^]: https://www.awwwards.com/case-study-david-whyte-experience-by-immersive-garden.html — fluid-sim atlas, baked noise, full stack (Nuxt/Three/GSAP/Lenis/WP/Vercel)
[^24^]: https://www.awwwards.com/immersivegarden/ — Immersive Garden profile (Studio of the Year ×3)
[^27^]: https://www.awwwards.com/case-study-immersive-gardens-new-website.html — KTX compression, channel packing, gltf-transform; Nuxt/Three/GSAP/Lenis/Strapi/Vercel
[^28^]: https://www.awwwards.com/case-study-ai-with-a-human-touch-the-new-face-of-kriss-ai.html — Kriss.ai dollhouse concept; Cloudflare Pages + R2
[^35^]: https://www.awwwards.com/sites/kpr — KPR SOTD Dec 26 2022
[^36^]: https://www.awwwards.com/sites/hape — HAPE SOTD Apr 27 2022
[^37^]: https://www.deptagency.com/case/creating-a-3d-nft-walking-hape/ — HAPE: custom OGL micro-framework, PBR, skinning; 4-week build
[^38^]: https://www.awwwards.com/directory/agency-studio/ — Unseen Studio directory stats (35 SOTD)
[^39^]: https://x.com/uns__nstudio — Unseen Studio: rebrand from GC, volumetric studies, particle sims, three.js tags
[^40^]: https://dogstudio.co/cases/hape-prime/ — HAPE walk concept, scroll camera, soundtrack sync
[^41^]: https://kprverse.com/ — KPR live site: terminal loader strings, device gating messages
[^45^]: https://www.awwwards.com/kpr-by-resn-wins-sotm.html — KPR by Resn SOTM: click-and-hold layers, tableaux, 3D+2D mix
+211
View File
@@ -0,0 +1,211 @@
# RECETTE SVELTEKIT — PLAYBOOK POUR AGENT DE CODE
### Transformer un site web classique en expérience de niveau Awwwards
> **Nature de ce document :** instructions opérationnelles destinées à un agent de code (Kimi CLI ou équivalent). Chaque règle est impérative et vérifiable. Ce playbook est distillé de l'analyse de 15 sites primés (Zentry, Igloo.inc, Immersive Garden, Noomo, Apple AirPods Pro, KPR, HAPE, Kriss.ai, Studio375, animejs.com, etc.). Il est volontairement autonome : tu n'as besoin d'aucun autre document pour l'appliquer, sauf si l'utilisateur fournit une charte de marque (qui alors **prime sur ce document** pour tout ce qui est couleurs, typographie, voix et iconographie).
---
## 0. MISSION
Transformer un site vitrine « classique » (HTML statique, WordPress, ou SPA sans direction artistique) en un site SvelteKit à forte identité visuelle et motion design maîtrisé, **sans sacrifier performance, accessibilité ni SEO**. Le résultat doit fonctionner parfaitement sur mobile 4G et matériel bas de gamme.
**Interdictions absolues :**
- Ne jamais installer de dépendance sans l'importer et l'utiliser.
- Ne jamais désactiver le SSR globalement pour « faire marcher » une lib client.
- Ne jamais animer autre chose que `transform` et `opacity` en JS.
- Ne jamais livrer une page dont le contenu est invisible sans JavaScript.
- Ne jamais réintroduire de service tiers (fonts, CDN, analytics) si le projet affirme une doctrine de souveraineté — tout asset se self-héberge.
---
## 1. LES 5 PRINCIPES (ADN commun des sites primés)
**P1 — Le scroll est le moteur de navigation principal.** Une seule instance Lenis au layout racine, partagée via contexte Svelte, cadencée par le ticker GSAP unique. Jamais de double boucle rAF.
**P2 — Bake au build, pas au runtime.** Toute valeur qui ne dépend pas d'une entrée utilisateur en direct est précalculée hors-ligne : bruit génératif baké en textures, caméras 3D bakées dans Blender (rig deux-Empty), animations squelettiques exportées en clips GLTF, textures compressées KTX2 via gltf-transform, images responsive générées au build (vite-imagetools).
**P3 — DOM d'abord, WebGL seulement où ça paie.** Zentry a produit des effets « 3D » primés en pur DOM (clip-path, preserve-3d, masks). Réserver WebGL à : systèmes de particules, simulations de fluide, vrais assets 3D à caméra contrôlée. Un flipbook canvas 2D (pattern Apple) couvre la plupart des besoins de « produit qui tourne au scroll ».
**P4 — Le motion se designe avant les pages.** Définir d'abord les tokens de mouvement (courbes, durées, staggers) en custom properties CSS, puis les composants. Toute durée/courbe hardcodée dans un composant est une erreur.
**P5 — Chaque état est un moment designé.** Loader, 404, état vide, hors-ligne : autant de touchpoints de marque. Le 404 générique du framework est interdit.
---
## 2. WORKFLOW EN 7 PHASES
### Phase 1 — AUDIT (toujours commencer ici)
- [ ] Inventorier pages, sections, assets, liens externes, formulaires du site source.
- [ ] Mesurer la stack existante (headers HTTP, poids des assets, dépendances tierces).
- [ ] Identifier les bugs de production (images cassées, mixed content, liens morts) et les lister AVANT toute refonte — une migration ne doit jamais masquer un bug existant.
- [ ] Identifier les contradictions doctrine/outillage (ex. discours anti-GAFAM + Google Fonts).
- [ ] Produire un court rapport d'audit et le faire valider avant de coder.
### Phase 2 — TOKENS & THÈME
- [ ] Créer `app.css` avec toutes les couleurs en `oklch()` (ou valeurs de la charte si fournie), typographies, échelle d'espacement, rayons, **tokens de motion** (courbes + durées).
- [ ] Déclarer le mapping Tailwind via `@theme inline` pour que les utilitaires référencent les variables.
- [ ] Thème via `data-theme` sur `<html>`, appliqué côté serveur dans `app.html` pour éviter tout FOUC.
- [ ] Fonts : woff2 self-hébergées dans `static/fonts/`, `@font-face` avec `font-display: swap`, `preload` de la display uniquement.
### Phase 3 — ARCHITECTURE
```
src/routes/
├── +layout.svelte ← SmoothScrollProvider + CursorTracker (si pertinent)
├── (site)/ ← pages de contenu (SSR complet)
│ ├── +page.svelte
│ └── [section]/[slug]/
├── (immersive)/ ← layout group : canvas lourds isolés ici
│ └── experience/+page.svelte
└── +error.svelte ← 404 designé (P5)
```
- [ ] Layout groups pour isoler tout contexte WebGL des pages de contenu.
- [ ] Imports dynamiques (`await import('three')`) derrière `{#if browser}` pour tout ce qui dépasse ~50 KB.
- [ ] `prerender = true` sur toutes les routes statiques ; adapter `adapter-static` ou `adapter-cloudflare` selon l'hébergement cible.
- [ ] Conserver strictement les URLs existantes (redirections 301 si renommage).
### Phase 4 — COMPOSANTS (bibliothèque de référence, §5)
- [ ] Installer uniquement les composants pertinents pour CE site — la bibliothèque n'est pas un package à copier en entier.
- [ ] Chaque composant livré avec : cleanup dans `$effect`, garde `prefers-reduced-motion`, aucune fuite de listener.
### Phase 5 — MOTION
- [ ] Transitions d'entrée/sortie de composants : transitions Svelte natives.
- [ ] Scrub/pinning lié au scroll : GSAP ScrollTrigger (import dynamique), OU `animation-timeline: view()` natif avec fallback ScrollTrigger pour Safari ancien.
- [ ] Micro-physics (curseur, springs) : Anime.js v4 imports modulaires (`animejs`, `animejs/text`) ou spring custom de 15 lignes.
- [ ] Vérifier le budget : ≤ 8-12 tweens concurrents sur mobile bas de gamme ; culler les animations hors viewport.
### Phase 6 — PERFORMANCE (budgets §4)
- [ ] Pipeline assets : `gltf-transform optimize --compress meshopt --texture-compress ktx2` pour tout GLB ; AVIF/WebP responsive pour les images.
- [ ] Media hero ≤ 400 KB avant interaction ; séquences d'images avec chargement conditionné par `navigator.connection`.
- [ ] Mesurer sur profil mobile 4G (CPU ×4, réseau « Fast 4G ») : LCP < 2,5 s, INP < 200 ms, CLS < 0,05.
### Phase 7 — ACCESSIBILITÉ & DÉGRADATION (§6)
- [ ] La ladder de dégradation complète et testée.
- [ ] Revue clavier complète (tab order, focus visible, skip-link).
- [ ] Contenu canvas doublé en DOM sémantique.
---
## 3. ORCHESTRATION D'ANIMATION — RÈGLES SVELTE 5
| Besoin | Outil | Règle |
|---|---|---|
| Entrée/sortie composant, listes | Transitions Svelte natives | Zéro dépendance, SSR-safe |
| Scrub scroll, pinning | GSAP + ScrollTrigger | Import dynamique, init dans `$effect`, kill au cleanup |
| Springs, stagger, split text | Anime.js v4 modulaire | `createScope({ root })` + `scope.revert()` au cleanup |
| Une seule horloge | `gsap.ticker` pilote Lenis | Aucune autre boucle rAF sauf canvas WebGL |
**Règles runes impératives :**
- État d'animation : `$state` / `$derived` dans le composant ou un module `.svelte.ts`. Jamais de store Svelte legacy pour des valeurs par-frame.
- **Ne jamais écrire des valeurs par-frame (scrollY, pointeur) dans de l'état réactif qui rend du DOM** — écrire dans des variables plain et flusher via rAF.
- GSAP ne doit jamais cibler un nœud que Svelte patche via binding ; cibler via `bind:this` sur du markup statique.
- Tout init GSAP/Anime/Lenis se fait dans `$effect` (ou `onMount`) avec fonction cleanup qui `kill()`/`revert()`/`destroy()`.
---
## 4. BUDGETS PERFORMANCE (mobile 4G, Android entrée de gamme)
| Ressource | Budget |
|---|---|
| JS initial compressé | ≤ 170 KB |
| Textures GPU | ≤ 8 MB, KTX2/BasisU uniquement |
| Modèles 3D | ≤ 500 KB/scène, meshopt ou Draco |
| Média hero avant interaction | ≤ 400 KB |
| Poids total page d'accueil | ≤ 2 MB |
| Animations JS concurrentes | ≤ 8-12 (transform/opacity only) |
| DevicePixelRatio canvas | plafonné à 1.5 |
| Objectif frame rate | 30 fps stables sur Mali-G52, 60 fps desktop |
**SSR vs CSR :** les pages canvas rendent un shell sémantique en SSR et hydratent le canvas côté client. Le prerender statique est le défaut ; l'hébergement cible est un CDN edge (Cloudflare Pages/R2, Netlify) — pas de serveur Node sauf justification explicite.
---
## 5. BIBLIOTHÈQUE DE COMPOSANTS (contrats + implémentations condensées)
### 5.1 SmoothScrollProvider — Lenis au layout racine
```svelte
<script lang="ts">
import { onMount, setContext, type Snippet } from 'svelte';
import Lenis from 'lenis';
import gsap from 'gsap';
let { children }: { children: Snippet } = $props();
let lenis: Lenis | null = $state(null);
setContext('lenis', { get: () => lenis });
onMount(() => {
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
lenis = new Lenis({ lerp: 0.1 });
gsap.ticker.add((t) => lenis?.raf(t * 1000));
gsap.ticker.lagSmoothing(0);
return () => { lenis?.destroy(); lenis = null; };
});
</script>
{@render children()}
```
### 5.2 KineticText — titrage cinétique au scroll
Split par **mots** (jamais par caractères pour les langues à apostrophes), `aria-label` avec la chaîne intacte, scrub via ScrollTrigger ou `animation-timeline: view()` en premier choix, stagger piloté par les tokens de motion du projet.
### 5.3 ParallaxLayer — profondeur multi-plans
CSS-first : si `CSS.supports('animation-timeline: view()')`, animer via keyframes + `view()` ; sinon fallback GSAP ScrollTrigger `scrub: true`, `yPercent` proportionnel à une prop `depth` (01). Toujours `transform`, jamais `top/margin`.
### 5.4 CursorTracker — curseur physique (desktop uniquement)
Garde `matchMedia('(pointer: coarse)')` → ne rien monter sur tactile. Spring (Anime.js `createSpring` ou custom), scale modulé par la **vélocité** du pointeur (pattern Igloo). Exposer position + vélocité via contexte pour alimenter des uniforms WebGL éventuels (curseur-source-de-lumière, pattern Unseen).
### 5.5 PageTransition — transitions de routes
`onNavigate` + View Transitions API en enhancement progressif ; fallback GSAP (out dans la promise d'`onNavigate`, in via `afterNavigate`). Durées lues depuis les custom properties CSS pour que les deux chemins partagent les mêmes tokens.
### 5.6 ScrollProgressBar — barre de progression
`<div>` fixe + `scaleX(progress)`, `role="progressbar"` avec `aria-valuenow`, listener scroll passif, calcul dans rAF.
### 5.7 FlipbookCanvas — séquence d'images scrubée (pattern Apple)
Action Svelte `use:flipbook`. Sélection de qualité via `navigator.connection.effectiveType` + `saveData` : `2g/slow-2g/saveData`**image fixe unique** ; `3g` → jeu réduit ; `4g` → séquence complète. Frames dessinées sur `<canvas>` via rAF, index = fraction de scroll. Préchargement progressif (ne pas bloquer le LCP).
### 5.8 WebGLCanvas — renderer unique (seulement si P3 le justifie)
Un seul `WebGLRenderer` pour toute l'app, fourni via `setContext` avec clé `Symbol`, scènes enfants enregistrées/retirées au cleanup, `setPixelRatio(min(dpr, 1.5))`, `setAnimationLoop` stoppé et `renderer.dispose()` au démontage. Import dynamique de `three`.
---
## 6. ACCESSIBILITÉ & LADDER DE DÉGRADATION
**Gate unique de reduced-motion** au niveau provider : si `prefers-reduced-motion: reduce` → pas de Lenis, pas de scrub, pas de flipbook (poster), contenu statique complet. Ne jamais implémenter la garde composant par composant.
**Ladder (dans l'ordre) :**
1. `saveData` ou `effectiveType` ≤ 3g → images statiques, zéro préchargement de séquence, vidéo non autoplay.
2. `deviceMemory` ≤ 4 ou échec de création de contexte WebGL → mode DOM-effects-only.
3. JS désactivé → le contenu SSR est complet et lisible.
4. Canvas/WebGL toujours `aria-hidden` avec miroir DOM sémantique du contenu.
5. Attribut `lang` correct sur chaque bascule de langue ; focus déplacé sur le `<h1>` après chaque transition de route.
---
## 7. OUTILLAGE
```jsonc
{
"dependencies": {
"lenis": "^1.3",
"gsap": "^3.13",
"animejs": "^4.2"
// "three": uniquement si P3 le justifie — import dynamique obligatoire
},
"devDependencies": {
"@sveltejs/adapter-static": "^3",
"@gltf-transform/cli": "^4",
"vite-imagetools": "^7",
"vite-plugin-pwa": "^1" // shell hors-ligne
}
}
```
HMR/WebGL : init/dispose symétriques dans `$effect`, shaders en imports `?raw` (hot-reload propre), paramètres de debug dans le hash d'URL.
---
## 8. DÉFINITION DE « TERMINÉ » (DoD — tout doit être vrai)
- [ ] Rapport d'audit livré et bugs de production pré-existants corrigés ou explicitement reportés.
- [ ] Aucune ressource chargée depuis un domaine tiers (fonts, CDN, scripts) sauf décision documentée.
- [ ] Lighthouse mobile : Performance ≥ 90, Accessibilité ≥ 95, Best Practices ≥ 95, SEO ≥ 95.
- [ ] Navigation clavier complète + `prefers-reduced-motion` vérifié manuellement.
- [ ] Page fonctionnelle et lisible avec JS désactivé.
- [ ] 404, vide et hors-ligne designés (P5).
- [ ] Toutes les URLs historiques préservées ou redirigées.
- [ ] Build de production vert, zéro warning d'accessibilité Svelte.
-3
View File
@@ -1,3 +0,0 @@
{
"url": "https://organisationka.b-cdn.net"
}
-1
View File
@@ -1 +0,0 @@
<svg viewBox="0 86.4 512 339.2" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><mask id="casto-face"><rect y="86.4" width="512" height="339.2" fill="#fff"/><path d="M372.7 159.7H139c-40.1 0-72.8 32.8-72.8 72.8S98.9 305 139 305h233.8c40.1 0 72.8-32.5 72.8-72.5.4-39.8-31.7-72.4-71.5-72.8zm-207.1 93.6s-10.7-7.9-25.8-7.9c-11.3 0-24.4 6.8-24.4 6.8-3.7-5.3-5.7-11.6-5.8-18.1 0-17.3 14-31.2 31.3-31.2s31.3 13.9 31.3 31.2c0 7.4-2.4 14-6.6 19.2m90.4 18c-34.3 0-33-26.3-33-26.3-.5-4.9 5.8-6.6 8-3.6 1.1 1.6 1.1 1.6 1.9 4.6 4.7 16.1 23.1 15.3 23.1 15.3s18.4 1.1 23.1-15.3c.8-2.7.8-3 1.9-4.6 2.2-2.7 8.5-1.4 8 3.6 0 .1 1.3 26.3-33 26.3m140.4-18.9s-13.2-6.8-24.5-6.8c-15.1 0-25.8 7.9-25.8 7.9-4.3-5.4-6.6-12.2-6.6-19.1 0-17.3 14-31.2 31.3-31.2s31.3 13.9 31.3 31.2c.2 6.5-1.8 12.9-5.7 18" fill="#000"/><path d="M107.4 129.1c-17.3-4.6-32.1 5.7-40.4 20-1.9 3.3-.6 6 1.4 7.1 2.8.8 4.7.3 7.7-4.1 6-10.7 16.2-16.7 27.7-14.5 0 0 8 2.5 9.1-2.7.8-3.4-2.2-5-5.5-5.8M417.2 140c-.3 5.2 8 5.2 8 5.2 11.5 1.1 18.7 6.3 21.4 18.3 1.6 5.2 3.3 6 6.3 6 2.2-.3 4.4-2.7 3.3-6.6-3.8-15.9-14-26.8-32.1-27.1-3.3-.2-6.6.7-6.9 4.2" fill="#000"/></mask><path d="M477.7 425.6h-89.3s-3.3-6.8-6.9-13.4-12.1-6.3-12.1-6.3H142s-8.2-1.1-12.4 6.3c-2.6 4.3-5 8.8-7.1 13.4H34.6C15.7 425.6 0 410.3 0 391.4V120.9C0 102 15.4 86.4 34.3 86.4h443.1c18.9 0 34.6 15.3 34.6 34.2v270.5c.3 18.9-15.4 34.5-34.3 34.5" mask="url(#casto-face)"/><path d="M371.1 203.2c-17.3 0-31.3 13.9-31.3 31.2 0 7.1 2.5 13.7 6.6 19.1 0 0 10.7-7.9 25.8-7.9 11.3 0 24.4 6.8 24.4 6.8 3.6-5.2 5.8-11.2 5.8-18.1 0-17.2-14-31.1-31.3-31.1m-230.2 0c-17.3 0-31.3 13.9-31.3 31.2 0 6.8 2.2 12.9 5.8 18.1 0 0 13.2-6.8 24.4-6.8 15.1 0 25.8 7.9 25.8 7.9 4.1-5.2 6.6-11.8 6.6-19.1 0-17.4-14-31.3-31.3-31.3"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

-1
View File
@@ -1 +0,0 @@
<svg viewBox="0 0 126.644 96" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M81.15,0c-1.2376,2.1973-2.3489,4.4704-3.3591,6.794-9.5975-1.4396-19.3718-1.4396-28.9945,0-.985-2.3236-2.1216-4.5967-3.3591-6.794-9.0166,1.5407-17.8059,4.2431-26.1405,8.0568C2.779,32.5304-1.6914,56.3725.5312,79.8863c9.6732,7.1476,20.5083,12.603,32.0505,16.0884,2.6014-3.4854,4.8998-7.1981,6.8698-11.0623-3.738-1.3891-7.3497-3.1318-10.8098-5.1523.9092-.6567,1.7932-1.3386,2.6519-1.9953,20.281,9.547,43.7696,9.547,64.0758,0,.8587.7072,1.7427,1.3891,2.6519,1.9953-3.4601,2.0457-7.0718,3.7632-10.835,5.1776,1.97,3.8642,4.2683,7.5769,6.8698,11.0623,11.5419-3.4854,22.3769-8.9156,32.0509-16.0631,2.626-27.2771-4.496-50.9172-18.817-71.8548C98.9811,4.2684,90.1918,1.5659,81.1752.0505l-.0252-.0505ZM42.2802,65.4144c-6.2383,0-11.4159-5.6575-11.4159-12.6535s4.9755-12.6788,11.3907-12.6788,11.5169,5.708,11.4159,12.6788c-.101,6.9708-5.026,12.6535-11.3907,12.6535ZM84.3576,65.4144c-6.2637,0-11.3907-5.6575-11.3907-12.6535s4.9755-12.6788,11.3907-12.6788,11.4917,5.708,11.3906,12.6788c-.101,6.9708-5.026,12.6535-11.3906,12.6535Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-1
View File
@@ -1 +0,0 @@
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>

Before

Width:  |  Height:  |  Size: 220 B

-1
View File
@@ -1 +0,0 @@
<svg viewBox="5.67 143.05 628.65 387.55" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"/><path d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8C343.2,346.5,335,363.3,326.8,380.1z"/></svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

-1
View File
@@ -1 +0,0 @@
<svg viewBox="0 0 75 79" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><mask id="masto-m"><rect width="75" height="79" fill="#fff"/><path d="M61.2484 27.0263V48.114H52.8916V27.6475C52.8916 23.3388 51.096 21.1413 47.4437 21.1413C43.4287 21.1413 41.4177 23.7409 41.4177 28.8755V40.0782H33.1111V28.8755C33.1111 23.7409 31.0965 21.1413 27.0815 21.1413C23.4507 21.1413 21.6371 23.3388 21.6371 27.6475V48.114H13.2839V27.0263C13.2839 22.7176 14.384 19.2946 16.5843 16.7572C18.8539 14.2258 21.8311 12.926 25.5264 12.926C29.8036 12.926 33.0357 14.5705 35.1905 17.8559L37.2698 21.346L39.3527 17.8559C41.5074 14.5705 44.7395 12.926 49.0095 12.926C52.7013 12.926 55.6784 14.2258 57.9553 16.7572C60.1531 19.2922 61.2508 22.7152 61.2484 27.0263Z" fill="#000"/></mask><path d="M73.8393 17.4898C72.6973 9.00165 65.2994 2.31235 56.5296 1.01614C55.05 0.797115 49.4441 0 36.4582 0H36.3612C23.3717 0 20.585 0.797115 19.1054 1.01614C10.5798 2.27644 2.79399 8.28712 0.904997 16.8758C-0.00358524 21.1056 -0.100549 25.7949 0.0682394 30.0965C0.308852 36.2651 0.355538 42.423 0.91577 48.5665C1.30307 52.6474 1.97872 56.6957 2.93763 60.6812C4.73325 68.042 12.0019 74.1676 19.1233 76.6666C26.7478 79.2728 34.9474 79.7055 42.8039 77.9162C43.6682 77.7151 44.5217 77.4817 45.3645 77.216C47.275 76.6092 49.5123 75.9305 51.1571 74.7385C51.1797 74.7217 51.1982 74.7001 51.2112 74.6753C51.2243 74.6504 51.2316 74.6229 51.2325 74.5948V68.6416C51.2321 68.6154 51.2259 68.5896 51.2142 68.5661C51.2025 68.5426 51.1858 68.522 51.1651 68.5058C51.1444 68.4896 51.1204 68.4783 51.0948 68.4726C51.0692 68.4669 51.0426 68.467 51.0171 68.4729C45.9835 69.675 40.8254 70.2777 35.6502 70.2682C26.7439 70.2682 24.3486 66.042 23.6626 64.2826C23.1113 62.762 22.7612 61.1759 22.6212 59.5646C22.6197 59.5375 22.6247 59.5105 22.6357 59.4857C22.6466 59.4609 22.6633 59.4391 22.6843 59.422C22.7053 59.4048 22.73 59.3929 22.7565 59.3871C22.783 59.3813 22.8104 59.3818 22.8367 59.3886C27.7864 60.5826 32.8604 61.1853 37.9522 61.1839C39.1768 61.1839 40.3978 61.1839 41.6224 61.1516C46.7435 61.008 52.1411 60.7459 57.1796 59.7621C57.3053 59.7369 57.431 59.7154 57.5387 59.6831C65.4861 58.157 73.0493 53.3672 73.8178 41.2381C73.8465 40.7606 73.9184 36.2364 73.9184 35.7409C73.9219 34.0569 74.4606 23.7949 73.8393 17.4898Z" mask="url(#masto-m)"/></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

-1
View File
@@ -1 +0,0 @@
<svg viewBox="14 34.5 122.5 57" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M75.091,35.685c-12.438,0-22.981,8.433-26.248,19.857-2.84-6.06-8.994-10.305-16.082-10.305-9.748,0-17.761,8.013-17.761,17.761s8.013,17.765,17.761,17.765c7.088,0,13.242-4.248,16.082-10.309,3.268,11.426,13.81,19.861,26.248,19.861,12.346,0,22.835-8.308,26.186-19.605,2.892,5.924,8.97,10.053,15.958,10.053,9.748,0,17.765-8.017,17.765-17.765s-8.017-17.761-17.765-17.761c-6.988,0-13.065,4.127-15.958,10.05-3.352-11.296-13.84-19.601-26.186-19.601h0ZM75.091,46.11c9.39,0,16.89,7.497,16.89,16.887s-7.501,16.89-16.89,16.89-16.887-7.501-16.887-16.89,7.497-16.887,16.887-16.887ZM32.762,55.663c4.114,0,7.338,3.221,7.338,7.335s-3.225,7.338-7.338,7.338-7.335-3.225-7.335-7.338,3.221-7.335,7.335-7.335ZM117.234,55.663c4.114,0,7.338,3.221,7.338,7.335s-3.225,7.338-7.338,7.338-7.335-3.225-7.335-7.338,3.221-7.335,7.335-7.335Z"/></svg>

Before

Width:  |  Height:  |  Size: 930 B

-1
View File
@@ -1 +0,0 @@
<svg viewBox="2799 -911 512 682.688" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="m2799-911v341.344l256-170.656"/><path d="m2799-569.656v341.344l256-170.656"/><path d="m3055-740.344v341.344l256-170.656"/></svg>

Before

Width:  |  Height:  |  Size: 249 B

-1
View File
@@ -1 +0,0 @@
<svg viewBox="0 0 307 307" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" style="fill-rule:evenodd;clip-rule:evenodd;"><g transform="matrix(1,0,0,1,-757,-1483)"><g transform="matrix(1.432974,0,0,1.80298,-288.929017,-864.20063)"><g transform="matrix(0.629919,0,0,0.500647,539.407527,1110.972982)"><path d="M356.969,675.637C323.753,644.675 302.968,600.546 302.968,551.6C302.968,458.019 378.944,382.044 472.524,382.044C566.105,382.044 642.08,458.019 642.08,551.6C642.08,629.48 589.46,695.166 517.864,715.015C501.974,691.667 487.572,670.399 498.34,635.393C485.202,642.099 476.944,655.288 474.445,669.626C469.556,661.943 464.129,654.533 457.841,647.931C441.837,631.125 420.016,619.373 409.919,597.549C405.306,587.241 402.145,572.504 405.799,561.838C408.436,582.54 418.507,601.587 436.545,612.617C450.296,621.023 465.703,622.408 481.126,625.661C497.203,629.048 522.738,636.688 538.403,633.928C551.425,631.638 578.749,617.255 578.02,601.656C577.765,596.289 571.213,587.245 568.79,582.066C565.566,575.171 567.48,573.682 567.848,566.765C568.885,547.188 563.715,537.516 554.206,521.395C542.674,501.84 530.016,490.024 508.095,482.087C488.062,474.831 457.231,478.938 456.996,479.271C461.046,483.711 464.773,488.906 465.295,495.055C457.084,487.46 449.14,479.387 439.945,472.948C419.607,458.71 406.868,460.052 398.782,484.817C394.568,497.72 391.382,515.255 395.288,528.453C397.657,536.447 402.575,542.952 408.175,548.951C397.217,547.458 390.574,537.94 388.158,527.623C360.997,586.373 354.95,614.781 356.969,675.637ZM558.455,619.379L558.452,619.376C554.235,619.505 550.344,617.264 547.164,614.697C545.37,613.176 542.74,611.209 542.372,608.931C541.643,604.396 545.254,601.153 547.406,599.808C549.251,598.658 551.495,598.017 553.613,597.527C558.442,596.411 563.455,595.377 568.319,596.323C569.774,596.606 571.22,597.077 572.417,597.951C573.614,598.821 574.544,600.138 574.705,601.612C574.824,602.705 574.525,603.796 574.164,604.833C573.322,607.217 572.16,609.476 570.71,611.548C569.274,613.613 567.556,615.508 565.485,616.934C563.414,618.361 560.969,619.304 558.455,619.379ZM486.399,552.769C503.076,550.51 505.109,576.334 489.02,577.418C472.931,578.502 471.118,554.84 486.399,552.769ZM430.213,501.5C431.753,505.171 434.926,507.616 437.667,510.309C431.445,512.197 425.166,510.394 419.704,507.264C420.028,513.033 424.299,518.121 427.498,522.174C422.225,523.384 417.564,521.577 413.944,517.769C413.891,523.13 415.717,527.806 418.347,532.34C408.062,537.456 403.622,526.696 403.097,518.275C402.522,509.02 406.267,485.669 412.907,478.941C420.336,471.412 431.875,480.182 438.166,485.238C443.719,489.698 449.061,495.087 454.092,500.111C447.487,504.476 437.648,504.294 430.213,501.5ZM559.47,568.921C555.834,562.599 554.722,560.76 552.607,556.182C551.168,553.064 550.439,549.366 555.655,549.256C565.117,549.055 568.395,564.041 561.026,569.27C560.844,569.397 560.628,569.465 560.407,569.465C560.02,569.465 559.662,569.257 559.47,568.921ZM553.254,508.59C555.938,471.688 555.624,447.806 521.4,481.304C534.318,487.312 545.181,496.897 553.254,508.59Z"/></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

-1
View File
@@ -1 +0,0 @@
<svg viewBox="181 296 556 462" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M226.328419,494.722069 C372.088573,431.216685 469.284839,389.350049 517.917216,369.122161 C656.772535,311.36743 685.625481,301.334815 704.431427,301.003532 C708.567621,300.93067 717.815839,301.955743 723.806446,306.816707 C728.864797,310.92121 730.256552,316.46581 730.922551,320.357329 C731.588551,324.248848 732.417879,333.113828 731.758626,340.040666 C724.234007,419.102486 691.675104,610.964674 675.110982,699.515267 C668.10208,736.984342 654.301336,749.547532 640.940618,750.777006 C611.904684,753.448938 589.856115,731.588035 561.733393,713.153237 C517.726886,684.306416 492.866009,666.349181 450.150074,638.200013 C400.78442,605.66878 432.786119,587.789048 460.919462,558.568563 C468.282091,550.921423 596.21508,434.556479 598.691227,424.000355 C599.00091,422.680135 599.288312,417.758981 596.36474,415.160431 C593.441168,412.561881 589.126229,413.450484 586.012448,414.157198 C581.598758,415.158943 511.297793,461.625274 375.109553,553.556189 C355.154858,567.258623 337.080515,573.934908 320.886524,573.585046 C303.033948,573.199351 268.692754,563.490928 243.163606,555.192408 C211.851067,545.013936 186.964484,539.632504 189.131547,522.346309 C190.260287,513.342589 202.659244,504.134509 226.328419,494.722069 Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

-1
View File
@@ -1 +0,0 @@
<svg viewBox="15.6 11.4 17.1 19" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M25.5172 25.0575C25.4647 26.4295 24.3093 27.5306 22.8929 27.5306C22.5691 27.5306 22.2591 27.4729 21.9727 27.3675C22.2591 27.4729 22.5692 27.5306 22.893 27.5306C24.3094 27.5306 25.4648 26.4295 25.5174 25.0576L25.5224 12.8063H27.8124C28.0331 13.971 28.7405 14.9704 29.7205 15.5948C29.7208 15.5952 29.7212 15.5956 29.7215 15.596C30.4037 16.0305 31.2171 16.284 32.0903 16.284V16.9645C32.0903 16.9645 32.0903 16.9645 32.0904 16.9646V19.3405C30.4684 19.3405 28.9654 18.8334 27.7386 17.9728V24.1852C27.7386 27.2878 25.1559 29.8119 21.9812 29.8119C20.7545 29.8119 19.6169 29.4338 18.6818 28.7921C18.6813 28.7916 18.6808 28.791 18.6802 28.7905C17.1964 27.7717 16.224 26.0876 16.224 24.1846C16.224 21.0821 18.8067 18.5579 21.9814 18.5579C22.2448 18.5579 22.503 18.5791 22.7572 18.6128V19.337C19.861 19.4031 17.4865 21.5682 17.1713 24.3333C17.4868 21.5685 19.8611 19.4037 22.7571 19.3376V21.7342C22.5115 21.6591 22.2519 21.6154 21.9812 21.6154C20.5314 21.6154 19.3519 22.7683 19.3519 24.1852C19.3519 25.1719 19.9247 26.0292 20.7616 26.4596C20.7616 26.4597 20.7616 26.4597 20.7616 26.4597C21.1265 26.6474 21.541 26.7549 21.9811 26.7549C23.3975 26.7549 24.5529 25.6538 24.6055 24.2819L24.6105 12.0305H27.7385C27.7385 12.2955 27.7646 12.5545 27.8123 12.8063H25.5223L25.5172 25.0575Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

-1
View File
@@ -1 +0,0 @@
<svg viewBox="0 0 720 720" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M360,0C161.18,0,0,161.18,0,360c0,65.41,17.45,126.75,47.94,179.61L0,720l187.02-44.21c51.34,28.18,110.28,44.21,172.98,44.21,198.82,0,360-161.18,360-360S558.82,0,360,0ZM360,655.52c-60.17,0-116.13-17.98-162.82-48.87l-110.49,28.14,30.99-105.61c-33.53-47.93-53.2-106.26-53.2-169.19,0-163.21,132.31-295.52,295.52-295.52s295.52,132.31,295.52,295.52-132.31,295.52-295.52,295.52Z"/><path d="M444.35,407.52l87.1,41.06c4,1.88,6.56,5.94,6.2,10.34-.94,11.46-5.54,34.43-26.13,55.02-58.12,58.12-162.49-7.64-166.74-10.18-25.67-13.79-50.06-32.24-73.19-55.36-23.12-23.12-41.58-47.52-55.37-73.19-2.55-4.24-68.31-108.61-10.18-166.74,20.59-20.59,43.56-25.19,55.02-26.13,4.41-.36,8.46,2.2,10.34,6.2l41.07,87.1c1.94,4.12,1.09,9.02-2.13,12.24l-30.61,30.61c-6.62,6.62-8.56,16.93-4,25.11,11.17,20.03,26.19,39.32,43.59,57.07,17.75,17.4,37.04,32.43,57.07,43.59,8.18,4.56,18.48,2.62,25.11-4l30.61-30.61c3.22-3.22,8.12-4.08,12.24-2.13Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

-1
View File
@@ -1 +0,0 @@
<svg viewBox="0 0 1200 1227" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z"/></svg>

Before

Width:  |  Height:  |  Size: 415 B

-115
View File
@@ -1,115 +0,0 @@
<!DOCTYPE html>
<html lang="{{ locale }}" data-locale="{{ locale }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Title & Description -->
<title>{% if title %}{{ title }} | {{ t.site.name }}{% else %}{{ t.site.name }} | {{ t.site.shortName }}{% endif %}</title>
<meta name="description" content="{{ description or t.site.description }}">
<meta name="keywords" content="{{ t.site.keywords }}">
<meta name="author" content="{{ t.site.name }}">
<!-- Canonical URL -->
<link rel="canonical" href="https://o-k-i.net{{ page.url or '/' }}">
<!-- Alternate languages -->
<link rel="alternate" hreflang="fr" href="https://o-k-i.net/">
<link rel="alternate" hreflang="en" href="https://o-k-i.net/en/">
<link rel="alternate" hreflang="x-default" href="https://o-k-i.net/">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://o-k-i.net{{ page.url or '/' }}">
<meta property="og:title" content="{% if title %}{{ title }}{% else %}{{ t.site.name }} - {{ t.site.og_tagline }}{% endif %}">
<meta property="og:description" content="{{ description or t.site.description }}">
<meta property="og:image" content="{{ '/assets/images/logo-512x512.png' | cdn }}">
<meta property="og:image:width" content="512">
<meta property="og:image:height" content="512">
<meta property="og:image:alt" content="Logo {{ t.site.name }}">
<meta property="og:locale" content="{{ t.site.og_locale }}">
<meta property="og:site_name" content="{{ t.site.name }}">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="https://o-k-i.net{{ page.url or '/' }}">
<meta name="twitter:title" content="{% if title %}{{ title }}{% else %}{{ t.site.name }} - {{ t.site.og_tagline }}{% endif %}">
<meta name="twitter:description" content="{{ description or t.site.description }}">
<meta name="twitter:image" content="{{ '/assets/images/logo-512x512.png' | cdn }}">
<meta name="twitter:image:alt" content="Logo {{ t.site.name }}">
<!-- Favicons -->
<link rel="icon" type="image/x-icon" href="{{ '/assets/images/favicon.ico' | cdn }}">
<link rel="icon" type="image/png" sizes="512x512" href="{{ '/assets/images/logo-512x512.png' | cdn }}">
<link rel="apple-touch-icon" href="{{ '/assets/images/logo-512x512.png' | cdn }}">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@600;700;800;900&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Stylesheet -->
<link rel="stylesheet" href="/assets/css/styles.css">
<!-- JSON-LD Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "{{ t.site.name }}",
"alternateName": "{{ t.site.shortName }}",
"url": "https://o-k-i.net",
"logo": "{{ '/assets/images/logo-512x512.png' | cdn }}",
"description": "{{ t.site.description }}",
"address": {
"@type": "PostalAddress",
"streetAddress": "36 Rue Lethière",
"addressLocality": "Basse-Terre",
"postalCode": "97100",
"addressCountry": "GP"
},
"contactPoint": {
"@type": "ContactPoint",
"email": "kontak@o-k-i.net",
"contactType": "customer service"
},
"sameAs": [
"https://niyaj.o-k-i.net/",
"https://kute.o-k-i.net/",
"https://labola.o-k-i.net/",
"https://liberapay.com/OKI/donate"
],
"foundingDate": "2024",
"nonprofitStatus": "NonprofitANBI"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "MusicGroup",
"name": "DJANGOKAM",
"genre": ["Reggae", "Zouk", "Konpa", "K-pop"],
"url": "https://djangokam.pawol.nu",
"sameAs": ["https://djangokam.pawol.nu"],
"member": {
"@type": "Organization",
"name": "{{ t.site.name }}",
"url": "https://o-k-i.net"
}
}
</script>
<script src="/assets/js/theme.js"></script>
<script src="/assets/js/lang-redirect.js"></script>
</head>
<body>
{% include "partials/nav.njk" %}
{{ content | safe }}
{% include "partials/footer.njk" %}
<script src="/assets/js/main.js"></script>
</body>
</html>
-24
View File
@@ -1,24 +0,0 @@
<section id="clients" class="partners">
<div class="section-container">
<h2>{{ t.clients.section_title }} <span class="accent-text">{{ t.clients.section_title_accent }}</span></h2>
<p class="section-subtitle">
{{ t.clients.section_subtitle }}
</p>
<div class="partners-grid">
{% for client in t.clients.items %}
<a href="{{ client.url }}" target="_blank" rel="noopener noreferrer" class="partner-card">
<div class="partner-logo-container">
<img src="{{ client.logo | cdn }}" alt="Logo {{ client.name }}" class="partner-logo">
</div>
<h3>{{ client.name }}</h3>
<p class="project-domain">{{ client.domain }}</p>
<p class="partner-description">{{ client.description }}</p>
<span class="partner-link">
{{ t.clients.link_text }} →
</span>
</a>
{% endfor %}
</div>
</div>
</section>
-33
View File
@@ -1,33 +0,0 @@
<section class="contact" id="contact">
<div class="section-container">
<h2>{{ t.contact.title }}</h2>
<p class="contact-lede">
{{ t.contact.lede | safe }}
</p>
</div>
<div class="contact-socials">
{% for section in t.contact.sections %}
<div class="contact-socials-group">
<h3 class="contact-socials-title">{{ section.title }}</h3>
<div class="contact-socials-row">
{% for link in section.links %}
{% set lname = link.text.toLowerCase() %}
<a href="{{ link.url }}" title="{{ link.text }}"{% if link.url.startsWith('http') %} target="_blank" rel="noopener noreferrer"{% endif %} class="contact-social-link">
{% if 'kontak' in lname or 'mail' in lname %}
{% include "icons/email.svg" %}
{% elif 'whatsapp' in lname %}
{% include "icons/whatsapp.svg" %}
{% elif 'telegram' in lname %}
{% include "icons/telegram.svg" %}
{% elif 'discord' in lname %}
{% include "icons/discord.svg" %}
{% elif 'labola' in lname or 'gitea' in lname %}
{% include "icons/gitea.svg" %}
{% endif %}
</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</section>
-63
View File
@@ -1,63 +0,0 @@
<section id="djangokam" class="djangokam">
<div class="section-container">
<p class="djangokam-surtitre">{{ t.djangokam.surtitre }}</p>
<h2>{{ t.djangokam.titre }} <span class="accent-text">{{ t.djangokam.titreAccent }}</span></h2>
<p class="section-subtitle">{{ t.djangokam.accroche }}</p>
<img src="{{ '/assets/images/djangokam-portrait.webp' | cdn }}" alt="Portrait officiel de DJANGOKAM" class="djangokam-portrait" width="512" height="512" loading="lazy">
<blockquote class="djangokam-highlight">
{{ t.djangokam.message_cle }}
</blockquote>
<div class="djangokam-histoire-wrapper">
<div class="djangokam-histoire">
{% for paragraphe in t.djangokam.histoire %}
<p>{{ paragraphe }}</p>
{% endfor %}
</div>
<figure class="djangokam-duo">
<a href="{{ t.djangokam.duo.url }}" target="_blank" rel="noopener noreferrer">
<img src="{{ t.djangokam.duo.image | cdn }}" alt="{{ t.djangokam.duo.alt }}" loading="lazy">
<figcaption>{{ t.djangokam.duo.legende }}</figcaption>
</a>
</figure>
</div>
<div class="djangokam-facts">
{% for fait in t.djangokam.faits %}
<div class="djangokam-fact-card">
<div class="djangokam-fact-icon">{{ fait.icon }}</div>
<h3>{{ fait.chiffre }}</h3>
<p>{{ fait.texte | safe }}</p>
</div>
{% endfor %}
</div>
<div class="djangokam-clips">
{% for clip in t.djangokam.clips %}
<figure class="djangokam-clip">
<a href="{{ clip.url }}" target="_blank" rel="noopener noreferrer">
<img src="{{ clip.image | cdn }}" alt="{{ clip.alt }}" loading="lazy">
<figcaption>{{ clip.titre }}</figcaption>
</a>
</figure>
{% endfor %}
</div>
<p class="djangokam-instrument">{{ t.djangokam.ia_instrument | safe }}</p>
<div class="cta-buttons djangokam-buttons">
{% for bouton in t.djangokam.boutons %}
<a href="{{ bouton.url }}" target="_blank" rel="noopener noreferrer" class="btn {% if bouton.primary %}btn-primary{% else %}btn-secondary{% endif %}">
{{ bouton.text }} →
</a>
{% endfor %}
</div>
<p class="djangokam-presse">
{{ t.djangokam.presse.texte }} <a href="mailto:{{ t.djangokam.presse.email }}">{{ t.djangokam.presse.email }}</a>
</p>
</div>
</section>
-19
View File
@@ -1,19 +0,0 @@
<section id="engagement" class="engagement">
<div class="section-container">
<h2>{{ t.engagement.section_title }} <span class="accent-text">{{ t.engagement.section_title_accent }}</span></h2>
<p class="section-subtitle">
{{ t.engagement.section_subtitle }}
</p>
<div class="engagement-options">
{% for card in t.engagement.cards %}
<div class="engagement-card">
<div class="engagement-icon">{{ card.icon }}</div>
<h3>{{ card.title }}</h3>
<p>{{ card.text }}</p>
<a href="#contact" class="btn btn-primary">{{ card.cta }}</a>
</div>
{% endfor %}
</div>
</div>
</section>
-31
View File
@@ -1,31 +0,0 @@
<section id="faq" class="faq">
<div class="section-container">
<h2>{{ t.faq.section_title }} <span class="accent-text">{{ t.faq.section_title_accent }}</span></h2>
<p class="section-subtitle">
{{ t.faq.section_subtitle }}
</p>
<div class="faq-container">
{% for item in t.faq.items %}
<div class="faq-item">
<button class="faq-question">
<span>{{ item.question }}</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-answer">
<p>{{ item.answer }}</p>
</div>
</div>
{% endfor %}
</div>
<div class="faq-footer">
<p>
{{ t.faq.footer_text }}
</p>
<a href="#contact" class="btn btn-secondary">
{{ t.faq.footer_cta }} →
</a>
</div>
</div>
</section>
-24
View File
@@ -1,24 +0,0 @@
<section id="services-libres" class="fediverse">
<div class="section-container">
<h2>{{ t.services.section_title }} <span class="accent-text">{{ t.services.section_title_accent }}</span></h2>
<p class="section-subtitle">
{{ t.services.section_subtitle }}
</p>
<div class="services-grid">
{% for service in t.services.items %}
<div class="service-card">
<div class="service-icon">{% include "icons/" + (service.platform | lower) + ".svg" %}</div>
<h3>{{ service.name }}</h3>
<div class="service-platform">{{ service.platform }}</div>
<p class="service-description">
{{ service.description }}
</p>
<a href="{{ service.url }}" target="_blank" class="service-link">
{{ service.linkText }} →
</a>
</div>
{% endfor %}
</div>
</div>
</section>
-30
View File
@@ -1,30 +0,0 @@
<footer>
<div class="flag-bar"></div>
<div class="footer-content">
<p class="footer-name">
{{ t.site.name }}
</p>
<p class="footer-meta">
© {% year %} {{ t.site.name }} | {{ t.site.tagline }}
</p>
<p class="footer-legal">
<a href="{% if locale == 'en' %}/en/legal-notice/{% else %}/mentions-legales/{% endif %}">
{{ t.footer.legal_link }}
</a>
</p>
<div class="footer-socials">
<a href="mailto:kontak@o-k-i.net" title="Email" class="social-link">
{% include "icons/email.svg" %}
</a>
<a href="https://wa.me/262692229740" target="_blank" rel="noopener noreferrer" title="WhatsApp" class="social-link">
{% include "icons/whatsapp.svg" %}
</a>
<a href="https://discord.gg/DPvUCA8dD" target="_blank" rel="noopener noreferrer" title="Discord" class="social-link">
{% include "icons/discord.svg" %}
</a>
<a href="https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/o-k-i.net" target="_blank" rel="noopener noreferrer" title="Code source sur Labola (Gitea)" class="social-link">
{% include "icons/gitea.svg" %}
</a>
</div>
</div>
</footer>
-19
View File
@@ -1,19 +0,0 @@
<section class="hero">
<div class="hero-content">
<h1>ORGANISATION KA</h1>
<h2 class="hero-subtitle">INTERNATIONALE</h2>
<p class="hero-tagline">
{{ t.site.tagline }}
</p>
<div class="hero-tags">
{% for tag in t.hero.tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</div>
<p>{{ t.site.description }}</p>
<div class="cta-buttons">
<a href="#solutions" class="btn btn-primary">{{ t.hero.cta_primary }}</a>
<a href="#mission" class="btn btn-secondary">{{ t.hero.cta_secondary }}</a>
</div>
</div>
</section>
-63
View File
@@ -1,63 +0,0 @@
<section id="hebergement" class="hosting">
<div class="section-container">
<h2>{{ t.hosting.section_title }} <span class="accent-text">{{ t.hosting.section_title_accent }}</span></h2>
<p class="section-subtitle">
{{ t.hosting.section_subtitle }}
</p>
<div class="hosting-banner">
<h3>{{ t.hosting.banner_title }}</h3>
<p>{{ t.hosting.banner_text }}</p>
<div class="hosting-features">
{% for feature in t.hosting.features %}
<div class="hosting-feature">
<span>✓</span> {{ feature }}
</div>
{% endfor %}
</div>
</div>
<div class="services-offerings">
<h3>{{ t.hosting.offerings_title }}</h3>
<div class="offer-cards">
{% for card in t.hosting.offer_cards %}
<div class="offer-card{% if card.modifier %} offer-card--{{ card.modifier }}{% endif %}">
<h4>{{ card.title }}</h4>
{% if card.intro %}
<p class="offer-intro">{{ card.intro }}</p>
{% endif %}
<ul class="offer-list">
{% for item in card.items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
<p class="offer-tagline">{{ card.tagline }}</p>
</div>
{% endfor %}
</div>
<div class="hosting-note">
<h4>{{ t.hosting.note_title }}</h4>
<p>{{ t.hosting.note_intro }}</p>
<ul>
{% for item in t.hosting.note_items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
</div>
<div class="hosting-why">
<h4>{{ t.hosting.why_title }}</h4>
<p>
{{ t.hosting.why_text }}
</p>
<a href="#contact" class="btn btn-primary">
{{ t.hosting.why_cta }} →
</a>
</div>
</div>
</div>
</section>
-21
View File
@@ -1,21 +0,0 @@
<section id="mission" class="mission">
<div class="section-container">
<h2>{{ t.mission.section_title }} <span class="accent-text">{{ t.mission.section_title_accent }}</span></h2>
<p class="mission-text">
{{ t.mission.paragraph_main }}
</p>
<p class="mission-text mission-text--secondary">
{{ t.mission.paragraph_secondary }}
</p>
<div class="values-grid">
{% for value in t.values %}
<div class="value-card">
<div class="value-icon">{{ value.icon }}</div>
<h3>{{ value.title }}</h3>
<p>{{ value.description }}</p>
</div>
{% endfor %}
</div>
</div>
</section>
-49
View File
@@ -1,49 +0,0 @@
<nav>
<div class="nav-container">
<a href="/" class="logo">
<img src="{{ '/assets/images/logo-512x512.png' | cdn }}" alt="{{ t.site.shortName }} Logo" class="logo-image">
<span>{{ t.site.shortName }}</span>
</a>
<ul class="nav-links">
{% for item in t.navigation %}
{% if item.children %}
<li class="has-dropdown">
<button type="button" class="nav-dropdown-trigger" aria-haspopup="true" aria-expanded="false">
{{ item.text }} <span class="dropdown-caret">▾</span>
</button>
<ul class="nav-dropdown">
{% for child in item.children %}
<li>
<a href="{{ child.url }}"{% if child.external %} target="_blank"{% endif %}>
{{ child.text }}
</a>
</li>
{% endfor %}
</ul>
</li>
{% else %}
<li>
<a href="{{ item.url }}"{% if item.external %} target="_blank"{% endif %}>
{{ item.text }}
</a>
</li>
{% endif %}
{% endfor %}
</ul>
{% if locale == 'en' %}
<a href="#" class="lang-switch" data-lang="fr">{{ t.ui.lang_switch_label }}</a>
{% else %}
<a href="#" class="lang-switch" data-lang="en">{{ t.ui.lang_switch_label }}</a>
{% endif %}
<button id="theme-toggle" class="theme-toggle" aria-label="{{ t.ui.theme_toggle }}" title="{{ t.ui.theme_toggle }}">
<span class="theme-icon theme-icon-dark">🌙</span>
<span class="theme-icon theme-icon-light">☀️</span>
</button>
<div class="menu-toggle">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div class="flag-bar"></div>
</nav>
-27
View File
@@ -1,27 +0,0 @@
<section id="partenaires" class="partners">
<div class="section-container">
<h2>{{ t.partners.section_title }} <span class="accent-text">{{ t.partners.section_title_accent }}</span></h2>
<p class="section-subtitle">
{{ t.partners.section_subtitle }}
</p>
<div class="partners-grid">
{% for partner in t.partners.items %}
<div class="partner-card">
<div class="partner-logo-container">
<img src="{{ partner.logo | cdn }}" alt="Logo {{ partner.name }}" class="partner-logo">
</div>
<h3>{{ partner.name }}</h3>
<p class="partner-description">{{ partner.description }}</p>
<div class="partner-links">
{% for link in partner.links %}
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer" class="partner-link">
{{ link.text }} →
</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
</section>
-19
View File
@@ -1,19 +0,0 @@
<section id="projets" class="projects">
<div class="section-container">
<h2>{{ t.projects.section_title }} <span class="accent-text">{{ t.projects.section_title_accent }}</span></h2>
<div class="project-cards">
{% for project in t.projects.items %}
<a href="{{ project.url }}" target="_blank" rel="noopener noreferrer" class="project-card">
<div class="project-icon">{{ project.icon }}</div>
<h3>{{ project.name }}</h3>
<p class="project-domain">{{ project.domain }}</p>
<p>{{ project.description }}</p>
<span class="service-link project-card-link">
{{ t.projects.link_text }} →
</span>
</a>
{% endfor %}
</div>
</div>
</section>
-40
View File
@@ -1,40 +0,0 @@
<section id="solutions" class="publics">
<div class="section-container">
<h2>{{ t.publics.section_title }} <span class="accent-text">{{ t.publics.section_title_accent }}</span></h2>
<p class="section-subtitle">
{{ t.publics.section_subtitle }}
</p>
<div class="publics-grid">
{% for public in t.publics.items %}
<div class="public-card">
<div class="public-icon">{{ public.icon }}</div>
<h3>{{ public.title }}</h3>
<p class="public-description">{{ public.description }}</p>
<div class="public-benefits">
<h4>{{ t.publics.benefits_label }}</h4>
<ul>
{% for benefit in public.benefits %}
<li>✓ {{ benefit }}</li>
{% endfor %}
</ul>
</div>
<a href="#contact" class="btn btn-primary public-cta">
{{ public.cta }} →
</a>
</div>
{% endfor %}
</div>
<div class="publics-footer">
<p>
{{ t.publics.footer_line1 }}
</p>
<p>
{{ t.publics.footer_line2 }}
</p>
</div>
</div>
</section>
-94
View File
@@ -1,94 +0,0 @@
<section id="dons" class="support">
<div class="section-container">
<h2>{{ t.support.section_title }} <span class="accent-text">{{ t.support.section_title_accent }}</span></h2>
<p class="support-lede">
{{ t.support.lede }}
</p>
<p class="support-lede-secondary">
{{ t.support.lede_secondary }}
</p>
<div class="donation-panels">
<div class="support-panel support-panel--primary">
<h3>{{ t.support.one_time_title }}</h3>
<div class="donation-grid">
{% for d in donations.oneTime %}
<a href="{{ d.url }}" class="donation-amount" target="_blank" rel="noopener noreferrer">
{{ d.amount }}{{ t.support.one_time_suffix }}
</a>
{% endfor %}
</div>
<div style="text-align: center;">
<label class="donation-form-label">{{ t.support.custom_amount_label }}</label>
<div class="custom-donation-form">
<input type="number" id="customAmountOneTime" placeholder="{{ t.support.custom_amount_placeholder }}" min="1" class="custom-amount-input">
<span class="currency-symbol">{{ t.support.one_time_suffix }}</span>
<button data-donation-type="oneTime" class="custom-donation-btn custom-donation-btn-primary">
{{ t.support.custom_amount_btn }}
</button>
</div>
</div>
</div>
<div class="support-panel support-panel--secondary">
<h3>{{ t.support.monthly_title }}</h3>
<div class="donation-grid">
{% for d in donations.monthly %}
<a href="{{ d.url }}" class="donation-amount monthly" target="_blank" rel="noopener noreferrer">
{{ d.amount }}{{ t.support.monthly_suffix }}
</a>
{% endfor %}
</div>
<div style="text-align: center;">
<label class="donation-form-label">{{ t.support.custom_monthly_label }}</label>
<div class="custom-donation-form">
<input type="number" id="customAmountMonthly" placeholder="{{ t.support.custom_amount_placeholder }}" min="1" class="custom-amount-input custom-amount-input-monthly">
<span class="currency-symbol">{{ t.support.monthly_suffix }}</span>
<button data-donation-type="monthly" class="custom-donation-btn custom-donation-btn-secondary">
{{ t.support.custom_monthly_btn }}
</button>
</div>
</div>
</div>
</div>
<div class="alt-donation-section">
<h3 class="alt-donation-title">
{{ t.support.alt_title }}
</h3>
<div class="alt-donation-grid">
<a href="{{ donations.liberapay }}" target="_blank" rel="noopener noreferrer" class="alt-donation-card alt-donation-card--liberapay">
<div class="alt-donation-icon">💛</div>
<h4>{{ t.support.liberapay_title }}</h4>
<p>
{{ t.support.liberapay_text }}
</p>
</a>
<a href="{{ donations.kofi }}" target="_blank" rel="noopener noreferrer" class="alt-donation-card alt-donation-card--kofi">
<div class="alt-donation-icon">☕</div>
<h4>{{ t.support.kofi_title }}</h4>
<p>
{{ t.support.kofi_text }}
</p>
</a>
</div>
</div>
<div class="security-note">
<p>
{{ t.support.security_stripe | safe }}
</p>
<p>
{{ t.support.security_alt }}
</p>
</div>
</div>
</section>
+12
View File
@@ -0,0 +1,12 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+20
View File
@@ -0,0 +1,20 @@
<!doctype html>
<html lang="%lang%" data-locale="%lang%">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="/theme.js"></script>
<script src="/lang-redirect.js" defer></script>
<script src="/registerSW.js" defer></script>
<link rel="preload" href="/fonts/archivo-latin-700-normal.woff2" as="font" type="font/woff2" crossorigin />
<link rel="stylesheet" href="/fonts/fonts.css" />
<link rel="icon" href="/favicon.ico" sizes="48x48" />
<link rel="icon" href="/images/logo-512x512.png" type="image/png" />
<link rel="apple-touch-icon" href="/images/logo-512x512.png" />
<link rel="manifest" href="/manifest.webmanifest" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
File diff suppressed because it is too large Load Diff
-363
View File
@@ -1,363 +0,0 @@
// Theme Management
(function() {
const themeToggle = document.getElementById('theme-toggle');
// Fonction pour obtenir le thème actuel
function getCurrentTheme() {
return document.documentElement.classList.contains('light-theme') ? 'light' : 'dark';
}
// Fonction pour changer de thème
function toggleTheme() {
const currentTheme = getCurrentTheme();
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
if (newTheme === 'light') {
document.documentElement.classList.add('light-theme');
} else {
document.documentElement.classList.remove('light-theme');
}
// Sauvegarder la préférence
localStorage.setItem('oki-theme', newTheme);
// Mettre à jour les styles de la nav en fonction du scroll actuel
const nav = document.querySelector('nav');
const currentScroll = window.pageYOffset;
const isLightTheme = newTheme === 'light';
if (currentScroll > 100) {
if (isLightTheme) {
nav.style.background = 'rgba(255, 248, 231, 0.98)';
nav.style.boxShadow = '0 2px 20px rgba(184, 122, 0, 0.3)';
} else {
nav.style.background = 'rgba(0, 0, 0, 0.98)';
nav.style.boxShadow = '0 2px 20px rgba(232, 166, 37, 0.3)';
}
} else {
if (isLightTheme) {
nav.style.background = 'rgba(255, 248, 231, 0.95)';
nav.style.boxShadow = '';
} else {
nav.style.background = 'rgba(0, 0, 0, 0.95)';
nav.style.boxShadow = '';
}
}
}
// Écouter le changement de thème système
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
darkModeMediaQuery.addEventListener('change', (e) => {
// Ne changer automatiquement que si l'utilisateur n'a pas de préférence stockée
if (!localStorage.getItem('oki-theme')) {
if (e.matches) {
document.documentElement.classList.remove('light-theme');
} else {
document.documentElement.classList.add('light-theme');
}
}
});
// Ajouter l'événement au bouton
themeToggle?.addEventListener('click', toggleTheme);
// Language switcher links
document.querySelectorAll('.lang-switch').forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault();
var lang = this.dataset.lang;
if (lang) {
localStorage.setItem('oki-lang-pref', lang);
switchLanguage(lang);
}
});
});
})();
// Language switch with page mapping
function switchLanguage(lang) {
const currentPath = window.location.pathname;
let newPath;
if (lang === 'fr') {
// Switch to French
if (currentPath.startsWith('/en/')) {
newPath = currentPath.substring(3); // Remove '/en'
} else {
newPath = currentPath;
}
newPath = newPath.replace('/press/', '/presse/');
newPath = newPath.replace('/legal-notice/', '/mentions-legales/');
newPath = newPath.replace('/donate/', '/dons/');
} else {
// Switch to English
if (!currentPath.startsWith('/en/')) {
newPath = '/en' + currentPath;
} else {
newPath = currentPath;
}
newPath = newPath.replace('/presse/', '/press/');
newPath = newPath.replace('/mentions-legales/', '/legal-notice/');
newPath = newPath.replace('/dons/', '/donate/');
}
window.location.href = newPath;
}
// Smooth scrolling (uniquement pour les ancres de la page courante)
document.querySelectorAll('a[href*="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
const url = new URL(this.href, window.location.href);
if (url.pathname !== window.location.pathname || !url.hash) return;
const target = document.querySelector(url.hash);
if (target) {
e.preventDefault();
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Mobile menu toggle
const menuToggle = document.querySelector('.menu-toggle');
const navLinks = document.querySelector('.nav-links');
let menuOpen = false;
menuToggle?.addEventListener('click', () => {
menuOpen = !menuOpen;
navLinks.classList.toggle('mobile-open', menuOpen);
menuToggle.classList.toggle('open', menuOpen);
});
// Si la fenêtre est agrandie au-delà du breakpoint mobile pendant que le
// menu est ouvert, on réinitialise son état pour éviter qu'il reste caché
// ou en position absolue quand la nav desktop redevient visible.
window.addEventListener('resize', () => {
if (menuOpen && window.innerWidth > 1100) {
menuOpen = false;
navLinks.classList.remove('mobile-open');
menuToggle.classList.remove('open');
}
});
// Close mobile menu when clicking outside
document.addEventListener('click', (e) => {
if (menuOpen && !menuToggle.contains(e.target) && !navLinks.contains(e.target)) {
menuOpen = false;
navLinks.classList.remove('mobile-open');
menuToggle.classList.remove('open');
}
});
// Close mobile menu after clicking a nav link
navLinks?.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
if (menuOpen) {
menuOpen = false;
navLinks.classList.remove('mobile-open');
menuToggle.classList.remove('open');
}
});
});
// Nav dropdown ("Ressources") toggle for touch/keyboard
document.querySelectorAll('.nav-dropdown-trigger').forEach(trigger => {
trigger.addEventListener('click', () => {
const item = trigger.closest('.has-dropdown');
const isOpen = item.classList.toggle('open');
trigger.setAttribute('aria-expanded', isOpen);
});
});
document.addEventListener('click', (e) => {
document.querySelectorAll('.has-dropdown.open').forEach(item => {
if (!item.contains(e.target)) {
item.classList.remove('open');
item.querySelector('.nav-dropdown-trigger')?.setAttribute('aria-expanded', 'false');
}
});
});
// Scroll animations
const observerOptions = {
threshold: 0.15,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in');
observer.unobserve(entry.target);
}
});
}, observerOptions);
// Observe all cards
document.querySelectorAll('.service-card, .project-card, .value-card, .partner-card, .public-card, .engagement-card').forEach(el => {
el.classList.add('fade-in-element');
observer.observe(el);
});
// Navbar scroll effect
let lastScroll = 0;
const nav = document.querySelector('nav');
window.addEventListener('scroll', () => {
const currentScroll = window.pageYOffset;
const isLightTheme = document.documentElement.classList.contains('light-theme');
if (currentScroll > 100) {
if (isLightTheme) {
nav.style.background = 'rgba(255, 248, 231, 0.98)';
nav.style.boxShadow = '0 2px 20px rgba(184, 122, 0, 0.3)';
} else {
nav.style.background = 'rgba(0, 0, 0, 0.98)';
nav.style.boxShadow = '0 2px 20px rgba(232, 166, 37, 0.3)';
}
} else {
if (isLightTheme) {
nav.style.background = 'rgba(255, 248, 231, 0.95)';
nav.style.boxShadow = '';
} else {
nav.style.background = 'rgba(0, 0, 0, 0.95)';
nav.style.boxShadow = '';
}
}
// Hide/show navbar on scroll
if (currentScroll > lastScroll && currentScroll > 500) {
nav.style.transform = 'translateY(-100%)';
} else {
nav.style.transform = 'translateY(0)';
}
lastScroll = currentScroll;
});
// Network animation enhancement
function createNetworkLine() {
const nodes = document.querySelectorAll('.node');
const networkBg = document.querySelector('.network-bg');
if (nodes.length > 1 && networkBg) {
for (let i = 0; i < nodes.length - 1; i++) {
const line = document.createElement('div');
line.style.position = 'absolute';
line.style.height = '1px';
line.style.background = 'linear-gradient(90deg, transparent, #E8A625, transparent)';
line.style.opacity = '0.2';
line.style.animation = `pulse 4s infinite ${i * 0.5}s`;
const node1 = nodes[i];
const node2 = nodes[i + 1];
// Calculate line position and angle
const x1 = parseFloat(node1.style.left);
const y1 = parseFloat(node1.style.top);
const x2 = parseFloat(node2.style.left);
const y2 = parseFloat(node2.style.top);
const distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
const angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
line.style.width = distance + '%';
line.style.left = x1 + '%';
line.style.top = y1 + '%';
line.style.transform = `rotate(${angle}deg)`;
line.style.transformOrigin = '0 0';
networkBg.appendChild(line);
}
}
}
// Initialize network lines
createNetworkLine();
// Add hover effect to project cards with tilt
document.querySelectorAll('.project-card, .service-card').forEach(card => {
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const rotateX = (y - centerY) / 20;
const rotateY = (centerX - x) / 20;
card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) translateY(-5px)`;
});
card.addEventListener('mouseleave', () => {
card.style.transform = '';
});
});
// Easter egg: Konami code
const konamiCode = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a'];
let konamiIndex = 0;
document.addEventListener('keydown', (e) => {
if (e.key === konamiCode[konamiIndex]) {
konamiIndex++;
if (konamiIndex === konamiCode.length) {
document.body.style.animation = 'pulse 1s';
setTimeout(() => {
alert('🎮 Félicitations! Vous avez trouvé le code secret! Bienvenue dans la résistance numérique!');
document.body.style.animation = '';
}, 1000);
konamiIndex = 0;
}
} else {
konamiIndex = 0;
}
});
// Progressive enhancement: Add loading states
// Disabled - not needed for external links
// document.querySelectorAll('.service-link, .project-card').forEach(link => {
// link.addEventListener('click', function(e) {
// if (this.href && this.href !== '#') {
// this.style.opacity = '0.6';
// this.innerHTML += ' ⌛';
// }
// });
// });
// Custom donation handler
function handleCustomDonation(type) {
const input = document.getElementById(type === 'oneTime' ? 'customAmountOneTime' : 'customAmountMonthly');
const amount = input.value;
if (!amount || amount < 1) {
alert('Veuillez entrer un montant valide');
return;
}
// Redirect to custom amount Stripe URL
const stripeUrl = type === 'oneTime'
? 'https://don.o-k-i.net/b/6oE2aO94P88X9u8eV4' // Custom one-time donation
: 'https://don.o-k-i.net/b/6oE2aO94P88X9u8eV4'; // Uses same link for custom amounts
window.location.href = stripeUrl;
}
// FAQ accordion toggle
document.querySelectorAll('.faq-question').forEach(button => {
button.addEventListener('click', () => {
button.parentElement.classList.toggle('active');
});
});
// Custom donation buttons
document.querySelectorAll('.custom-donation-btn').forEach(button => {
button.addEventListener('click', () => {
const type = button.dataset.donationType;
if (type) handleCustomDonation(type);
});
});
-12
View File
@@ -1,12 +0,0 @@
---
layout: layouts/base.njk
title: Dons
description: Soutenez ORGANISATION KA INTERNATIONALE par un don ponctuel ou mensuel via Stripe, Liberapay ou Ko-fi.
---
<section class="press-hero">
<h1>{{ t.support.section_title }} <span class="accent-text">{{ t.support.section_title_accent }}</span></h1>
<p class="press-lede">{{ t.support.lede }}</p>
</section>
{% include "partials/support.njk" %}
-12
View File
@@ -1,12 +0,0 @@
---
layout: layouts/base.njk
title: Donate
description: Support ORGANISATION KA INTERNATIONALE with a one-time or monthly donation through Stripe, Liberapay or Ko-fi.
---
<section class="press-hero">
<h1>{{ t.support.section_title }} <span class="accent-text">{{ t.support.section_title_accent }}</span></h1>
<p class="press-lede">{{ t.support.lede }}</p>
</section>
{% include "partials/support.njk" %}
-3
View File
@@ -1,3 +0,0 @@
{
"locale": "en"
}
-16
View File
@@ -1,16 +0,0 @@
---
layout: layouts/base.njk
---
{% include "partials/hero.njk" %}
{% include "partials/publics.njk" %}
{% include "partials/projects.njk" %}
{% include "partials/partners.njk" %}
{% include "partials/djangokam.njk" %}
{% include "partials/mission.njk" %}
{% include "partials/hosting.njk" %}
{% include "partials/clients.njk" %}
{% include "partials/fediverse.njk" %}
{% include "partials/faq.njk" %}
{% include "partials/engagement.njk" %}
{% include "partials/contact.njk" %}
-222
View File
@@ -1,222 +0,0 @@
---
layout: layouts/base.njk
title: Legal Notice
description: Legal notice and legal information of ORGANISATION KA INTERNATIONALE (OKI) - Hosting, GDPR, AGPL-V3 license
---
<section style="padding: 8rem 2rem 4rem; min-height: 100vh; background: linear-gradient(135deg, var(--noir-profond) 0%, var(--gris-sombre) 100%);">
<div class="section-container" style="max-width: 900px;">
<h1 style="font-size: 2.5rem; margin-bottom: 3rem; text-align: center;">
<span class="accent-text">Legal Notice</span>
</h1>
<!-- Website Presentation -->
<div style="background: rgba(232, 166, 37, 0.1); border-left: 4px solid var(--or-oki); padding: 2rem; margin-bottom: 2rem; border-radius: 10px;">
<h2 style="color: var(--or-oki); margin-bottom: 1.5rem; font-size: 1.8rem;">
1. Website Presentation
</h2>
<p style="margin-bottom: 1rem; line-height: 1.6;">
In accordance with applicable law, we hereby provide users of the website <strong>o-k-i.net</strong>
with the identity of the various parties involved in its creation and management:
</p>
<ul style="list-style: none; padding: 0; margin: 1.5rem 0;">
<li style="margin-bottom: 1rem;">
<strong style="color: var(--or-oki);">Owner:</strong><br>
ORGANISATION KA INTERNATIONALE<br>
36 Rue Lethière, 97100 Basse-Terre
</li>
<li style="margin-bottom: 1rem;">
<strong style="color: var(--or-oki);">Publishing Manager:</strong><br>
ORGANISATION KA INTERNATIONALE<br>
Contact: <a href="mailto:kontak@o-k-i.net" style="color: var(--or-oki); text-decoration: none;">kontak@o-k-i.net</a>
</li>
<li style="margin-bottom: 1rem;">
<strong style="color: var(--or-oki);">Webmaster:</strong><br>
Cédric FAMIBELLE-PRONZOLA<br>
Contact: <a href="mailto:contact@cedric-pronzola.dev" style="color: var(--or-oki); text-decoration: none;">contact@cedric-pronzola.dev</a>
</li>
<li style="margin-bottom: 1rem;">
<strong style="color: var(--or-oki);">Hosting Provider:</strong><br>
o2switch<br>
222-224 Boulevard Gustave Flaubert, 63000 Clermont-Ferrand, France
</li>
<li style="margin-bottom: 1rem;">
<strong style="color: var(--or-oki);">Data Protection Officer:</strong><br>
ORGANISATION KA INTERNATIONALE<br>
Contact: <a href="mailto:kontak@o-k-i.net" style="color: var(--or-oki); text-decoration: none;">kontak@o-k-i.net</a>
</li>
</ul>
</div>
<!-- Terms of Use -->
<div style="background: rgba(0, 168, 84, 0.1); border-left: 4px solid var(--vert-oki); padding: 2rem; margin-bottom: 2rem; border-radius: 10px;">
<h2 style="color: var(--vert-oki); margin-bottom: 1.5rem; font-size: 1.8rem;">
2. Terms of Use of the Website and Services
</h2>
<p style="line-height: 1.6;">
The website is protected by intellectual property laws. Use of the website constitutes full acceptance
of the terms of use described below. These terms of use may be modified or supplemented at any time.
</p>
<p style="margin-top: 1rem; line-height: 1.6;">
Users of the website may not reuse or exploit the elements of the website without prior authorization.
</p>
</div>
<!-- Description of Services -->
<div style="background: rgba(237, 28, 76, 0.1); border-left: 4px solid var(--rouge-oki); padding: 2rem; margin-bottom: 2rem; border-radius: 10px;">
<h2 style="color: var(--rouge-oki); margin-bottom: 1.5rem; font-size: 1.8rem;">
3. Description of Services Provided
</h2>
<p style="line-height: 1.6;">
The website <strong>o-k-i.net</strong> is intended to provide information about all activities
of ORGANISATION KA INTERNATIONALE.
</p>
<p style="margin-top: 1rem; line-height: 1.6;">
ORGANISATION KA INTERNATIONALE strives to provide information that is as accurate as possible.
However, it cannot be held responsible for omissions, inaccuracies and deficiencies in updates,
whether due to its own actions or those of third-party partners providing this information.
</p>
</div>
<!-- Technical Limitations -->
<div style="background: rgba(232, 166, 37, 0.1); border-left: 4px solid var(--or-oki); padding: 2rem; margin-bottom: 2rem; border-radius: 10px;">
<h2 style="color: var(--or-oki); margin-bottom: 1.5rem; font-size: 1.8rem;">
4. Technical Limitations
</h2>
<p style="line-height: 1.6;">
The website uses JavaScript technology. The site is hosted within the European Union in compliance with GDPR.
</p>
<p style="margin-top: 1rem; line-height: 1.6;">
ORGANISATION KA INTERNATIONALE cannot be held responsible for material damage related to the use of the website.
Furthermore, the website user agrees to access the site using recent equipment, free from viruses,
and with a current, up-to-date browser.
</p>
</div>
<!-- Intellectual Property and Licenses -->
<div style="background: rgba(0, 168, 84, 0.1); border-left: 4px solid var(--vert-oki); padding: 2rem; margin-bottom: 2rem; border-radius: 10px;">
<h2 style="color: var(--vert-oki); margin-bottom: 1.5rem; font-size: 1.8rem;">
5. Intellectual Property and Licenses
</h2>
<p style="line-height: 1.6;">
The source code of the website <strong>o-k-i.net</strong> is distributed under the
<strong>GNU Affero General Public License v3.0 (AGPL-V3)</strong>, a free license that guarantees
the following freedoms:
</p>
<ul style="list-style: disc; padding-left: 2rem; margin: 1rem 0;">
<li style="margin-bottom: 0.5rem;">Freedom to use the software for any purpose</li>
<li style="margin-bottom: 0.5rem;">Freedom to study how the program works and adapt it</li>
<li style="margin-bottom: 0.5rem;">Freedom to redistribute copies</li>
<li style="margin-bottom: 0.5rem;">Freedom to improve the program and publish improvements</li>
</ul>
<p style="margin-top: 1rem; line-height: 1.6;">
This license ensures that any modification to the source code must be published under the same AGPL-V3 license.
If you use this code to provide an online service, you must also make the modified source code available
to users of that service.
</p>
<p style="margin-top: 1rem; line-height: 1.6;">
The full text of the license is available at:
<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" style="color: var(--or-oki); text-decoration: none;">
https://www.gnu.org/licenses/agpl-3.0.html
</a>
</p>
<p style="margin-top: 1rem; line-height: 1.6;">
Original textual and visual content of the website (excluding source code) remains the property of
ORGANISATION KA INTERNATIONALE. Its use is subject to the conditions of the AGPL-V3 license for
technical elements, and requires authorization for other uses.
</p>
</div>
<!-- Limitations of Liability -->
<div style="background: rgba(237, 28, 76, 0.1); border-left: 4px solid var(--rouge-oki); padding: 2rem; margin-bottom: 2rem; border-radius: 10px;">
<h2 style="color: var(--rouge-oki); margin-bottom: 1.5rem; font-size: 1.8rem;">
6. Limitations of Liability
</h2>
<p style="line-height: 1.6;">
ORGANISATION KA INTERNATIONALE cannot be held responsible for direct or indirect damage caused to
the user's equipment, when accessing the website o-k-i.net, and resulting either from the use of equipment
not meeting the specifications indicated in section 4, or from the occurrence of a bug or incompatibility.
</p>
<p style="margin-top: 1rem; line-height: 1.6;">
ORGANISATION KA INTERNATIONALE cannot also be held responsible for indirect damage (such as loss of market
or loss of opportunity) resulting from the use of the website o-k-i.net.
</p>
</div>
<!-- Personal Data Management -->
<div style="background: rgba(232, 166, 37, 0.1); border-left: 4px solid var(--or-oki); padding: 2rem; margin-bottom: 2rem; border-radius: 10px;">
<h2 style="color: var(--or-oki); margin-bottom: 1.5rem; font-size: 1.8rem;">
7. Personal Data Management
</h2>
<p style="line-height: 1.6;">
Personal data is protected in particular by European Regulation 2016/679 of April 27, 2016
relating to the protection of natural persons with regard to the processing of personal data (GDPR).
</p>
<p style="margin-top: 1rem; line-height: 1.6;">
Personal data collected on the website o-k-i.net is processed for the following purposes:
</p>
<ul style="list-style: disc; padding-left: 2rem; margin: 1rem 0;">
<li style="margin-bottom: 0.5rem;">Navigation on the website</li>
<li style="margin-bottom: 0.5rem;">Prevention of computer fraud</li>
<li style="margin-bottom: 0.5rem;">Improvement and optimization of navigation</li>
<li style="margin-bottom: 0.5rem;">Audience measurement and statistics</li>
</ul>
<p style="margin-top: 1rem; line-height: 1.6;">
In application of these provisions, you have the right to access, rectify, delete and transfer your personal data.
You can exercise these rights by contacting:
<a href="mailto:kontak@o-k-i.net" style="color: var(--or-oki); text-decoration: none;">kontak@o-k-i.net</a>
</p>
</div>
<!-- Incident Notification -->
<div style="background: rgba(0, 168, 84, 0.1); border-left: 4px solid var(--vert-oki); padding: 2rem; margin-bottom: 2rem; border-radius: 10px;">
<h2 style="color: var(--vert-oki); margin-bottom: 1.5rem; font-size: 1.8rem;">
8. Incident Notification
</h2>
<p style="line-height: 1.6;">
Regardless of the efforts made, no method of transmission over the Internet and no method of electronic storage
is completely secure. We therefore cannot guarantee absolute security.
</p>
<p style="margin-top: 1rem; line-height: 1.6;">
If we became aware of a security breach, we would notify affected users so they can take appropriate measures.
Our incident notification procedures take into account our legal obligations, whether at the national or European level.
We undertake to fully inform our customers of all security-related issues and to provide all necessary information
to help them meet their own regulatory reporting obligations.
</p>
<p style="margin-top: 1rem; line-height: 1.6;">
Security measures in place include: firewalls, encryption, pseudonymization and physical security measures.
</p>
</div>
<!-- Cookies and Web Beacons -->
<div style="background: rgba(237, 28, 76, 0.1); border-left: 4px solid var(--rouge-oki); padding: 2rem; margin-bottom: 2rem; border-radius: 10px;">
<h2 style="color: var(--rouge-oki); margin-bottom: 1.5rem; font-size: 1.8rem;">
9. Cookies and Web Beacons
</h2>
<p style="line-height: 1.6;">
The website may use cookies to enhance the user experience. Users can disable cookies in their browser settings.
</p>
<p style="margin-top: 1rem; line-height: 1.6;">
Some pages may contain web beacons (invisible pixels) to measure website traffic and performance.
</p>
</div>
<!-- Applicable Law and Jurisdiction -->
<div style="background: rgba(232, 166, 37, 0.1); border-left: 4px solid var(--or-oki); padding: 2rem; margin-bottom: 2rem; border-radius: 10px;">
<h2 style="color: var(--or-oki); margin-bottom: 1.5rem; font-size: 1.8rem;">
10. Applicable Law and Jurisdiction
</h2>
<p style="line-height: 1.6;">
Any dispute related to the use of the website o-k-i.net is governed by French law.
Except where the law does not permit, exclusive jurisdiction is granted to the competent courts of Guadeloupe.
</p>
</div>
<!-- Back to Home -->
<div style="text-align: center; margin-top: 3rem;">
<a href="/en/" class="btn btn-primary">
← Back to Home
</a>
</div>
</div>
</section>
-64
View File
@@ -1,64 +0,0 @@
---
layout: layouts/base.njk
title: Press
description: Press resources for DJANGOKAM, the first Caribbean AI artist produced by ORGANISATION KA INTERNATIONALE (OKI) — HD visuals and listening links.
---
<section class="press-hero">
<h1>{{ t.press.title }} <span class="accent-text">{{ t.press.title_accent }}</span></h1>
<p class="press-lede">{{ t.press.lede }}</p>
</section>
<section class="press-facts">
<div class="section-container">
<h2>{{ t.press.facts_title }}</h2>
<div class="press-facts-list">
{% for fact in t.press.facts %}
<div class="press-fact">
<span class="press-fact-label">{{ fact.label }}</span>
<span>{{ fact.value }}</span>
</div>
{% endfor %}
</div>
</div>
</section>
<section class="press-visuals">
<div class="section-container">
<h2>{{ t.press.visuals_title }}</h2>
<p class="press-visuals-intro">{{ t.press.visuals_intro }}</p>
<div class="press-visual-grid">
{% for visual in t.press.visuals %}
<div class="press-visual">
<a href="{{ visual.image | cdn }}" target="_blank" rel="noopener noreferrer">
<img src="{{ visual.image | cdn }}" alt="{{ visual.caption }}" loading="lazy">
<span class="press-visual-caption">{{ visual.caption }}</span>
</a>
<a href="{{ ('/assets/press/' + visual.pdf) | cdn }}" class="press-visual-pdf-link">{{ t.press.visual_pdf_label }}</a>
</div>
{% endfor %}
</div>
<div class="press-visuals-pdf">
<a href="{{ ('/assets/press/' + t.press.visuals_pdf_file) | cdn }}" class="btn btn-primary">{{ t.press.visuals_pdf_label }} →</a>
</div>
</div>
</section>
<section class="press-links">
<div class="section-container">
<h2>{{ t.press.links_title }}</h2>
<div class="press-links-list">
{% for link in t.press.links %}
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer">{{ link.text }}</a>
{% endfor %}
</div>
</div>
</section>
<section class="press-contact">
<div class="section-container">
<h2>{{ t.press.contact_title }}</h2>
<p class="press-contact-text">{{ t.press.contact_text }}</p>
<a href="mailto:{{ t.press.contact_email }}" class="btn btn-primary">{{ t.press.contact_email }}</a>
</div>
</section>
+8
View File
@@ -0,0 +1,8 @@
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
const lang = event.url.pathname.startsWith('/en') ? 'en' : 'fr';
return resolve(event, {
transformPageChunk: ({ html }) => html.replaceAll('%lang%', lang)
});
};
+9
View File
@@ -0,0 +1,9 @@
// Imports vite-imagetools en mode picture (`…&as=picture`).
// Fichier global (non-module) requis pour que le wildcard soit pris en compte.
declare module '*&as=picture' {
const picture: {
sources: Record<string, string>;
img: { src: string; w: number; h: number };
};
export default picture;
}
-16
View File
@@ -1,16 +0,0 @@
---
layout: layouts/base.njk
---
{% include "partials/hero.njk" %}
{% include "partials/publics.njk" %}
{% include "partials/projects.njk" %}
{% include "partials/partners.njk" %}
{% include "partials/djangokam.njk" %}
{% include "partials/mission.njk" %}
{% include "partials/hosting.njk" %}
{% include "partials/clients.njk" %}
{% include "partials/fediverse.njk" %}
{% include "partials/faq.njk" %}
{% include "partials/engagement.njk" %}
{% include "partials/contact.njk" %}

Before

Width:  |  Height:  |  Size: 341 KiB

After

Width:  |  Height:  |  Size: 341 KiB

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

+110
View File
@@ -0,0 +1,110 @@
<script lang="ts">
import BrandIcon from '$lib/components/icons/BrandIcon.svelte';
import type { Bundle, Locale } from '$lib/i18n';
let { t, locale }: { t: Bundle; locale: Locale } = $props();
const year = new Date().getFullYear();
const legalHref = $derived(locale === 'en' ? '/en/legal-notice/' : '/mentions-legales/');
</script>
<footer>
<div class="flag-bar" aria-hidden="true"></div>
<div class="footer-content container">
<p class="footer-name">{t.site.name}</p>
<p class="footer-meta">© {year} {t.site.name} | {t.site.tagline}</p>
<p class="footer-legal">
<a href={legalHref}>{t.footer.legal_link}</a>
</p>
<div class="footer-socials">
<a href="mailto:kontak@o-k-i.net" title="Email" class="social-link" aria-label="Email">
<BrandIcon name="email" />
</a>
<a
href="https://wa.me/262692229740"
target="_blank"
rel="noopener noreferrer"
title="WhatsApp"
class="social-link"
aria-label="WhatsApp"
>
<BrandIcon name="whatsapp" />
</a>
<a
href="https://discord.gg/DPvUCA8dD"
target="_blank"
rel="noopener noreferrer"
title="Discord"
class="social-link"
aria-label="Discord"
>
<BrandIcon name="discord" />
</a>
<a
href="https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/o-k-i.net"
target="_blank"
rel="noopener noreferrer"
title="Code source sur Labola (Gitea)"
class="social-link"
aria-label="Code source sur Labola (Gitea)"
>
<BrandIcon name="gitea" />
</a>
</div>
</div>
</footer>
<style>
footer {
background: var(--noir-profond);
border-top: 1px solid var(--line);
}
.footer-content {
padding: var(--space-4) var(--space-4);
text-align: center;
display: flex;
flex-direction: column;
gap: var(--space-1);
align-items: center;
}
.footer-name {
font-family: var(--font-display);
font-weight: 700;
text-transform: uppercase;
letter-spacing: -0.01em;
color: var(--or-oki);
}
.footer-meta {
color: var(--muted);
font-size: 0.9rem;
}
.footer-legal a {
font-size: 0.9rem;
}
.footer-socials {
display: flex;
gap: var(--space-2);
margin-top: var(--space-1);
}
.social-link {
color: var(--muted);
display: inline-flex;
transition: color var(--dur-tanbou) var(--ease-ka), transform var(--dur-tanbou) var(--ease-ka);
}
.social-link:hover {
color: var(--or-oki);
transform: translateY(-2px);
}
.social-link :global(svg) {
width: 1.4rem;
height: 1.4rem;
}
</style>
+381
View File
@@ -0,0 +1,381 @@
<script lang="ts">
import { page } from '$app/state';
import { alternatePath, type Bundle, type Locale } from '$lib/i18n';
let { t, locale }: { t: Bundle; locale: Locale } = $props();
let menuOpen = $state(false);
let openDropdown = $state<string | null>(null);
const altLocale: Locale = $derived(locale === 'fr' ? 'en' : 'fr');
const altHref = $derived(alternatePath(page.url.pathname, altLocale));
function rememberLangChoice() {
localStorage.setItem('oki-lang-pref', altLocale);
}
function toggleTheme() {
const isLight = document.documentElement.classList.toggle('light-theme');
localStorage.setItem('oki-theme', isLight ? 'light' : 'dark');
}
function toggleDropdown(text: string) {
openDropdown = openDropdown === text ? null : text;
}
function onKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
openDropdown = null;
menuOpen = false;
}
}
function closeMenu() {
menuOpen = false;
openDropdown = null;
}
</script>
<svelte:window onkeydown={onKeydown} />
<nav aria-label={t.ui?.nav_label ?? 'Navigation principale'}>
<div class="nav-container">
<a href={locale === 'en' ? '/en/' : '/'} class="logo" onclick={closeMenu}>
<img src="/images/logo-512x512.png" alt="{t.site.shortName} Logo" class="logo-image" width="40" height="40" />
<span>{t.site.shortName}</span>
</a>
<ul class="nav-links" id="nav-links" class:mobile-open={menuOpen}>
{#each t.navigation as item (item.text)}
{#if item.children}
<li class="has-dropdown" class:open={openDropdown === item.text}>
<button
type="button"
class="nav-dropdown-trigger"
aria-haspopup="true"
aria-expanded={openDropdown === item.text}
onclick={() => toggleDropdown(item.text)}
>
{item.text} <span class="dropdown-caret" aria-hidden="true"></span>
</button>
<ul class="nav-dropdown">
{#each item.children as child (child.text)}
<li>
<a
href={child.url}
target={child.external ? '_blank' : undefined}
rel={child.external ? 'noopener noreferrer' : undefined}
onclick={closeMenu}>{child.text}</a
>
</li>
{/each}
</ul>
</li>
{:else}
<li>
<a href={item.url} onclick={closeMenu}>{item.text}</a>
</li>
{/if}
{/each}
</ul>
<a href={altHref} class="lang-switch" hreflang={altLocale} onclick={rememberLangChoice}>
{t.ui.lang_switch_label}
</a>
<button
type="button"
class="theme-toggle"
aria-label={t.ui.theme_toggle}
title={t.ui.theme_toggle}
onclick={toggleTheme}
>
<svg class="theme-icon theme-icon-dark" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 14.5A8.5 8.5 0 0 1 9.5 4 8.5 8.5 0 1 0 20 14.5Z" />
</svg>
<svg class="theme-icon theme-icon-light" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="4.5" />
<path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.5 4.5l2 2M17.5 17.5l2 2M19.5 4.5l-2 2M6.5 17.5l-2 2" />
</svg>
</button>
<button
type="button"
class="menu-toggle"
class:open={menuOpen}
aria-expanded={menuOpen}
aria-controls="nav-links"
aria-label="Menu"
onclick={() => (menuOpen = !menuOpen)}
>
<span></span>
<span></span>
<span></span>
</button>
</div>
<div class="flag-bar" aria-hidden="true"></div>
</nav>
<style>
nav {
position: fixed;
top: 0;
width: 100%;
padding: 1rem 2rem 0.85rem;
background: var(--noir-oki);
z-index: 1000;
transition: background var(--dur-mesure) var(--ease-ka);
}
nav .flag-bar {
position: absolute;
bottom: 0;
left: 0;
}
:global(html.light-theme) nav {
box-shadow: 0 1px 0 var(--line);
}
.nav-container {
max-width: var(--container);
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.logo {
display: flex;
align-items: center;
gap: 0.6rem;
font-family: var(--font-display);
font-size: 1.4rem;
font-weight: 700;
color: var(--or-oki);
letter-spacing: 0.02em;
}
.logo-image {
height: 2.5rem;
width: auto;
background: #fff;
padding: 0.25rem;
border-radius: var(--radius-md);
}
.nav-links {
display: flex;
gap: 2rem;
list-style: none;
}
.nav-links a {
color: var(--blanc-creme);
font-weight: 500;
font-size: 0.95rem;
}
.nav-links a:hover {
color: var(--or-oki);
}
.lang-switch {
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.5rem 0.7rem;
color: var(--blanc-creme);
font-weight: 700;
font-size: 0.8rem;
letter-spacing: 0.04em;
transition:
border-color var(--dur-tanbou) var(--ease-ka),
color var(--dur-tanbou) var(--ease-ka);
}
.lang-switch:hover {
border-color: var(--or-oki);
color: var(--or-oki);
}
.theme-toggle {
background: transparent;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.5rem 0.7rem;
cursor: pointer;
display: flex;
align-items: center;
color: var(--blanc-creme);
transition: border-color var(--dur-tanbou) var(--ease-ka);
}
.theme-toggle:hover {
border-color: var(--or-oki);
}
.theme-icon {
width: 1.1rem;
height: 1.1rem;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: square;
}
.theme-icon-light {
display: none;
}
:global(html.light-theme) .theme-icon-dark {
display: none;
}
:global(html.light-theme) .theme-icon-light {
display: block;
}
.menu-toggle {
display: none;
flex-direction: column;
gap: 5px;
background: none;
border: none;
cursor: pointer;
padding: 0.25rem;
}
.menu-toggle span {
width: 24px;
height: 2px;
background: var(--or-oki);
transition: transform var(--dur-tanbou) var(--ease-ka), opacity var(--dur-tanbou) var(--ease-ka);
}
.has-dropdown {
position: relative;
}
.nav-dropdown-trigger {
display: flex;
align-items: center;
gap: 0.3rem;
background: none;
border: none;
padding: 0;
font-family: inherit;
font-weight: 500;
font-size: 0.95rem;
color: var(--blanc-creme);
cursor: pointer;
}
.nav-dropdown-trigger:hover {
color: var(--or-oki);
}
.dropdown-caret {
font-size: 0.7em;
transition: transform var(--dur-tanbou) var(--ease-ka);
}
.nav-dropdown {
position: absolute;
top: 100%;
left: 0;
margin-top: 1rem;
min-width: 200px;
list-style: none;
background: var(--noir-oki);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.4rem 0;
opacity: 0;
visibility: hidden;
transform: translateY(-6px);
transition:
opacity var(--dur-tanbou) var(--ease-ka),
transform var(--dur-tanbou) var(--ease-ka),
visibility var(--dur-tanbou);
}
.has-dropdown:hover .nav-dropdown,
.has-dropdown.open .nav-dropdown,
.nav-dropdown-trigger:focus-visible + .nav-dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.has-dropdown:hover .dropdown-caret,
.has-dropdown.open .dropdown-caret {
transform: rotate(180deg);
}
.nav-dropdown a {
display: block;
padding: 0.55rem 1.1rem;
white-space: nowrap;
font-size: 0.9rem;
}
@media (max-width: 1100px) {
.nav-links {
display: none;
}
.nav-links.mobile-open {
display: flex;
position: absolute;
top: 100%;
left: 0;
right: 0;
flex-direction: column;
padding: 1rem;
border-top: 2px solid var(--or-oki);
background: var(--noir-oki);
}
.menu-toggle {
display: flex;
}
.menu-toggle.open span:nth-child(1) {
transform: rotate(45deg) translateY(8px);
}
.menu-toggle.open span:nth-child(2) {
opacity: 0;
}
.menu-toggle.open span:nth-child(3) {
transform: rotate(-45deg) translateY(-8px);
}
.nav-dropdown-trigger {
pointer-events: none;
opacity: 0.7;
}
.dropdown-caret {
display: none;
}
.nav-dropdown {
position: static;
opacity: 1;
visibility: visible;
transform: none;
margin: 0.25rem 0 0.5rem 1rem;
border: none;
background: none;
padding: 0;
}
.nav-dropdown a {
padding: 0.4rem 0;
font-size: 0.9rem;
}
}
</style>
+33
View File
@@ -0,0 +1,33 @@
<script lang="ts">
interface Picture {
sources: Record<string, string>;
img: { src: string; w: number; h: number };
}
interface Props {
/** Objet issu d'un import `?format=avif;webp;png&w=…&as=picture`. */
picture: Picture;
alt: string;
/** lazy par défaut ; eager uniquement pour le LCP. */
loading?: 'lazy' | 'eager';
class?: string;
sizes?: string;
}
let { picture, alt, loading = 'lazy', class: className = '', sizes }: Props = $props();
</script>
<picture>
{#each Object.entries(picture.sources) as [format, srcset] (format)}
<source {srcset} type="image/{format}" {sizes} />
{/each}
<img
src={picture.img.src}
{alt}
width={picture.img.w}
height={picture.img.h}
{loading}
decoding="async"
class={className}
/>
</picture>
+102
View File
@@ -0,0 +1,102 @@
<script lang="ts">
import { getBundle, alternatePath, type Locale } from '$lib/i18n';
interface Props {
/** Titre de page (sans le nom du site). Absent = home. */
title?: string;
description?: string;
/** Chemin canonique courant, ex. `/dons/`. */
path: string;
locale: Locale;
}
let { title, description, path, locale }: Props = $props();
const site = $derived(getBundle(locale).site);
const altLocale: Locale = $derived(locale === 'fr' ? 'en' : 'fr');
const altPath = $derived(alternatePath(path, altLocale));
const fullTitle = $derived(title ? `${title} | ${site.name}` : `${site.name} | ${site.shortName}`);
const ogTitle = $derived(title ?? `${site.name} - ${site.og_tagline}`);
const desc = $derived(description ?? site.description);
const canonical = $derived(`https://o-k-i.net${path}`);
const orgJsonLd = $derived(
JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Organization',
name: site.name,
alternateName: site.shortName,
url: 'https://o-k-i.net',
logo: 'https://o-k-i.net/images/logo-512x512.png',
description: site.description,
address: {
'@type': 'PostalAddress',
streetAddress: '36 Rue Lethière',
addressLocality: 'Basse-Terre',
postalCode: '97100',
addressCountry: 'GP'
},
contactPoint: {
'@type': 'ContactPoint',
email: 'kontak@o-k-i.net',
contactType: 'customer service'
},
sameAs: [
'https://niyaj.o-k-i.net/',
'https://kute.o-k-i.net/',
'https://labola.o-k-i.net/',
'https://liberapay.com/OKI/donate'
],
foundingDate: '2024',
nonprofitStatus: 'NonprofitANBI'
})
);
const djangokamJsonLd = JSON.stringify({
'@context': 'https://schema.org',
'@type': 'MusicGroup',
name: 'DJANGOKAM',
genre: ['Reggae', 'Zouk', 'Konpa', 'K-pop'],
url: 'https://djangokam.pawol.nu',
sameAs: ['https://djangokam.pawol.nu'],
member: {
'@type': 'Organization',
name: 'ORGANISATION KA INTERNATIONALE',
url: 'https://o-k-i.net'
}
});
</script>
<svelte:head>
<title>{fullTitle}</title>
<meta name="description" content={desc} />
<meta name="keywords" content={site.keywords} />
<meta name="author" content={site.name} />
<link rel="canonical" href={canonical} />
<link rel="alternate" hreflang={locale} href={canonical} />
<link rel="alternate" hreflang={altLocale} href={`https://o-k-i.net${altPath}`} />
<link rel="alternate" hreflang="x-default" href="https://o-k-i.net/" />
<meta property="og:type" content="website" />
<meta property="og:url" content={canonical} />
<meta property="og:title" content={ogTitle} />
<meta property="og:description" content={desc} />
<meta property="og:image" content="https://o-k-i.net/images/logo-512x512.png" />
<meta property="og:image:width" content="512" />
<meta property="og:image:height" content="512" />
<meta property="og:image:alt" content="Logo {site.name}" />
<meta property="og:locale" content={site.og_locale} />
<meta property="og:site_name" content={site.name} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content={canonical} />
<meta name="twitter:title" content={ogTitle} />
<meta name="twitter:description" content={desc} />
<meta name="twitter:image" content="https://o-k-i.net/images/logo-512x512.png" />
<meta name="twitter:image:alt" content="Logo {site.name}" />
{@html `<script type="application/ld+json">${orgJsonLd}</script>`}
{@html `<script type="application/ld+json">${djangokamJsonLd}</script>`}
</svelte:head>
+185
View File
@@ -0,0 +1,185 @@
<script lang="ts">
interface BrandPath {
d: string;
fillRule?: 'evenodd';
}
interface BrandIconDef {
viewBox: string;
/** Transforms `<g>` hérités des fichiers legacy (du plus externe au plus interne). */
groups?: string[];
paths: BrandPath[];
}
/**
* Logos officiels repris EXACTEMENT de `legacy/src/_includes/icons/*.svg`
* (viewBox et attributs `d` inchangés).
*
* Les logos qui utilisaient un `<mask>` (castopod, mastodon) sont portés en
* `fill-rule="evenodd"` : le `d` du masque est concaténé au `d` principal,
* rendu strictement identique sans dépendre d'ids de masque.
*/
const ICONS: Record<string, BrandIconDef> = {
castopod: {
viewBox: '0 86.4 512 339.2',
paths: [
{
d: 'M477.7 425.6h-89.3s-3.3-6.8-6.9-13.4-12.1-6.3-12.1-6.3H142s-8.2-1.1-12.4 6.3c-2.6 4.3-5 8.8-7.1 13.4H34.6C15.7 425.6 0 410.3 0 391.4V120.9C0 102 15.4 86.4 34.3 86.4h443.1c18.9 0 34.6 15.3 34.6 34.2v270.5c.3 18.9-15.4 34.5-34.3 34.5 M372.7 159.7H139c-40.1 0-72.8 32.8-72.8 72.8S98.9 305 139 305h233.8c40.1 0 72.8-32.5 72.8-72.5.4-39.8-31.7-72.4-71.5-72.8zm-207.1 93.6s-10.7-7.9-25.8-7.9c-11.3 0-24.4 6.8-24.4 6.8-3.7-5.3-5.7-11.6-5.8-18.1 0-17.3 14-31.2 31.3-31.2s31.3 13.9 31.3 31.2c0 7.4-2.4 14-6.6 19.2m90.4 18c-34.3 0-33-26.3-33-26.3-.5-4.9 5.8-6.6 8-3.6 1.1 1.6 1.1 1.6 1.9 4.6 4.7 16.1 23.1 15.3 23.1 15.3s18.4 1.1 23.1-15.3c.8-2.7.8-3 1.9-4.6 2.2-2.7 8.5-1.4 8 3.6 0 .1 1.3 26.3-33 26.3m140.4-18.9s-13.2-6.8-24.5-6.8c-15.1 0-25.8 7.9-25.8 7.9-4.3-5.4-6.6-12.2-6.6-19.1 0-17.3 14-31.2 31.3-31.2s31.3 13.9 31.3 31.2c.2 6.5-1.8 12.9-5.7 18 M107.4 129.1c-17.3-4.6-32.1 5.7-40.4 20-1.9 3.3-.6 6 1.4 7.1 2.8.8 4.7.3 7.7-4.1 6-10.7 16.2-16.7 27.7-14.5 0 0 8 2.5 9.1-2.7.8-3.4-2.2-5-5.5-5.8M417.2 140c-.3 5.2 8 5.2 8 5.2 11.5 1.1 18.7 6.3 21.4 18.3 1.6 5.2 3.3 6 6.3 6 2.2-.3 4.4-2.7 3.3-6.6-3.8-15.9-14-26.8-32.1-27.1-3.3-.2-6.6.7-6.9 4.2',
fillRule: 'evenodd'
},
{
d: 'M371.1 203.2c-17.3 0-31.3 13.9-31.3 31.2 0 7.1 2.5 13.7 6.6 19.1 0 0 10.7-7.9 25.8-7.9 11.3 0 24.4 6.8 24.4 6.8 3.6-5.2 5.8-11.2 5.8-18.1 0-17.2-14-31.1-31.3-31.1m-230.2 0c-17.3 0-31.3 13.9-31.3 31.2 0 6.8 2.2 12.9 5.8 18.1 0 0 13.2-6.8 24.4-6.8 15.1 0 25.8 7.9 25.8 7.9 4.1-5.2 6.6-11.8 6.6-19.1 0-17.4-14-31.3-31.3-31.3'
}
]
},
discord: {
viewBox: '0 0 126.644 96',
paths: [
{
d: 'M81.15,0c-1.2376,2.1973-2.3489,4.4704-3.3591,6.794-9.5975-1.4396-19.3718-1.4396-28.9945,0-.985-2.3236-2.1216-4.5967-3.3591-6.794-9.0166,1.5407-17.8059,4.2431-26.1405,8.0568C2.779,32.5304-1.6914,56.3725.5312,79.8863c9.6732,7.1476,20.5083,12.603,32.0505,16.0884,2.6014-3.4854,4.8998-7.1981,6.8698-11.0623-3.738-1.3891-7.3497-3.1318-10.8098-5.1523.9092-.6567,1.7932-1.3386,2.6519-1.9953,20.281,9.547,43.7696,9.547,64.0758,0,.8587.7072,1.7427,1.3891,2.6519,1.9953-3.4601,2.0457-7.0718,3.7632-10.835,5.1776,1.97,3.8642,4.2683,7.5769,6.8698,11.0623,11.5419-3.4854,22.3769-8.9156,32.0509-16.0631,2.626-27.2771-4.496-50.9172-18.817-71.8548C98.9811,4.2684,90.1918,1.5659,81.1752.0505l-.0252-.0505ZM42.2802,65.4144c-6.2383,0-11.4159-5.6575-11.4159-12.6535s4.9755-12.6788,11.3907-12.6788,11.5169,5.708,11.4159,12.6788c-.101,6.9708-5.026,12.6535-11.3907,12.6535ZM84.3576,65.4144c-6.2637,0-11.3907-5.6575-11.3907-12.6535s4.9755-12.6788,11.3907-12.6788,11.4917,5.708,11.3906,12.6788c-.101,6.9708-5.026,12.6535-11.3906,12.6535Z'
}
]
},
email: {
viewBox: '0 0 24 24',
paths: [
{
d: 'M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z'
}
]
},
gitea: {
viewBox: '5.67 143.05 628.65 387.55',
paths: [
{
d: 'M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z'
},
{
d: 'M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8C343.2,346.5,335,363.3,326.8,380.1z'
}
]
},
mastodon: {
viewBox: '0 0 75 79',
paths: [
{
d: 'M73.8393 17.4898C72.6973 9.00165 65.2994 2.31235 56.5296 1.01614C55.05 0.797115 49.4441 0 36.4582 0H36.3612C23.3717 0 20.585 0.797115 19.1054 1.01614C10.5798 2.27644 2.79399 8.28712 0.904997 16.8758C-0.00358524 21.1056 -0.100549 25.7949 0.0682394 30.0965C0.308852 36.2651 0.355538 42.423 0.91577 48.5665C1.30307 52.6474 1.97872 56.6957 2.93763 60.6812C4.73325 68.042 12.0019 74.1676 19.1233 76.6666C26.7478 79.2728 34.9474 79.7055 42.8039 77.9162C43.6682 77.7151 44.5217 77.4817 45.3645 77.216C47.275 76.6092 49.5123 75.9305 51.1571 74.7385C51.1797 74.7217 51.1982 74.7001 51.2112 74.6753C51.2243 74.6504 51.2316 74.6229 51.2325 74.5948V68.6416C51.2321 68.6154 51.2259 68.5896 51.2142 68.5661C51.2025 68.5426 51.1858 68.522 51.1651 68.5058C51.1444 68.4896 51.1204 68.4783 51.0948 68.4726C51.0692 68.4669 51.0426 68.467 51.0171 68.4729C45.9835 69.675 40.8254 70.2777 35.6502 70.2682C26.7439 70.2682 24.3486 66.042 23.6626 64.2826C23.1113 62.762 22.7612 61.1759 22.6212 59.5646C22.6197 59.5375 22.6247 59.5105 22.6357 59.4857C22.6466 59.4609 22.6633 59.4391 22.6843 59.422C22.7053 59.4048 22.73 59.3929 22.7565 59.3871C22.783 59.3813 22.8104 59.3818 22.8367 59.3886C27.7864 60.5826 32.8604 61.1853 37.9522 61.1839C39.1768 61.1839 40.3978 61.1839 41.6224 61.1516C46.7435 61.008 52.1411 60.7459 57.1796 59.7621C57.3053 59.7369 57.431 59.7154 57.5387 59.6831C65.4861 58.157 73.0493 53.3672 73.8178 41.2381C73.8465 40.7606 73.9184 36.2364 73.9184 35.7409C73.9219 34.0569 74.4606 23.7949 73.8393 17.4898Z M61.2484 27.0263V48.114H52.8916V27.6475C52.8916 23.3388 51.096 21.1413 47.4437 21.1413C43.4287 21.1413 41.4177 23.7409 41.4177 28.8755V40.0782H33.1111V28.8755C33.1111 23.7409 31.0965 21.1413 27.0815 21.1413C23.4507 21.1413 21.6371 23.3388 21.6371 27.6475V48.114H13.2839V27.0263C13.2839 22.7176 14.384 19.2946 16.5843 16.7572C18.8539 14.2258 21.8311 12.926 25.5264 12.926C29.8036 12.926 33.0357 14.5705 35.1905 17.8559L37.2698 21.346L39.3527 17.8559C41.5074 14.5705 44.7395 12.926 49.0095 12.926C52.7013 12.926 55.6784 14.2258 57.9553 16.7572C60.1531 19.2922 61.2508 22.7152 61.2484 27.0263Z',
fillRule: 'evenodd'
}
]
},
nextcloud: {
viewBox: '14 34.5 122.5 57',
paths: [
{
d: 'M75.091,35.685c-12.438,0-22.981,8.433-26.248,19.857-2.84-6.06-8.994-10.305-16.082-10.305-9.748,0-17.761,8.013-17.761,17.761s8.013,17.765,17.761,17.765c7.088,0,13.242-4.248,16.082-10.309,3.268,11.426,13.81,19.861,26.248,19.861,12.346,0,22.835-8.308,26.186-19.605,2.892,5.924,8.97,10.053,15.958,10.053,9.748,0,17.765-8.017,17.765-17.765s-8.017-17.761-17.765-17.761c-6.988,0-13.065,4.127-15.958,10.05-3.352-11.296-13.84-19.601-26.186-19.601h0ZM75.091,46.11c9.39,0,16.89,7.497,16.89,16.887s-7.501,16.89-16.89,16.89-16.887-7.501-16.887-16.89,7.497-16.887,16.887-16.887ZM32.762,55.663c4.114,0,7.338,3.221,7.338,7.335s-3.225,7.338-7.338,7.338-7.335-3.225-7.335-7.338,3.221-7.335,7.335-7.335ZM117.234,55.663c4.114,0,7.338,3.221,7.338,7.335s-3.225,7.338-7.338,7.338-7.335-3.225-7.335-7.338,3.221-7.335,7.335-7.335Z'
}
]
},
peertube: {
viewBox: '2799 -911 512 682.688',
paths: [
{ d: 'm2799-911v341.344l256-170.656' },
{ d: 'm2799-569.656v341.344l256-170.656' },
{ d: 'm3055-740.344v341.344l256-170.656' }
]
},
stoat: {
viewBox: '0 0 307 307',
groups: [
'matrix(1,0,0,1,-757,-1483)',
'matrix(1.432974,0,0,1.80298,-288.929017,-864.20063)',
'matrix(0.629919,0,0,0.500647,539.407527,1110.972982)'
],
paths: [
{
d: 'M356.969,675.637C323.753,644.675 302.968,600.546 302.968,551.6C302.968,458.019 378.944,382.044 472.524,382.044C566.105,382.044 642.08,458.019 642.08,551.6C642.08,629.48 589.46,695.166 517.864,715.015C501.974,691.667 487.572,670.399 498.34,635.393C485.202,642.099 476.944,655.288 474.445,669.626C469.556,661.943 464.129,654.533 457.841,647.931C441.837,631.125 420.016,619.373 409.919,597.549C405.306,587.241 402.145,572.504 405.799,561.838C408.436,582.54 418.507,601.587 436.545,612.617C450.296,621.023 465.703,622.408 481.126,625.661C497.203,629.048 522.738,636.688 538.403,633.928C551.425,631.638 578.749,617.255 578.02,601.656C577.765,596.289 571.213,587.245 568.79,582.066C565.566,575.171 567.48,573.682 567.848,566.765C568.885,547.188 563.715,537.516 554.206,521.395C542.674,501.84 530.016,490.024 508.095,482.087C488.062,474.831 457.231,478.938 456.996,479.271C461.046,483.711 464.773,488.906 465.295,495.055C457.084,487.46 449.14,479.387 439.945,472.948C419.607,458.71 406.868,460.052 398.782,484.817C394.568,497.72 391.382,515.255 395.288,528.453C397.657,536.447 402.575,542.952 408.175,548.951C397.217,547.458 390.574,537.94 388.158,527.623C360.997,586.373 354.95,614.781 356.969,675.637ZM558.455,619.379L558.452,619.376C554.235,619.505 550.344,617.264 547.164,614.697C545.37,613.176 542.74,611.209 542.372,608.931C541.643,604.396 545.254,601.153 547.406,599.808C549.251,598.658 551.495,598.017 553.613,597.527C558.442,596.411 563.455,595.377 568.319,596.323C569.774,596.606 571.22,597.077 572.417,597.951C573.614,598.821 574.544,600.138 574.705,601.612C574.824,602.705 574.525,603.796 574.164,604.833C573.322,607.217 572.16,609.476 570.71,611.548C569.274,613.613 567.556,615.508 565.485,616.934C563.414,618.361 560.969,619.304 558.455,619.379ZM486.399,552.769C503.076,550.51 505.109,576.334 489.02,577.418C472.931,578.502 471.118,554.84 486.399,552.769ZM430.213,501.5C431.753,505.171 434.926,507.616 437.667,510.309C431.445,512.197 425.166,510.394 419.704,507.264C420.028,513.033 424.299,518.121 427.498,522.174C422.225,523.384 417.564,521.577 413.944,517.769C413.891,523.13 415.717,527.806 418.347,532.34C408.062,537.456 403.622,526.696 403.097,518.275C402.522,509.02 406.267,485.669 412.907,478.941C420.336,471.412 431.875,480.182 438.166,485.238C443.719,489.698 449.061,495.087 454.092,500.111C447.487,504.476 437.648,504.294 430.213,501.5ZM559.47,568.921C555.834,562.599 554.722,560.76 552.607,556.182C551.168,553.064 550.439,549.366 555.655,549.256C565.117,549.055 568.395,564.041 561.026,569.27C560.844,569.397 560.628,569.465 560.407,569.465C560.02,569.465 559.662,569.257 559.47,568.921ZM553.254,508.59C555.938,471.688 555.624,447.806 521.4,481.304C534.318,487.312 545.181,496.897 553.254,508.59Z',
fillRule: 'evenodd'
}
]
},
telegram: {
viewBox: '181 296 556 462',
paths: [
{
d: 'M226.328419,494.722069 C372.088573,431.216685 469.284839,389.350049 517.917216,369.122161 C656.772535,311.36743 685.625481,301.334815 704.431427,301.003532 C708.567621,300.93067 717.815839,301.955743 723.806446,306.816707 C728.864797,310.92121 730.256552,316.46581 730.922551,320.357329 C731.588551,324.248848 732.417879,333.113828 731.758626,340.040666 C724.234007,419.102486 691.675104,610.964674 675.110982,699.515267 C668.10208,736.984342 654.301336,749.547532 640.940618,750.777006 C611.904684,753.448938 589.856115,731.588035 561.733393,713.153237 C517.726886,684.306416 492.866009,666.349181 450.150074,638.200013 C400.78442,605.66878 432.786119,587.789048 460.919462,558.568563 C468.282091,550.921423 596.21508,434.556479 598.691227,424.000355 C599.00091,422.680135 599.288312,417.758981 596.36474,415.160431 C593.441168,412.561881 589.126229,413.450484 586.012448,414.157198 C581.598758,415.158943 511.297793,461.625274 375.109553,553.556189 C355.154858,567.258623 337.080515,573.934908 320.886524,573.585046 C303.033948,573.199351 268.692754,563.490928 243.163606,555.192408 C211.851067,545.013936 186.964484,539.632504 189.131547,522.346309 C190.260287,513.342589 202.659244,504.134509 226.328419,494.722069 Z'
}
]
},
tiktok: {
viewBox: '15.6 11.4 17.1 19',
paths: [
{
d: 'M25.5172 25.0575C25.4647 26.4295 24.3093 27.5306 22.8929 27.5306C22.5691 27.5306 22.2591 27.4729 21.9727 27.3675C22.2591 27.4729 22.5692 27.5306 22.893 27.5306C24.3094 27.5306 25.4648 26.4295 25.5174 25.0576L25.5224 12.8063H27.8124C28.0331 13.971 28.7405 14.9704 29.7205 15.5948C29.7208 15.5952 29.7212 15.5956 29.7215 15.596C30.4037 16.0305 31.2171 16.284 32.0903 16.284V16.9645C32.0903 16.9645 32.0903 16.9645 32.0904 16.9646V19.3405C30.4684 19.3405 28.9654 18.8334 27.7386 17.9728V24.1852C27.7386 27.2878 25.1559 29.8119 21.9812 29.8119C20.7545 29.8119 19.6169 29.4338 18.6818 28.7921C18.6813 28.7916 18.6808 28.791 18.6802 28.7905C17.1964 27.7717 16.224 26.0876 16.224 24.1846C16.224 21.0821 18.8067 18.5579 21.9814 18.5579C22.2448 18.5579 22.503 18.5791 22.7572 18.6128V19.337C19.861 19.4031 17.4865 21.5682 17.1713 24.3333C17.4868 21.5685 19.8611 19.4037 22.7571 19.3376V21.7342C22.5115 21.6591 22.2519 21.6154 21.9812 21.6154C20.5314 21.6154 19.3519 22.7683 19.3519 24.1852C19.3519 25.1719 19.9247 26.0292 20.7616 26.4596C20.7616 26.4597 20.7616 26.4597 20.7616 26.4597C21.1265 26.6474 21.541 26.7549 21.9811 26.7549C23.3975 26.7549 24.5529 25.6538 24.6055 24.2819L24.6105 12.0305H27.7385C27.7385 12.2955 27.7646 12.5545 27.8123 12.8063H25.5223L25.5172 25.0575Z',
fillRule: 'evenodd'
}
]
},
whatsapp: {
viewBox: '0 0 720 720',
paths: [
{
d: 'M360,0C161.18,0,0,161.18,0,360c0,65.41,17.45,126.75,47.94,179.61L0,720l187.02-44.21c51.34,28.18,110.28,44.21,172.98,44.21,198.82,0,360-161.18,360-360S558.82,0,360,0ZM360,655.52c-60.17,0-116.13-17.98-162.82-48.87l-110.49,28.14,30.99-105.61c-33.53-47.93-53.2-106.26-53.2-169.19,0-163.21,132.31-295.52,295.52-295.52s295.52,132.31,295.52,295.52-132.31,295.52-295.52,295.52Z'
},
{
d: 'M444.35,407.52l87.1,41.06c4,1.88,6.56,5.94,6.2,10.34-.94,11.46-5.54,34.43-26.13,55.02-58.12,58.12-162.49-7.64-166.74-10.18-25.67-13.79-50.06-32.24-73.19-55.36-23.12-23.12-41.58-47.52-55.37-73.19-2.55-4.24-68.31-108.61-10.18-166.74,20.59-20.59,43.56-25.19,55.02-26.13,4.41-.36,8.46,2.2,10.34,6.2l41.07,87.1c1.94,4.12,1.09,9.02-2.13,12.24l-30.61,30.61c-6.62,6.62-8.56,16.93-4,25.11,11.17,20.03,26.19,39.32,43.59,57.07,17.75,17.4,37.04,32.43,57.07,43.59,8.18,4.56,18.48,2.62,25.11-4l30.61-30.61c3.22-3.22,8.12-4.08,12.24-2.13Z'
}
]
},
x: {
viewBox: '0 0 1200 1227',
paths: [
{
d: 'M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z'
}
]
}
};
interface Props {
/** Nom de la marque : castopod, discord, email, gitea, mastodon, nextcloud, peertube, stoat, telegram, tiktok, whatsapp, x. */
name: string;
class?: string;
}
let { name, class: className = '' }: Props = $props();
const def = $derived(ICONS[name]);
$effect(() => {
if (!def && import.meta.env.DEV) {
console.warn(`[BrandIcon] Icône de marque inconnue : « ${name} »`);
}
});
</script>
{#snippet renderPaths(iconDef: BrandIconDef, depth: number)}
{#if iconDef.groups && depth < iconDef.groups.length}
<g transform={iconDef.groups[depth]}>
{@render renderPaths(iconDef, depth + 1)}
</g>
{:else}
{#each iconDef.paths as p (p.d)}
<path d={p.d} fill-rule={p.fillRule ?? 'nonzero'} />
{/each}
{/if}
{/snippet}
{#if def}
<svg
viewBox={def.viewBox}
fill="currentColor"
aria-hidden="true"
class={className}
xmlns="http://www.w3.org/2000/svg"
>
{@render renderPaths(def, 0)}
</svg>
{/if}
<style>
svg {
width: 1em;
height: 1em;
}
</style>
@@ -0,0 +1,122 @@
<script lang="ts">
import { onMount } from 'svelte';
import { prefersReducedMotion, syncopatedDelay } from '$lib/motion/tokens';
interface Props {
/** Chaîne intacte — restituée via aria-label. */
text: string;
/** Balise du titre (h1-h4). */
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'span';
class?: string;
id?: string;
}
let { text, as = 'h2', class: className = '', id }: Props = $props();
let element: HTMLElement;
/**
* Split par mots (jamais par caractères — langues à apostrophes).
* Sur un titre (h1-h4) : aria-label avec la chaîne intacte + mots
* aria-hidden. Sur span/p : aria-label est prohibé — les mots
* restent lisibles naturellement par les lecteurs d'écran.
*/
const words = $derived(text.split(/\s+/).filter(Boolean));
const named = $derived(['h1', 'h2', 'h3', 'h4'].includes(as));
function rangeFor(index: number): { start: number; end: number; delay: number } {
const delay = syncopatedDelay(index);
const start = Math.min(5 + (delay / 960) * 25, 55);
return { start, end: start + 30, delay };
}
onMount(() => {
// Fallback GSAP ScrollTrigger uniquement si animation-timeline: view() est absent
if (prefersReducedMotion()) return;
if (CSS.supports('animation-timeline: view()')) return;
let ctx: { revert(): void } | undefined;
let killed = false;
(async () => {
const [{ gsap }, { ScrollTrigger }] = await Promise.all([
import('gsap'),
import('gsap/ScrollTrigger')
]);
if (killed) return;
gsap.registerPlugin(ScrollTrigger);
const targets = element.querySelectorAll('.kt-word');
gsap.set(targets, { opacity: 0, yPercent: 60 });
ctx = gsap.context(() => {
targets.forEach((target, index) => {
gsap.to(target, {
opacity: 1,
yPercent: 0,
duration: 0.48,
ease: 'power3.out',
delay: syncopatedDelay(index) / 1000,
scrollTrigger: { trigger: element, start: 'top 85%', once: true }
});
});
}, element);
})();
return () => {
killed = true;
ctx?.revert();
};
});
</script>
<svelte:element
this={as}
{id}
class="kinetic-text {className}"
aria-label={named ? text : undefined}
bind:this={element}
>
{#each words as word, i (i)}
{@const range = rangeFor(i)}
<span
class="kt-word"
aria-hidden={named ? true : undefined}
style="--kt-start: {range.start}%; --kt-end: {range.end}%; --kt-delay: {range.delay}ms"
>{word}{#if i < words.length - 1}&nbsp;{/if}</span
>
{/each}
</svelte:element>
<style>
.kinetic-text {
margin: 0;
}
.kt-word {
display: inline-block;
will-change: transform, opacity;
}
/* Chemin natif : scrub via animation-timeline: view() (playbook §5.2).
L'état caché initial n'existe QUE dans ce bloc @supports :
sans JS ni support, le contenu reste intégralement visible. */
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
.kt-word {
animation: kt-rise linear both;
animation-timeline: view();
animation-range: entry var(--kt-start) entry var(--kt-end);
}
}
}
@keyframes kt-rise {
from {
opacity: 0;
transform: translateY(60%);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
@@ -0,0 +1,64 @@
<script lang="ts">
import { onMount } from 'svelte';
let bar: HTMLDivElement;
let progress = $state(0);
onMount(() => {
let ticking = false;
function update() {
ticking = false;
const scrollable = document.documentElement.scrollHeight - window.innerHeight;
progress = scrollable > 0 ? Math.min(window.scrollY / scrollable, 1) : 0;
// Écriture DOM directe : jamais d'état réactif par frame (playbook §3)
bar.style.transform = `scaleX(${progress})`;
}
function onScroll() {
if (!ticking) {
ticking = true;
requestAnimationFrame(update);
}
}
update();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll, { passive: true });
return () => {
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onScroll);
};
});
</script>
<div
class="scroll-progress"
role="progressbar"
aria-label="Progression de lecture"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow={Math.round(progress * 100)}
bind:this={bar}
></div>
<style>
.scroll-progress {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
z-index: 1001;
background: linear-gradient(to right, var(--or-oki), var(--vert-oki));
transform: scaleX(0);
transform-origin: left;
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.scroll-progress {
display: none;
}
}
</style>
+154
View File
@@ -0,0 +1,154 @@
<script lang="ts">
import KineticText from '$lib/components/motion/KineticText.svelte';
import ResponsiveImage from '$lib/components/ResponsiveImage.svelte';
import type { Bundle, Locale } from '$lib/i18n';
import kaubuntuCom from '$lib/assets/images/clients/kaubuntu-com.png?format=avif;webp;png&w=320;640&as=picture';
import kaubuntuRe from '$lib/assets/images/clients/kaubuntu-re.png?format=avif;webp;png&w=320;640&as=picture';
import kbm from '$lib/assets/images/clients/kbm.gp.webp?format=avif;webp&w=320;640&as=picture';
let { t }: { t: Bundle; locale?: Locale } = $props();
const LOGOS: Record<string, typeof kaubuntuCom> = {
'/assets/images/clients/kaubuntu-com.png': kaubuntuCom,
'/assets/images/clients/kaubuntu-re.png': kaubuntuRe,
'/assets/images/clients/kbm.gp.webp': kbm
};
</script>
<section id="clients" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.clients.section_title} />{' '}
<span class="accent-text"
><KineticText as="span" text={t.clients.section_title_accent} /></span
>
</h2>
<p class="section-subtitle">{t.clients.section_subtitle}</p>
<div class="clients-grid">
{#each t.clients.items as client (client.domain)}
<a href={client.url} target="_blank" rel="noopener noreferrer" class="client-card">
<div class="client-logo-container">
<ResponsiveImage
picture={LOGOS[client.logo]}
alt="Logo {client.name}"
sizes="130px"
class="client-logo"
/>
</div>
<h3>{client.name}</h3>
<p class="client-domain">{client.domain}</p>
<p class="client-description">{client.description}</p>
<span class="client-link">{t.clients.link_text}</span>
</a>
{/each}
</div>
</div>
</section>
<style>
section {
background: var(--noir-profond);
}
.section-title {
text-align: center;
}
.accent-text {
color: var(--or-oki);
}
.section-subtitle {
text-align: center;
margin-left: auto;
margin-right: auto;
}
.clients-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 3rem;
max-width: 900px;
margin-left: auto;
margin-right: auto;
}
.client-card {
background: var(--card-bg);
border: var(--border-card);
border-top: 4px solid var(--turquoise-caraibes);
border-radius: var(--radius-md);
padding: 2.5rem;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
color: inherit;
transition:
transform var(--dur-tanbou) var(--ease-ka),
border-color var(--dur-tanbou) var(--ease-ka);
}
.client-card:hover {
transform: translateY(-4px);
border-top-color: var(--or-oki);
}
.client-logo-container {
width: 130px;
height: 130px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.5rem;
background: color-mix(in srgb, var(--blanc-creme) 4%, transparent);
border-radius: var(--radius-md);
padding: 1rem;
}
.client-logo-container :global(picture) {
display: contents;
}
.client-logo-container :global(.client-logo) {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.client-card h3 {
color: var(--or-oki);
margin-bottom: 0.5rem;
font-size: 1.3rem;
}
.client-domain {
color: var(--rouge-oki);
font-size: 0.9rem;
margin-bottom: 1rem;
font-family: monospace;
text-transform: none;
}
.client-description {
line-height: 1.6;
margin-bottom: 1.5rem;
flex-grow: 1;
}
.client-link {
color: var(--or-oki);
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.4rem;
transition: gap var(--dur-tanbou) var(--ease-ka);
}
.client-card:hover .client-link {
gap: 0.6rem;
}
</style>
+182
View File
@@ -0,0 +1,182 @@
<script lang="ts">
import BrandIcon from '$lib/components/icons/BrandIcon.svelte';
import KineticText from '$lib/components/motion/KineticText.svelte';
import type { Bundle, Locale } from '$lib/i18n';
import { splitLeadingEmoji } from './pictos';
let { t }: { t: Bundle; locale?: Locale } = $props();
type BrandName =
| 'email'
| 'whatsapp'
| 'discord'
| 'telegram'
| 'mastodon'
| 'peertube'
| 'nextcloud'
| 'gitea'
| 'castopod'
| 'stoat'
| 'tiktok'
| 'x';
/** Reprend la logique du partial legacy : icône déduite du libellé du lien. */
function iconFor(text: string): BrandName | null {
const lname = text.toLowerCase();
if (lname.includes('kontak') || lname.includes('mail')) return 'email';
if (lname.includes('whatsapp')) return 'whatsapp';
if (lname.includes('telegram')) return 'telegram';
if (lname.includes('discord')) return 'discord';
if (lname.includes('labola') || lname.includes('gitea')) return 'gitea';
return null;
}
</script>
<section class="section contact" id="contact">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.contact.title} />
</h2>
<!-- Contenu éditorial du bundle i18n (lien mailto balisé). -->
<p class="contact-lede">{@html t.contact.lede}</p>
</div>
<div class="contact-socials container">
{#each t.contact.sections as group (group.title)}
{@const title = splitLeadingEmoji(group.title)}
<div class="contact-socials-group">
<h3 class="contact-socials-title">
{#if title.icon}
<svg class="icon" aria-hidden="true"><use href="/icons.svg#{title.icon}" /></svg>
{/if}
{title.text}
</h3>
<div class="contact-socials-row">
{#each group.links as link (link.url)}
{@const icon = iconFor(link.text)}
<a
href={link.url}
target={link.url.startsWith('http') ? '_blank' : undefined}
rel={link.url.startsWith('http') ? 'noopener noreferrer' : undefined}
class="contact-social-link"
aria-label={link.text}
>
{#if icon}
<BrandIcon name={icon} />
{:else}
<span class="contact-social-text">{link.text}</span>
{/if}
</a>
{/each}
</div>
</div>
{/each}
</div>
</section>
<style>
.contact {
background: var(--card-bg);
}
.section-title {
text-align: center;
color: var(--or-oki);
}
.contact-lede {
text-align: center;
max-width: 800px;
margin: 0 auto 3rem;
font-size: 1.1rem;
line-height: 1.6;
}
.contact-lede :global(a) {
font-weight: 600;
}
.contact-socials {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
}
.contact-socials-group {
text-align: center;
}
.contact-socials-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--or-oki);
margin-bottom: 0.75rem;
opacity: 0.9;
display: flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
}
.contact-socials-title .icon {
width: 1.1em;
height: 1.1em;
}
.contact-socials-row {
display: flex;
justify-content: center;
align-items: center;
gap: 1.5rem;
flex-wrap: wrap;
}
.contact-social-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 3rem;
height: 3rem;
color: var(--or-oki);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--or-oki) 8%, transparent);
transition:
transform var(--dur-tanbou) var(--ease-ka),
background var(--dur-tanbou) var(--ease-ka);
}
.contact-social-link:hover {
transform: scale(1.15);
background: color-mix(in srgb, var(--or-oki) 15%, transparent);
}
.contact-social-link :global(svg) {
width: 60%;
height: 60%;
}
.contact-social-text {
font-size: 0.7rem;
font-weight: 700;
padding: 0 0.25rem;
text-align: center;
}
@media (min-width: 768px) {
.contact-socials {
flex-direction: row;
align-items: flex-start;
justify-content: center;
gap: 3rem;
}
.contact-socials-row {
gap: 2rem;
}
.contact-social-link {
width: 3.5rem;
height: 3.5rem;
}
}
</style>
@@ -0,0 +1,329 @@
<script lang="ts">
import KineticText from '$lib/components/motion/KineticText.svelte';
import ResponsiveImage from '$lib/components/ResponsiveImage.svelte';
import type { Bundle, Locale } from '$lib/i18n';
import { pictoFor } from './pictos';
import portrait from '$lib/assets/images/djangokam-portrait.webp?format=avif;webp&w=480;960&as=picture';
import sheToldMe from '$lib/assets/images/djangokam-she-told-me.webp?format=avif;webp&w=320;640&as=picture';
import iWantYouGirl from '$lib/assets/images/djangokam-i-want-you-girl.webp?format=avif;webp&w=320;640&as=picture';
import ishMwenKute from '$lib/assets/images/djangokam-ish-mwen-kute.jpg?format=avif;webp;jpg&w=320;640&as=picture';
let { t }: { t: Bundle; locale?: Locale } = $props();
const POCHETTES: Record<string, typeof portrait> = {
'/assets/images/djangokam-she-told-me.webp': sheToldMe,
'/assets/images/djangokam-i-want-you-girl.webp': iWantYouGirl,
'/assets/images/djangokam-ish-mwen-kute.jpg': ishMwenKute
};
</script>
<section id="djangokam" class="section">
<div class="container">
<p class="djangokam-surtitre">{t.djangokam.surtitre}</p>
<h2 class="section-title">
<KineticText as="span" text={t.djangokam.titre} />{' '}
<span class="accent-text"
><KineticText as="span" text={t.djangokam.titreAccent} /></span
>
</h2>
<p class="section-subtitle">{t.djangokam.accroche}</p>
<ResponsiveImage
picture={portrait}
alt="Portrait officiel de DJANGOKAM"
sizes="220px"
class="djangokam-portrait"
/>
<blockquote class="djangokam-highlight">
{t.djangokam.message_cle}
</blockquote>
<div class="djangokam-histoire-wrapper">
<div class="djangokam-histoire">
{#each t.djangokam.histoire as paragraphe (paragraphe)}
<p>{paragraphe}</p>
{/each}
</div>
<figure class="djangokam-duo">
<a href={t.djangokam.duo.url} target="_blank" rel="noopener noreferrer">
<ResponsiveImage
picture={POCHETTES[t.djangokam.duo.image]}
alt={t.djangokam.duo.alt}
sizes="(max-width: 768px) 220px, 280px"
/>
</a>
<figcaption>{t.djangokam.duo.legende}</figcaption>
</figure>
</div>
<div class="djangokam-facts">
{#each t.djangokam.faits as fait (fait.chiffre)}
<div class="djangokam-fact-card">
<div class="djangokam-fact-icon">
<svg class="icon" aria-hidden="true"
><use href="/icons.svg#{pictoFor(fait.icon)}" /></svg
>
</div>
<h3>{fait.chiffre}</h3>
<!-- Contenu éditorial du bundle i18n (liens internes balisés). -->
<p>{@html fait.texte}</p>
</div>
{/each}
</div>
<div class="djangokam-clips">
{#each t.djangokam.clips as clip (clip.titre)}
<figure class="djangokam-clip">
<a href={clip.url} target="_blank" rel="noopener noreferrer">
<ResponsiveImage
picture={POCHETTES[clip.image]}
alt={clip.alt}
sizes="(max-width: 768px) 220px, 280px"
/>
</a>
<figcaption>{clip.titre}</figcaption>
</figure>
{/each}
</div>
<!-- Contenu éditorial du bundle i18n (liens internes balisés). -->
<p class="djangokam-instrument">{@html t.djangokam.ia_instrument}</p>
<div class="cta-buttons djangokam-buttons">
{#each t.djangokam.boutons as bouton (bouton.url)}
<a
href={bouton.url}
target="_blank"
rel="noopener noreferrer"
class="btn"
class:btn-solid={bouton.primary}
>
{bouton.text}
</a>
{/each}
</div>
<p class="djangokam-presse">
{t.djangokam.presse.texte}
<a href="mailto:{t.djangokam.presse.email}">{t.djangokam.presse.email}</a>
</p>
</div>
</section>
<style>
section {
background: var(--noir-profond);
text-align: center;
}
.djangokam-surtitre {
text-transform: uppercase;
letter-spacing: 0.15em;
font-size: 0.85rem;
font-weight: 600;
color: var(--vert-oki);
margin-bottom: 0.5rem;
}
.accent-text {
color: var(--or-oki);
}
.section-subtitle {
margin-left: auto;
margin-right: auto;
}
:global(.djangokam-portrait) {
width: 220px;
height: 220px;
object-fit: cover;
border-radius: var(--radius-md);
border: 3px solid var(--or-oki);
margin: 1rem auto 2rem;
}
.djangokam-highlight {
max-width: 800px;
margin: 2rem auto;
padding: 2rem;
text-align: left;
font-size: 1.1rem;
line-height: 1.8;
background: var(--card-bg);
border-left: 4px solid var(--or-oki);
border-radius: 0 var(--radius-md) var(--radius-md) 0;
}
.djangokam-histoire-wrapper {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 2.5rem;
align-items: center;
max-width: 1000px;
margin: 0 auto 3rem;
}
.djangokam-histoire {
text-align: left;
}
.djangokam-histoire p {
font-size: 1.1rem;
line-height: 1.8;
margin-bottom: 1.5rem;
}
.djangokam-duo,
.djangokam-clip {
text-align: center;
}
.djangokam-duo a,
.djangokam-clip a {
color: inherit;
display: inline-block;
transition: transform var(--dur-tanbou) var(--ease-ka);
}
.djangokam-duo a:hover,
.djangokam-clip a:hover {
transform: translateY(-4px);
}
.djangokam-duo :global(picture),
.djangokam-clip :global(picture) {
display: contents;
}
.djangokam-duo :global(img),
.djangokam-clip :global(img) {
width: 100%;
max-width: 280px;
border-radius: var(--radius-md);
border: 2px solid var(--or-oki);
}
.djangokam-duo figcaption,
.djangokam-clip figcaption {
margin-top: 0.75rem;
font-size: 0.9rem;
opacity: 0.8;
}
.djangokam-duo:hover figcaption,
.djangokam-clip:hover figcaption {
opacity: 1;
color: var(--or-oki);
}
.djangokam-clips {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 2.5rem;
margin-bottom: 3rem;
}
.djangokam-facts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1.5rem;
margin-bottom: 3rem;
}
.djangokam-fact-card {
background: var(--card-bg);
border: var(--border-card);
border-left: 3px solid var(--vert-oki);
border-radius: var(--radius-md);
padding: 2rem;
transition: transform var(--dur-tanbou) var(--ease-ka);
}
.djangokam-fact-card:nth-child(2n) {
border-left-color: var(--rouge-oki);
}
.djangokam-fact-card:hover {
transform: translateY(-4px);
}
.djangokam-fact-icon {
color: var(--or-oki);
margin-bottom: 1rem;
}
.djangokam-fact-icon .icon {
width: 2rem;
height: 2rem;
}
.djangokam-fact-card h3 {
color: var(--or-oki);
margin-bottom: 0.5rem;
font-size: 1.2rem;
}
.djangokam-instrument {
max-width: 800px;
margin: 0 auto 3rem;
font-size: 1.1rem;
line-height: 1.8;
opacity: 0.85;
}
.djangokam-instrument :global(a),
.djangokam-fact-card :global(a) {
font-weight: 600;
}
.djangokam-instrument :global(a:hover),
.djangokam-fact-card :global(a:hover) {
text-decoration: underline;
}
.djangokam-buttons {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 2rem;
}
.djangokam-presse {
font-size: 0.9rem;
opacity: 0.7;
}
.djangokam-presse a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
:global(.djangokam-portrait) {
width: 160px;
height: 160px;
}
.djangokam-histoire-wrapper {
grid-template-columns: 1fr;
}
.djangokam-duo :global(img),
.djangokam-clip :global(img) {
max-width: 220px;
}
.djangokam-facts {
grid-template-columns: 1fr;
}
.djangokam-buttons {
flex-direction: column;
}
}
</style>
@@ -0,0 +1,98 @@
<script lang="ts">
import KineticText from '$lib/components/motion/KineticText.svelte';
import type { Bundle, Locale } from '$lib/i18n';
import { pictoFor } from './pictos';
let { t }: { t: Bundle; locale?: Locale } = $props();
</script>
<section id="engagement" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.engagement.section_title} />{' '}
<span class="accent-text"
><KineticText as="span" text={t.engagement.section_title_accent} /></span
>
</h2>
<p class="section-subtitle">{t.engagement.section_subtitle}</p>
<div class="engagement-options">
{#each t.engagement.cards as card (card.title)}
<div class="engagement-card">
<div class="engagement-icon">
<svg class="icon" aria-hidden="true"
><use href="/icons.svg#{pictoFor(card.icon)}" /></svg
>
</div>
<h3>{card.title}</h3>
<p>{card.text}</p>
<a href="#contact" class="btn btn-solid">{card.cta}</a>
</div>
{/each}
</div>
</div>
</section>
<style>
.section-title {
text-align: center;
}
.accent-text {
color: var(--or-oki);
}
.section-subtitle {
text-align: center;
margin-left: auto;
margin-right: auto;
}
.engagement-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin: 3rem 0;
}
.engagement-card {
background: var(--card-bg);
border: var(--border-card);
border-top: 4px solid var(--or-oki);
border-radius: var(--radius-md);
padding: 2.5rem;
text-align: center;
transition: transform var(--dur-tanbou) var(--ease-ka);
}
.engagement-card:hover {
transform: translateY(-4px);
}
.engagement-icon {
color: var(--or-oki);
margin-bottom: 1rem;
}
.engagement-icon .icon {
width: 2.25rem;
height: 2.25rem;
}
.engagement-card h3 {
color: var(--or-oki);
margin-bottom: 1rem;
font-size: 1.35rem;
}
.engagement-card p {
margin-bottom: 1.5rem;
line-height: 1.6;
}
@media (max-width: 768px) {
.engagement-options {
grid-template-columns: 1fr;
}
}
</style>
+124
View File
@@ -0,0 +1,124 @@
<script lang="ts">
import KineticText from '$lib/components/motion/KineticText.svelte';
import type { Bundle, Locale } from '$lib/i18n';
let { t }: { t: Bundle; locale?: Locale } = $props();
</script>
<section id="faq" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.faq.section_title} />{' '}
<span class="accent-text"
><KineticText as="span" text={t.faq.section_title_accent} /></span
>
</h2>
<p class="section-subtitle">{t.faq.section_subtitle}</p>
<div class="faq-container">
{#each t.faq.items as item (item.question)}
<details class="faq-item">
<summary class="faq-question">
<span>{item.question}</span>
<span class="faq-icon" aria-hidden="true">+</span>
</summary>
<div class="faq-answer">
<p>{item.answer}</p>
</div>
</details>
{/each}
</div>
<div class="faq-footer">
<p>{t.faq.footer_text}</p>
<a href="#contact" class="btn">{t.faq.footer_cta}</a>
</div>
</div>
</section>
<style>
section {
background: var(--noir-profond);
}
.section-title {
text-align: center;
}
.accent-text {
color: var(--or-oki);
}
.section-subtitle {
text-align: center;
margin-left: auto;
margin-right: auto;
}
.faq-container {
max-width: 800px;
margin: 3rem auto;
}
.faq-item {
background: var(--card-bg);
border: var(--border-card);
border-left: 3px solid var(--line);
border-radius: var(--radius-md);
margin-bottom: 1rem;
transition: border-color var(--dur-tanbou) var(--ease-ka);
}
.faq-item:hover,
.faq-item[open] {
border-left-color: var(--or-oki);
}
.faq-question {
list-style: none;
color: var(--blanc-creme);
padding: 1.5rem;
font-size: 1.05rem;
font-weight: 600;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.faq-question::-webkit-details-marker {
display: none;
}
.faq-question:hover {
color: var(--or-oki);
}
.faq-icon {
font-size: 1.4rem;
color: var(--or-oki);
flex-shrink: 0;
transition: transform var(--dur-tanbou) var(--ease-ka);
}
.faq-item[open] .faq-icon {
transform: rotate(45deg);
}
.faq-answer p {
padding: 0 1.5rem 1.5rem;
line-height: 1.8;
color: var(--muted);
}
.faq-footer {
text-align: center;
margin-top: 3rem;
}
.faq-footer p {
font-size: 1.1rem;
margin-bottom: 1rem;
}
</style>
@@ -0,0 +1,43 @@
<script lang="ts">
import KineticText from '$lib/components/motion/KineticText.svelte';
import type { Bundle, Locale } from '$lib/i18n';
import HostingVillage from './HostingVillage.svelte';
let { t, locale }: { t: Bundle; locale: Locale } = $props();
</script>
<section id="services-libres" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.services.section_title} />{' '}
<span class="accent-text"
><KineticText as="span" text={t.services.section_title_accent} /></span
>
</h2>
<p class="section-subtitle">{t.services.section_subtitle}</p>
<!-- Scène « village créole » : remplace la grille de cards du legacy
(composant autonome, fallback grille accessible inclus). -->
<HostingVillage {t} {locale} />
</div>
</section>
<style>
section {
background: var(--noir-profond);
}
.section-title {
text-align: center;
}
.accent-text {
color: var(--or-oki);
}
.section-subtitle {
text-align: center;
margin-left: auto;
margin-right: auto;
}
</style>
+118
View File
@@ -0,0 +1,118 @@
<script lang="ts">
import type { Bundle, Locale } from '$lib/i18n';
import { splitLeadingEmoji } from './pictos';
let { t }: { t: Bundle; locale?: Locale } = $props();
</script>
<section class="hero">
<div class="hero-content container">
<h1>ORGANISATION KA</h1>
<h2 class="hero-subtitle">INTERNATIONALE</h2>
<p class="hero-tagline">{t.site.tagline}</p>
<div class="hero-tags">
{#each t.hero.tags as rawTag (rawTag)}
{@const tag = splitLeadingEmoji(rawTag)}
<span class="tag">
{#if tag.icon}
<svg class="icon" aria-hidden="true"><use href="/icons.svg#{tag.icon}" /></svg>
{/if}
{tag.text}
</span>
{/each}
</div>
<p class="hero-description">{t.site.description}</p>
<div class="cta-buttons">
<a href="#solutions" class="btn btn-solid">{t.hero.cta_primary}</a>
<a href="#mission" class="btn">{t.hero.cta_secondary}</a>
</div>
</div>
</section>
<style>
.hero {
padding: 9rem 0 5rem;
min-height: 100vh;
display: flex;
align-items: center;
position: relative;
overflow: hidden;
border-bottom: 1px solid var(--line);
}
.hero::before {
content: '';
position: absolute;
top: 0;
right: -5%;
width: 55%;
height: 100%;
background: url('/images/logo-512x512.png') center / contain no-repeat;
opacity: 0.05;
pointer-events: none;
}
.hero-content {
position: relative;
z-index: 2;
width: 100%;
}
h1 {
font-size: clamp(2.2rem, 6vw, 4rem);
font-weight: 900;
margin-bottom: 0.4rem;
line-height: 1.05;
}
.hero-subtitle {
font-size: clamp(1.4rem, 4vw, 2.2rem);
font-weight: 700;
margin-bottom: 1.25rem;
color: var(--or-oki);
}
.hero-tagline {
font-size: 1.4rem;
margin-bottom: 1.5rem;
color: var(--or-oki);
font-weight: 600;
}
.hero-description {
font-size: 1.2rem;
margin-bottom: 2rem;
opacity: 0.85;
max-width: 680px;
}
.hero-tags {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
margin-bottom: 2rem;
}
.hero-tags .tag {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.cta-buttons {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
@media (max-width: 768px) {
.cta-buttons {
flex-direction: column;
}
.cta-buttons .btn {
width: 100%;
text-align: center;
}
}
</style>
+261
View File
@@ -0,0 +1,261 @@
<script lang="ts">
import KineticText from '$lib/components/motion/KineticText.svelte';
import type { Bundle, Locale } from '$lib/i18n';
let { t }: { t: Bundle; locale?: Locale } = $props();
</script>
<section id="hebergement" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.hosting.section_title} />{' '}
<span class="accent-text"
><KineticText as="span" text={t.hosting.section_title_accent} /></span
>
</h2>
<p class="section-subtitle">{t.hosting.section_subtitle}</p>
<div class="hosting-banner">
<h3>{t.hosting.banner_title}</h3>
<p>{t.hosting.banner_text}</p>
<div class="hosting-features">
{#each t.hosting.features as feature (feature)}
<div class="hosting-feature">
<span aria-hidden="true"></span>
{feature}
</div>
{/each}
</div>
</div>
<div class="services-offerings">
<h3>{t.hosting.offerings_title}</h3>
<div class="offer-cards">
{#each t.hosting.offer_cards as card (card.title)}
<div class="offer-card{card.modifier ? ` offer-card--${card.modifier}` : ''}">
<h4>{card.title}</h4>
{#if card.intro}
<p class="offer-intro">{card.intro}</p>
{/if}
<ul class="offer-list">
{#each card.items as item (item)}
<li>{item}</li>
{/each}
</ul>
<p class="offer-tagline">{card.tagline}</p>
</div>
{/each}
</div>
</div>
<div class="hosting-note">
<h4>{t.hosting.note_title}</h4>
<p>{t.hosting.note_intro}</p>
<ul>
{#each t.hosting.note_items as item (item)}
<li>{item}</li>
{/each}
</ul>
</div>
<div class="hosting-why">
<h4>{t.hosting.why_title}</h4>
<p>{t.hosting.why_text}</p>
<a href="#contact" class="btn btn-solid">{t.hosting.why_cta}</a>
</div>
</div>
</section>
<style>
.section-title {
text-align: center;
}
.accent-text {
color: var(--or-oki);
}
.section-subtitle {
text-align: center;
margin-left: auto;
margin-right: auto;
}
.hosting-banner {
background: var(--card-bg);
border: var(--border-card);
border-top: 4px solid var(--or-oki);
border-radius: var(--radius-md);
padding: 2.5rem;
max-width: 900px;
margin: 0 auto 3rem;
text-align: center;
}
.services-offerings h3 {
text-align: center;
color: var(--or-oki);
margin: 3rem 0 2rem;
font-size: 1.5rem;
}
.offer-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 3rem;
}
.offer-card {
background: var(--card-bg);
border: var(--border-card);
border-left: 4px solid var(--vert-oki);
border-radius: var(--radius-md);
padding: 2rem;
}
.offer-card--hosting {
border-left-color: var(--rouge-oki);
}
.offer-card--fediverse {
border-left-color: var(--or-oki);
}
.offer-card h4 {
color: var(--vert-oki);
margin-bottom: 1rem;
font-size: 1.15rem;
}
.offer-card--hosting h4 {
color: var(--rouge-oki);
}
.offer-card--fediverse h4 {
color: var(--or-oki);
}
.offer-intro {
margin-bottom: 1rem;
font-size: 0.95rem;
color: var(--muted);
}
.offer-list {
list-style: none;
margin-bottom: 1.5rem;
}
.offer-list li {
margin-bottom: 0.5rem;
padding-left: 1rem;
position: relative;
}
.offer-list li::before {
content: '';
position: absolute;
left: 0;
top: 0.55em;
width: 6px;
height: 6px;
background: var(--vert-oki);
border-radius: 1px;
}
.offer-card--hosting .offer-list li::before {
background: var(--rouge-oki);
}
.offer-card--fediverse .offer-list li::before {
background: var(--or-oki);
}
.offer-tagline {
font-size: 0.9rem;
color: var(--muted);
font-style: italic;
}
.hosting-banner h3 {
color: var(--or-oki);
font-size: 1.6rem;
margin-bottom: 1rem;
}
.hosting-features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.25rem;
margin: 2rem 0;
text-align: left;
}
.hosting-feature {
display: flex;
align-items: center;
gap: 0.5rem;
}
.hosting-feature span {
color: var(--vert-oki);
font-size: 1.1rem;
}
.hosting-note {
background: var(--card-bg);
border: var(--border-card);
border-left: 4px solid var(--or-oki);
border-radius: var(--radius-md);
padding: 2rem;
margin-top: 3rem;
margin-bottom: 2rem;
}
.hosting-note h4 {
color: var(--or-oki);
margin-bottom: 1rem;
font-size: 1.15rem;
}
.hosting-note ul {
list-style: none;
}
.hosting-note li {
margin-bottom: 0.5rem;
padding-left: 1rem;
position: relative;
}
.hosting-note li::before {
content: '';
position: absolute;
left: 0;
top: 0.55em;
width: 6px;
height: 6px;
background: var(--or-oki);
border-radius: 1px;
}
.hosting-why {
text-align: center;
margin-top: 3rem;
}
.hosting-why h4 {
color: var(--or-oki);
margin-bottom: 1rem;
font-size: 1.15rem;
}
.hosting-why p {
max-width: 800px;
margin: 0 auto 2rem;
line-height: 1.6;
}
</style>
@@ -0,0 +1,540 @@
<script lang="ts">
import type { Bundle, Locale } from '$lib/i18n';
/**
* HostingVillage — scène SVG isométrique « village créole ».
* Chaque bâtiment est une instance hébergée OKI (lien externe) :
* BOKANTE (Mastodon) → case créole à ka
* GADE (PeerTube) → karbay
* NIYAJ (Nextcloud) → silo nuage
* KUTE (Castopod) → case au panneau « KUTE » + antenne
* Fallback accessible : grille de cards HTML (miroir sémantique)
* affichée à la place de la scène si prefers-reduced-motion ou ≤ 640 px.
* DOM/SVG uniquement, aucun WebGL, aucune dépendance (playbook P3).
*/
let { t, locale }: { t: Bundle; locale: Locale } = $props();
interface HostedService {
name: string;
platform: string;
description: string;
url: string;
linkText: string;
}
/** Secours si le bundle ne fournit pas l'entrée (contenu legacy stable). */
const DEFAULTS: Record<string, HostedService> = {
BOKANTE: {
name: 'BOKANTE',
platform: 'Mastodon',
description: '',
url: 'https://bokante.o-k-i.net',
linkText: 'BOKANTE'
},
GADE: {
name: 'GADE',
platform: 'PeerTube',
description: '',
url: 'https://gade.o-k-i.net',
linkText: 'GADE'
},
NIYAJ: {
name: 'NIYAJ',
platform: 'Nextcloud',
description: '',
url: 'https://niyaj.o-k-i.net',
linkText: 'NIYAJ'
},
KUTE: {
name: 'KUTE',
platform: 'Castopod',
description: '',
url: 'https://kute.o-k-i.net/',
linkText: 'KUTE'
}
};
// Les instances de la section « Services hébergés » vivent dans t.services.items
let items = $derived((t.services?.items ?? []) as HostedService[]);
function svc(name: string): HostedService {
return items.find((i) => i.name?.toUpperCase() === name) ?? DEFAULTS[name];
}
let bokante = $derived(svc('BOKANTE'));
let gade = $derived(svc('GADE'));
let niyaj = $derived(svc('NIYAJ'));
let kute = $derived(svc('KUTE'));
let sceneLabel = $derived(
locale === 'en'
? 'Isometric Creole village of OKI hosted services: each building is a link to an instance.'
: 'Village créole isométrique des services hébergés OKI : chaque bâtiment est un lien vers une instance.'
);
</script>
<div class="hosting-village">
<!-- ── Scène isométrique (masquée si reduced-motion ou ≤ 640 px) ── -->
<div class="village-scene">
<svg viewBox="0 0 960 540" width="100%" role="group" aria-label={sceneLabel}>
<!-- Cadre nuit -->
<rect class="sky" x="0" y="0" width="960" height="540" rx="6" />
<!-- Étoiles angulaires -->
<rect class="star" x="120" y="60" width="3" height="3" />
<rect class="star" x="210" y="118" width="3" height="3" />
<rect class="star" x="700" y="58" width="3" height="3" />
<rect class="star" x="822" y="138" width="3" height="3" />
<rect class="star" x="902" y="66" width="3" height="3" />
<rect class="star" x="64" y="182" width="3" height="3" />
<rect class="star" x="770" y="206" width="3" height="3" />
<rect class="star" x="420" y="64" width="3" height="3" />
<!-- Rivage puis sol en losange (arêtes à 30°) -->
<polygon class="shore" points="480,133 860,352 480,571 100,352" />
<polygon class="ground" points="480,138 830,340 480,542 130,340" />
<!-- Place centrale + flambeaux -->
<polygon class="plaza" points="480,321 600,390 480,459 360,390" />
<polygon class="win" points="420,386.5 423.5,390 420,393.5 416.5,390" />
<polygon class="win win-delay" points="540,386.5 543.5,390 540,393.5 536.5,390" />
<polygon class="win" points="480,344.5 483.5,348 480,351.5 476.5,348" />
<!-- ══ NIYAJ — Nextcloud : silo nuage (arrière-plan, gauche) ══ -->
<a
class="bldg"
href={niyaj.url}
target="_blank"
rel="noopener noreferrer"
aria-label="{niyaj.name}{niyaj.platform}"
>
<g class="bldg-art">
<path
class="silo-body"
d="M234,290 L234,190 L306,190 L306,290 A36,20.8 0 0 1 234,290 Z"
/>
<path class="roof-or" d="M234,190 A36,32 0 0 1 306,190 Z" />
<line class="roof-line" x1="234" y1="190" x2="306" y2="190" />
<rect class="win" x="250" y="218" width="10" height="10" />
<rect class="win win-delay" x="272" y="246" width="10" height="10" />
<rect class="door" x="260" y="258" width="18" height="32" />
<!-- Nuage angulaire au-dessus du silo -->
<rect class="cloud" x="244" y="134" width="52" height="12" />
<rect class="cloud" x="254" y="122" width="30" height="12" />
<rect class="cloud" x="268" y="112" width="18" height="10" />
<rect class="cloud" x="258" y="150" width="3" height="5" />
<rect class="cloud" x="276" y="148" width="3" height="5" />
<path
class="bldg-outline"
d="M234,190 A36,32 0 0 1 306,190 L306,290 A36,20.8 0 0 1 234,290 Z"
/>
</g>
<g class="bldg-label">
<rect class="label-plaque" x="186" y="70" width="168" height="24" rx="3" />
<text class="label-text" x="270" y="87">{niyaj.name}{niyaj.platform}</text>
</g>
</a>
<!-- ══ GADE — PeerTube : karbay (arrière-plan, droite) ══ -->
<a
class="bldg"
href={gade.url}
target="_blank"
rel="noopener noreferrer"
aria-label="{gade.name}{gade.platform}"
>
<g class="bldg-art">
<rect class="post" x="657" y="188" width="6" height="72" />
<polygon class="deck-top" points="660,262 729,302 660,342 591,302" />
<polygon class="deck-left" points="591,302 660,342 660,350 591,310" />
<polygon class="deck-right" points="660,342 729,302 729,310 660,350" />
<rect class="post" x="570" y="238" width="6" height="72" />
<rect class="post" x="744" y="238" width="6" height="72" />
<rect class="post" x="657" y="288" width="6" height="72" />
<polygon class="roof-vert-dark" points="566,238 660,292 660,132" />
<polygon class="roof-vert" points="660,292 754,238 660,132" />
<line class="roof-line" x1="660" y1="132" x2="660" y2="292" />
<!-- Lanterne suspendue -->
<line class="lantern-cord" x1="640" y1="262" x2="640" y2="274" />
<rect class="win" x="635" y="274" width="10" height="12" />
<path
class="bldg-outline"
d="M660,132 L754,238 L750,238 L750,310 L729,310 L660,350 L591,310 L570,310 L570,238 L566,238 Z"
/>
</g>
<g class="bldg-label">
<rect class="label-plaque" x="576" y="92" width="168" height="24" rx="3" />
<text class="label-text" x="660" y="109">{gade.name}{gade.platform}</text>
</g>
</a>
<!-- ══ BOKANTE — Mastodon : case créole à ka (avant-plan, gauche) ══ -->
<a
class="bldg"
href={bokante.url}
target="_blank"
rel="noopener noreferrer"
aria-label="{bokante.name}{bokante.platform}"
>
<g class="bldg-art">
<polygon class="wall-left" points="312,430 390,475 390,420 312,375" />
<polygon class="wall-right" points="390,475 468,430 468,375 390,420" />
<line class="lamella" x1="338" y1="390" x2="338" y2="445" />
<line class="lamella" x1="364" y1="405" x2="364" y2="460" />
<line class="lamella" x1="410" y1="409" x2="410" y2="464" />
<line class="lamella" x1="429" y1="397.5" x2="429" y2="452.5" />
<line class="lamella" x1="448" y1="386" x2="448" y2="441" />
<polygon class="win" points="330,404 348,414 348,428 330,418" />
<polygon class="win win-delay" points="430,410 446,401 446,415 430,424" />
<polygon class="door" points="398,470 416,460 416,430 398,440" />
<rect class="door-knob" x="410" y="444" width="3" height="3" />
<polygon class="roof-rouge-dark" points="293,375 390,431 390,283" />
<polygon class="roof-rouge" points="390,431 487,375 390,283" />
<line class="roof-line" x1="390" y1="283" x2="390" y2="431" />
<!-- Ka (tambour gwoka) devant la case -->
<path class="drum" d="M405,468 L405,490 A10,5.8 0 0 0 425,490 L425,468 Z" />
<ellipse class="drum-top" cx="415" cy="468" rx="10" ry="5.8" />
<path
class="bldg-outline"
d="M390,283 L487,375 L468,375 L468,430 L390,475 L312,430 L312,375 L293,375 Z"
/>
</g>
<g class="bldg-label">
<rect class="label-plaque" x="316" y="232" width="168" height="24" rx="3" />
<text class="label-text" x="400" y="249">{bokante.name}{bokante.platform}</text>
</g>
</a>
<!-- ══ KUTE — Castopod : case au panneau « KUTE » + antenne (avant-plan, droite) ══ -->
<a
class="bldg"
href={kute.url}
target="_blank"
rel="noopener noreferrer"
aria-label="{kute.name}{kute.platform}"
>
<g class="bldg-art">
<polygon class="wall-left" points="544,440 600,478 600,432 544,394" />
<polygon class="wall-right" points="600,478 676,440 676,394 600,432" />
<line class="lamella" x1="572" y1="413" x2="572" y2="459" />
<line class="lamella" x1="625" y1="419" x2="625" y2="465" />
<line class="lamella" x1="650" y1="407" x2="650" y2="453" />
<polygon class="win" points="560,414 574,422 574,434 560,426" />
<polygon class="door" points="608,474 624,465 624,431 608,440" />
<!-- Panneau « KUTE » sur la façade gauche -->
<polygon class="sign" points="548,404 592,429 592,443 548,418" />
<text
class="sign-text"
transform="matrix(1,0.577,0,1,552,416)"
x="0"
y="0">KUTE</text
>
<polygon class="roof-rouge-dark" points="517,384 600,432 600,304" />
<polygon class="roof-rouge" points="600,432 683,384 600,304" />
<line class="roof-line" x1="600" y1="304" x2="600" y2="432" />
<!-- Antenne d'émission (podcast) -->
<line class="antenna" x1="600" y1="304" x2="600" y2="284" />
<line class="antenna-tick" x1="600" y1="284" x2="593" y2="276" />
<line class="antenna-tick" x1="600" y1="284" x2="607" y2="276" />
<line class="antenna-tick" x1="588" y1="270" x2="582" y2="262" />
<line class="antenna-tick" x1="612" y1="270" x2="618" y2="262" />
<path
class="bldg-outline"
d="M600,304 L683,384 L676,394 L676,440 L600,478 L544,440 L544,394 L517,384 Z"
/>
</g>
<g class="bldg-label">
<rect class="label-plaque" x="516" y="496" width="168" height="24" rx="3" />
<text class="label-text" x="600" y="513">{kute.name}{kute.platform}</text>
</g>
</a>
</svg>
</div>
<!-- ── Fallback : grille de cards (miroir sémantique de la scène) ── -->
<ul class="village-fallback" role="list">
{#each items as item (item.name)}
<li class="card vf-card">
<div class="vf-head">
<h3>{item.name}</h3>
<span class="tag">{item.platform}</span>
</div>
<p class="vf-desc">{item.description}</p>
<a class="vf-link" href={item.url} target="_blank" rel="noopener noreferrer"
>{item.linkText}</a
>
</li>
{/each}
</ul>
</div>
<style>
.hosting-village {
margin-top: var(--space-4);
}
.village-scene svg {
display: block;
width: 100%;
height: auto;
}
/* ── Nuit, sol, végétation minérale ── */
.sky {
fill: color-mix(in srgb, var(--noir-profond) 45%, var(--noir-oki));
stroke: var(--line);
stroke-width: 1;
}
.star {
fill: var(--blanc-creme);
opacity: 0.25;
}
.shore {
fill: var(--gris-sombre);
opacity: 0.35;
}
.ground {
fill: var(--noir-profond);
stroke: var(--line);
stroke-width: 1;
}
.plaza {
fill: rgba(255, 255, 255, 0.04);
stroke: var(--line);
stroke-width: 1;
}
/* ── Matériaux des bâtiments ── */
.wall-left {
fill: color-mix(in srgb, var(--gris-sombre) 60%, var(--noir-oki));
}
.wall-right {
fill: var(--gris-sombre);
}
.lamella {
stroke: rgba(255, 255, 255, 0.06);
stroke-width: 1;
}
.door {
fill: var(--noir-oki);
}
.door-knob {
fill: var(--or-oki);
opacity: 0.7;
}
.silo-body {
fill: color-mix(in srgb, var(--gris-sombre) 75%, var(--noir-oki));
}
.deck-top {
fill: var(--gris-sombre);
}
.deck-left {
fill: color-mix(in srgb, var(--gris-sombre) 60%, var(--noir-oki));
}
.deck-right {
fill: color-mix(in srgb, var(--gris-sombre) 80%, var(--noir-oki));
}
.post {
fill: color-mix(in srgb, var(--gris-sombre) 50%, var(--noir-oki));
}
.drum {
fill: color-mix(in srgb, var(--gris-sombre) 55%, var(--noir-oki));
}
.drum-top {
fill: var(--noir-profond);
stroke: color-mix(in srgb, var(--or-oki) 55%, transparent);
stroke-width: 1;
}
.cloud {
fill: var(--blanc-creme);
opacity: 0.15;
}
.sign {
fill: var(--noir-oki);
stroke: color-mix(in srgb, var(--or-oki) 45%, transparent);
stroke-width: 1;
}
.sign-text {
font-family: var(--font-display);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
fill: var(--blanc-creme);
}
.antenna {
stroke: var(--blanc-creme);
stroke-width: 2;
opacity: 0.5;
}
.antenna-tick {
stroke: var(--or-oki);
stroke-width: 1.5;
opacity: 0.75;
}
.lantern-cord {
stroke: var(--line);
stroke-width: 1;
}
/* ── Toits : couleurs du drapeau, un accent par bâtiment ── */
.roof-rouge {
fill: var(--rouge-oki);
}
.roof-rouge-dark {
fill: color-mix(in srgb, var(--rouge-oki) 55%, var(--noir-oki));
}
.roof-vert {
fill: var(--vert-oki);
}
.roof-vert-dark {
fill: color-mix(in srgb, var(--vert-oki) 55%, var(--noir-oki));
}
.roof-or {
fill: var(--or-oki);
}
.roof-line {
stroke: rgba(0, 0, 0, 0.35);
stroke-width: 1;
}
/* ── Fenêtres lumineuses : scintillement subtil,
coupé par la gate reduced-motion globale de base.css ── */
.win {
fill: var(--or-clair);
opacity: 0.82;
animation: win-glow 4.5s ease-in-out infinite alternate;
}
.win-delay {
animation-delay: -2.2s;
}
@keyframes win-glow {
from {
opacity: 0.5;
}
to {
opacity: 0.95;
}
}
/* ── Interaction : liseré or + label + léger soulèvement ── */
.bldg {
cursor: pointer;
}
.bldg-art {
transition: transform var(--dur-tanbou) var(--ease-ka);
}
.bldg:hover .bldg-art,
.bldg:focus-visible .bldg-art {
transform: translateY(-5px);
}
.bldg-outline {
fill: none;
stroke: var(--or-oki);
stroke-width: 2.5;
stroke-linejoin: miter;
opacity: 0;
transition: opacity var(--dur-tanbou) var(--ease-ka);
}
.bldg:hover .bldg-outline,
.bldg:focus-visible .bldg-outline {
opacity: 1;
}
.bldg-label {
opacity: 0;
transition: opacity var(--dur-tanbou) var(--ease-ka);
}
.bldg:hover .bldg-label,
.bldg:focus-visible .bldg-label {
opacity: 1;
}
.label-plaque {
fill: rgba(13, 13, 13, 0.88);
stroke: var(--or-oki);
stroke-width: 1;
}
.label-text {
font-family: var(--font-display);
font-size: 13px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
text-anchor: middle;
fill: var(--or-oki);
}
/* ── Fallback : miroir sémantique, un seul actif à la fois ── */
.village-fallback {
display: none;
list-style: none;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: var(--space-3);
}
.vf-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
flex-wrap: wrap;
margin-bottom: var(--space-2);
}
.vf-head h3 {
font-size: 1.1rem;
}
.vf-desc {
color: var(--muted);
font-size: 0.95rem;
margin-bottom: var(--space-2);
}
.vf-link {
font-weight: 600;
}
@media (prefers-reduced-motion: reduce), (max-width: 640px) {
.village-scene {
display: none;
}
.village-fallback {
display: grid;
}
}
</style>
@@ -0,0 +1,98 @@
<script lang="ts">
import KineticText from '$lib/components/motion/KineticText.svelte';
import type { Bundle, Locale } from '$lib/i18n';
import { pictoFor } from './pictos';
let { t }: { t: Bundle; locale?: Locale } = $props();
</script>
<section id="mission" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.mission.section_title} />{' '}
<span class="accent-text"
><KineticText as="span" text={t.mission.section_title_accent} /></span
>
</h2>
<p class="mission-text">{t.mission.paragraph_main}</p>
<p class="mission-text mission-text--secondary">{t.mission.paragraph_secondary}</p>
<div class="values-grid">
{#each t.values as value (value.title)}
<div class="value-card">
<div class="value-icon">
<svg class="icon" aria-hidden="true"
><use href="/icons.svg#{pictoFor(value.icon)}" /></svg
>
</div>
<h3>{value.title}</h3>
<p>{value.description}</p>
</div>
{/each}
</div>
</div>
</section>
<style>
section {
background: var(--noir-profond);
text-align: center;
}
.accent-text {
color: var(--or-oki);
}
.mission-text {
font-size: 1.2rem;
max-width: 800px;
margin: 0 auto 3rem;
line-height: 1.8;
}
.mission-text--secondary {
font-size: 1.1rem;
}
.values-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-top: 3rem;
max-width: 1000px;
margin-left: auto;
margin-right: auto;
}
.value-card {
background: var(--card-bg);
border: var(--border-card);
border-top: 3px solid var(--or-oki);
border-radius: var(--radius-md);
padding: 2rem;
transition:
transform var(--dur-tanbou) var(--ease-ka),
border-color var(--dur-tanbou) var(--ease-ka);
}
.value-card:hover {
transform: translateY(-4px);
border-top-color: var(--vert-oki);
}
.value-icon {
color: var(--or-oki);
margin-bottom: 1rem;
}
.value-icon .icon {
width: 2rem;
height: 2rem;
}
.value-card h3 {
color: var(--or-oki);
margin-bottom: 0.5rem;
font-size: 1.2rem;
}
</style>
+164
View File
@@ -0,0 +1,164 @@
<script lang="ts">
import KineticText from '$lib/components/motion/KineticText.svelte';
import ResponsiveImage from '$lib/components/ResponsiveImage.svelte';
import type { Bundle, Locale } from '$lib/i18n';
import joukawouve from '$lib/assets/images/joukawouve.png?format=avif;webp;png&w=320;640&as=picture';
import akv from '$lib/assets/images/akv.png?format=avif;webp;png&w=320;640&as=picture';
import cyberMawonaj from '$lib/assets/images/cyber_mawonaj.jpg?format=avif;webp;jpg&w=320;640&as=picture';
let { t }: { t: Bundle; locale?: Locale } = $props();
const LOGOS: Record<string, typeof joukawouve> = {
'/assets/images/joukawouve.png': joukawouve,
'/assets/images/akv.png': akv,
'/assets/images/cyber_mawonaj.jpg': cyberMawonaj
};
</script>
<section id="partenaires" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.partners.section_title} />{' '}
<span class="accent-text"
><KineticText as="span" text={t.partners.section_title_accent} /></span
>
</h2>
<p class="section-subtitle">{t.partners.section_subtitle}</p>
<div class="partners-grid">
{#each t.partners.items as partner (partner.name)}
<div class="partner-card">
<div class="partner-logo-container">
<ResponsiveImage
picture={LOGOS[partner.logo]}
alt="Logo {partner.name}"
sizes="130px"
class="partner-logo"
/>
</div>
<h3>{partner.name}</h3>
<p class="partner-description">{partner.description}</p>
<div class="partner-links">
{#each partner.links as link (link.url)}
<a href={link.url} target="_blank" rel="noopener noreferrer" class="partner-link">
{link.text}
</a>
{/each}
</div>
</div>
{/each}
</div>
</div>
</section>
<style>
section {
background: var(--noir-profond);
}
.section-title {
text-align: center;
}
.accent-text {
color: var(--or-oki);
}
.section-subtitle {
text-align: center;
margin-left: auto;
margin-right: auto;
}
.partners-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 3rem;
max-width: 900px;
margin-left: auto;
margin-right: auto;
}
.partner-card {
background: var(--card-bg);
border: var(--border-card);
border-top: 4px solid var(--turquoise-caraibes);
border-radius: var(--radius-md);
padding: 2.5rem;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
transition:
transform var(--dur-tanbou) var(--ease-ka),
border-color var(--dur-tanbou) var(--ease-ka);
}
.partner-card:hover {
transform: translateY(-4px);
border-top-color: var(--or-oki);
}
.partner-logo-container {
width: 130px;
height: 130px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.5rem;
background: color-mix(in srgb, var(--blanc-creme) 4%, transparent);
border-radius: var(--radius-md);
padding: 1rem;
}
.partner-logo-container :global(picture) {
display: contents;
}
.partner-logo-container :global(.partner-logo) {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.partner-card h3 {
color: var(--or-oki);
margin-bottom: 1rem;
font-size: 1.3rem;
}
.partner-description {
line-height: 1.6;
margin-bottom: 1.5rem;
flex-grow: 1;
}
.partner-links {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.6rem;
}
.partner-link {
color: var(--or-oki);
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.95rem;
padding: 0.4rem 0.9rem;
border: 1px solid var(--or-oki);
border-radius: var(--radius-sm);
transition:
background var(--dur-tanbou) var(--ease-ka),
gap var(--dur-tanbou) var(--ease-ka);
}
.partner-link:hover {
background: color-mix(in srgb, var(--or-oki) 12%, transparent);
gap: 0.6rem;
}
</style>
+114
View File
@@ -0,0 +1,114 @@
<script lang="ts">
import KineticText from '$lib/components/motion/KineticText.svelte';
import type { Bundle, Locale } from '$lib/i18n';
import { pictoFor } from './pictos';
let { t }: { t: Bundle; locale?: Locale } = $props();
</script>
<section id="projets" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.projects.section_title} />{' '}
<span class="accent-text"
><KineticText as="span" text={t.projects.section_title_accent} /></span
>
</h2>
<div class="project-cards">
{#each t.projects.items as item (item.name)}
<a href={item.url} target="_blank" rel="noopener noreferrer" class="project-card">
<div class="project-icon">
<svg class="icon" aria-hidden="true"
><use href="/icons.svg#{pictoFor(item.icon)}" /></svg
>
</div>
<h3>{item.name}</h3>
<p class="project-domain">{item.domain}</p>
<p>{item.description}</p>
<span class="project-card-link">{t.projects.link_text}</span>
</a>
{/each}
</div>
</div>
</section>
<style>
.section-title {
text-align: center;
}
.accent-text {
color: var(--or-oki);
}
.project-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-top: 3rem;
}
.project-card {
background: var(--card-bg);
border: var(--border-card);
border-top: 4px solid var(--or-oki);
border-radius: var(--radius-md);
padding: 2rem;
text-align: center;
display: block;
color: inherit;
transition:
transform var(--dur-tanbou) var(--ease-ka),
border-color var(--dur-tanbou) var(--ease-ka);
}
.project-card:hover {
transform: translateY(-4px);
border-color: var(--or-oki);
}
.project-icon {
color: var(--or-oki);
margin-bottom: 1rem;
}
.project-icon .icon {
width: 2.5rem;
height: 2.5rem;
}
.project-card h3 {
color: var(--or-oki);
margin-bottom: 0.5rem;
font-size: 1.4rem;
}
.project-domain {
color: var(--rouge-oki);
font-size: 0.9rem;
margin-bottom: 1rem;
font-family: monospace;
text-transform: none;
}
.project-card-link {
color: var(--or-oki);
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.4rem;
margin-top: 1rem;
transition: gap var(--dur-tanbou) var(--ease-ka);
}
.project-card:hover .project-card-link {
gap: 0.75rem;
}
@media (max-width: 768px) {
.project-cards {
grid-template-columns: 1fr;
}
}
</style>
+172
View File
@@ -0,0 +1,172 @@
<script lang="ts">
import KineticText from '$lib/components/motion/KineticText.svelte';
import type { Bundle, Locale } from '$lib/i18n';
import { pictoFor } from './pictos';
let { t }: { t: Bundle; locale?: Locale } = $props();
</script>
<section id="solutions" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.publics.section_title} />{' '}
<span class="accent-text"
><KineticText as="span" text={t.publics.section_title_accent} /></span
>
</h2>
<p class="section-subtitle">{t.publics.section_subtitle}</p>
<div class="publics-grid">
{#each t.publics.items as item (item.title)}
<div class="public-card">
<div class="public-icon">
<svg class="icon" aria-hidden="true"
><use href="/icons.svg#{pictoFor(item.icon)}" /></svg
>
</div>
<h3>{item.title}</h3>
<p class="public-description">{item.description}</p>
<div class="public-benefits">
<h4>{t.publics.benefits_label}</h4>
<ul>
{#each item.benefits as benefit (benefit)}
<li>{benefit}</li>
{/each}
</ul>
</div>
<a href="#contact" class="btn btn-solid public-cta">{item.cta}</a>
</div>
{/each}
</div>
<div class="publics-footer">
<p class="publics-footer-lead">{t.publics.footer_line1}</p>
<p>{t.publics.footer_line2}</p>
</div>
</div>
</section>
<style>
.section-title {
text-align: center;
}
.accent-text {
color: var(--or-oki);
}
.section-subtitle {
text-align: center;
margin-left: auto;
margin-right: auto;
}
.publics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 3rem;
}
.public-card {
background: var(--card-bg);
border: var(--border-card);
border-top: 4px solid var(--turquoise-caraibes);
border-radius: var(--radius-md);
padding: 2.5rem;
text-align: center;
display: flex;
flex-direction: column;
transition:
transform var(--dur-tanbou) var(--ease-ka),
border-color var(--dur-tanbou) var(--ease-ka);
}
.public-card:hover {
transform: translateY(-4px);
border-top-color: var(--or-oki);
}
.public-icon {
color: var(--or-oki);
margin-bottom: 1rem;
}
.public-icon .icon {
width: 2.5rem;
height: 2.5rem;
}
.public-card h3 {
color: var(--or-oki);
margin-bottom: 1rem;
font-size: 1.5rem;
}
.public-description {
margin-bottom: 2rem;
line-height: 1.6;
flex-grow: 1;
}
.public-benefits {
background: var(--card-bg);
border: var(--border-card);
border-radius: var(--radius-sm);
padding: 1.5rem;
margin-bottom: 2rem;
text-align: left;
}
.public-benefits h4 {
color: var(--or-oki);
margin-bottom: 1rem;
font-size: 1.05rem;
}
.public-benefits ul {
list-style: none;
}
.public-benefits li {
margin-bottom: 0.5rem;
padding-left: 0.5rem;
}
.public-benefits li::before {
content: '✓';
color: var(--vert-oki);
margin-right: 0.35rem;
}
.public-cta {
margin-top: auto;
}
.publics-footer {
text-align: center;
margin-top: 4rem;
padding: 2.5rem;
background: var(--card-bg);
border: var(--border-card);
border-radius: var(--radius-md);
}
.publics-footer-lead {
font-size: 1.2rem;
margin-bottom: 1rem;
font-weight: 600;
}
.publics-footer p:last-child {
opacity: 0.85;
}
@media (max-width: 768px) {
.publics-grid {
grid-template-columns: 1fr;
}
}
</style>
+48
View File
@@ -0,0 +1,48 @@
/**
* Mapping emojis legacy → pictos du sprite OKI (`/icons.svg`).
* Charte §4 : zéro emoji décoratif en production.
* Les clés sont normalisées (sans sélecteur de variation U+FE0F
* ni modificateur de teint U+1F3FBU+1F3FF) pour fiabiliser la
* correspondance avec les chaînes des JSON i18n.
*/
const PICTOS: Record<string, string> = {
'🌐': 'lanme', // web, liens internationaux
'📢': 'lambi', // annonces, chaînes
'💬': 'pawol', // communauté, discussion
'💻': 'kle', // code, open-source
'✊': 'glo', // luttes, tarifs solidaires
'💸': 'glo', // tarifs préférentiels / solidaires
'🔓': 'kle', // liberté, open-source
'🤝': 'jaden', // solidarité
'🛡': 'mawon', // souveraineté
'💼': 'zetwal', // entreprises responsables
'🏛': 'kannen', // collectifs, patrimoine
'🎵': 'mizik-note', // sorties musicales
'🎶': 'mizik-note', // transcriptions musicales (PAWOL.NU)
'🎬': 'zetwal', // clips / plateforme multimédia
'🎤': 'glo', // artistes engagés
'📝': 'pawol', // paroles & traductions
'📚': 'kannen' // documents historiques (GONG.GP)
};
/** Picto du sprite correspondant à un emoji legacy (`ka` en repli). */
export function pictoFor(icon: string): string {
const key = icon.replace(/\uFE0E|\uFE0F/g, '').replace(/[\u{1F3FB}-\u{1F3FF}]/gu, '');
return PICTOS[key] ?? 'ka';
}
const LEADING_EMOJI = /^(?<emoji>\p{Extended_Pictographic}\uFE0F?[\u{1F3FB}-\u{1F3FF}]?)\s*/u;
/**
* Sépare un emoji en tête d'une chaîne de contenu (tags hero, etc.) :
* l'emoji devient un picto du sprite, le texte reste intact.
*/
export function splitLeadingEmoji(raw: string): { icon: string | null; text: string } {
const match = raw.match(LEADING_EMOJI);
if (!match?.groups?.emoji) return { icon: null, text: raw };
return {
icon: pictoFor(match.groups.emoji),
text: raw.slice(match[0].length)
};
}

Some files were not shown because too many files have changed in this diff Show More