Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d1885b543 | ||
|
|
f6de9aa5df | ||
|
|
a6f359559b | ||
|
|
30858926fa | ||
|
|
d0d80a105e | ||
|
|
d0fc7207f2
|
||
|
|
66da280a50
|
||
|
|
742a275958
|
||
|
|
e3b562afcf
|
||
|
|
284e29f6d5
|
||
|
|
2f16ccb4b8
|
||
|
|
4307c80bd6
|
||
|
|
17540980af
|
||
|
|
da094adaad
|
||
|
|
5f5e581d99
|
||
|
|
851b03c60b
|
||
|
|
db5fc5240c
|
||
|
|
1644d2606f
|
||
|
|
d3afa0ca1d | ||
|
|
de4fd1d54c | ||
|
|
b650a52591 | ||
|
|
1189141937 | ||
|
|
e2df9c0501 | ||
|
|
964149e0bb | ||
|
|
46a039d2d2 | ||
|
|
c27f37f979
|
||
|
|
6559ac39a4
|
||
|
|
f1a9fa63e0
|
||
|
|
646d3b676f
|
||
|
|
37eeee07dd
|
||
|
|
36d1e307bd
|
||
|
|
8dac00d692
|
||
|
|
9425f25442
|
||
|
|
1755bc1eda
|
||
|
|
2f6bb848e3
|
||
|
|
cf7385a350
|
||
|
|
d32801a5d1
|
||
|
|
be73fb4a70
|
||
|
|
5908b3f273
|
||
|
|
fd86ba8b6a
|
||
|
|
853866b4c4
|
||
|
|
bd52f78922
|
||
|
|
218ec8072f
|
||
|
|
931bd48a52
|
||
|
|
19711cc8e0
|
||
|
|
ac0fc22f3b
|
||
|
|
40df39cc67
|
||
|
|
09fef8f590
|
||
|
|
711244235f
|
||
|
|
a1e39b88bf
|
||
|
|
929cb46d4c
|
||
|
|
ffb2eedccb
|
||
|
|
58dd9297e0
|
@@ -1,5 +1,16 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
.svelte-kit/
|
||||||
|
build/
|
||||||
_site/
|
_site/
|
||||||
|
# Documents de travail locaux (charte, recettes, briefs, guides agents)
|
||||||
|
AGENTS.md
|
||||||
|
charte-oki-design-system.md
|
||||||
|
prompt-kimi-cli-transformation-oki.md
|
||||||
|
recette-sveltekit-15-sites-awwwards.md
|
||||||
|
recette-sveltekit-playbook-agent.md
|
||||||
|
svelte-5_code_writer.md
|
||||||
|
svelte_core_bestpractices.md
|
||||||
|
playbook-oki-sveltekit.md
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.log
|
*.log
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -1,5 +1,35 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [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`
|
||||||
|
- ✅ **CSP stricte** : refactorisation des scripts inline en fichiers externes et suppression des gestionnaires `onclick` inline
|
||||||
|
|
||||||
|
### 🎬 Contenu
|
||||||
|
- ✅ **Nouveau projet** : ajout de `FEDIVERSE.OKI` dans la section projets
|
||||||
|
- ✅ **Logos clients** : récupération locale des logos distants et service via le CDN
|
||||||
|
- ✅ **Réseaux sociaux** : mise à jour du lien Discord, retrait de Stoat, X (Twitter) et TikTok
|
||||||
|
- ✅ **Chaînes PAWOL.NU** : distinction visuelle dans la page contact entre les chaînes WhatsApp/Telegram et les autres moyens de contact
|
||||||
|
- ✅ **Mentions légales** : hébergeur mis à jour (OVH → o2switch)
|
||||||
|
- ✅ **Footer** : année du copyright générée automatiquement via un shortcode Eleventy
|
||||||
|
- ✅ **Liens Labola** : lien footer vers le dépôt du site, lien contact vers l'organisation Gitea
|
||||||
|
|
||||||
|
### 🔧 Technique
|
||||||
|
- ✅ Shortcode Eleventy `{% year %}` pour l'année en cours
|
||||||
|
- ✅ Événements JS attachés dans `main.js` pour les accordéons FAQ et les dons personnalisés
|
||||||
|
|
||||||
## Version 2.1.0 - Design Afrofuturisme Caribéen (2025-11-05)
|
## Version 2.1.0 - Design Afrofuturisme Caribéen (2025-11-05)
|
||||||
|
|
||||||
### 🌓 Système de Thème Clair/Sombre
|
### 🌓 Système de Thème Clair/Sombre
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
# Guide de contribution
|
# Guide de contribution
|
||||||
|
|
||||||
Merci de votre intérêt pour contribuer au site dE l'ORGANISATION KA INTERNATIONALE !
|
Merci de votre intérêt pour contribuer au site de l'ORGANISATION KA INTERNATIONALE !
|
||||||
|
|
||||||
|
Le site est une application **SvelteKit + Svelte 5 (runes) + TypeScript**, entièrement pré-rendue en statique. Avant de contribuer, lisez `charte-oki-design-system.md` (marque) et `svelte_core_bestpractices.md` (conventions Svelte 5).
|
||||||
|
|
||||||
## Ajouter du contenu
|
## Ajouter du contenu
|
||||||
|
|
||||||
### Ajouter un service Fédiverse
|
Le contenu éditorial vit dans les bundles i18n : `src/lib/i18n/fr/*.json` et `src/lib/i18n/en/*.json` — **toujours modifier les deux locales**.
|
||||||
|
|
||||||
Éditez `src/_data/services.json` :
|
### Ajouter un service hébergé
|
||||||
|
|
||||||
|
Éditez `src/lib/i18n/fr/services.json` (et `en/`) :
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "NOM_DU_SERVICE",
|
"name": "NOM_DU_SERVICE",
|
||||||
"icon": "🎯",
|
|
||||||
"platform": "Nom de la plateforme",
|
"platform": "Nom de la plateforme",
|
||||||
"description": "Description du service",
|
"description": "Description du service",
|
||||||
"url": "https://service.o-k-i.net/",
|
"url": "https://service.o-k-i.net/",
|
||||||
@@ -19,112 +22,85 @@ Merci de votre intérêt pour contribuer au site dE l'ORGANISATION KA INTERNATIO
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Le service apparaîtra automatiquement dans la grille de fallback du village (`HostingVillage.svelte`). Pour qu'il ait aussi son bâtiment dans la scène SVG, voir « Modifier un composant ».
|
||||||
|
|
||||||
### Ajouter un projet web
|
### Ajouter un projet web
|
||||||
|
|
||||||
Éditez `src/_data/projects.json` :
|
Éditez `src/lib/i18n/fr/projects.json` (et `en/`). Le champ `icon` attend un emoji legacy qui sera remplacé au rendu par un picto du sprite — voir le mapping dans `src/lib/components/sections/pictos.ts` et choisissez-en un qui y figure.
|
||||||
|
|
||||||
```json
|
### Ajouter un client ou partenaire
|
||||||
{
|
|
||||||
"name": "NOM.PROJET",
|
Placez le logo dans `src/lib/assets/images/` (et `clients/` pour les clients), puis importez-le dans le composant de section concerné via vite-imagetools :
|
||||||
"icon": "📱",
|
|
||||||
"domain": "nom.projet",
|
```ts
|
||||||
"description": "Description du projet"
|
import logo from '$lib/assets/images/mon-logo.png?format=avif;webp;png&w=320;640&as=picture';
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
et affichez-le avec `<ResponsiveImage picture={logo} alt="…" />`. Ne jamais référencer une image distante : tout asset est self-hébergé (doctrine souveraineté + CSP).
|
||||||
|
|
||||||
### Ajouter une valeur
|
### Ajouter une valeur
|
||||||
|
|
||||||
Éditez `src/_data/values.json` :
|
Éditez `src/lib/i18n/fr/values.json` (et `en/`). Le champ `icon` suit le même mapping que les projets (`pictos.ts`).
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"icon": "🌟",
|
|
||||||
"title": "Titre",
|
|
||||||
"description": "Description de la valeur"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Modifier la navigation
|
### Modifier la navigation
|
||||||
|
|
||||||
Éditez `src/_data/navigation.json` :
|
Éditez `src/lib/i18n/fr/navigation.json` (et `en/`) :
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "text": "Texte du lien", "url": "/#ancre", "external": false }
|
||||||
"text": "Texte du lien",
|
|
||||||
"url": "#section",
|
|
||||||
"external": false
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Pour un lien externe, mettez `"external": true`.
|
Pour un lien externe, mettez `"external": true`. Les URLs de la version EN doivent pointer sous `/en/`.
|
||||||
|
|
||||||
### Ajouter une section de contact
|
### Modifier les informations de contact
|
||||||
|
|
||||||
Éditez `src/_data/contact.json` :
|
Les liens de contact sont regroupés dans `src/lib/i18n/fr/contact.json` (et `en/`). L'icône de chaque lien est déduite de son libellé (`whatsapp`, `telegram`, `discord`, `labola`, `kontak`/`mail`). Les emojis en tête des titres de groupes sont convertis en pictos du sprite (`pictos.ts`).
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"title": "🔗 Titre de la section",
|
|
||||||
"links": [
|
|
||||||
{
|
|
||||||
"text": "Nom du lien",
|
|
||||||
"url": "https://example.com"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Modifier les styles
|
## Modifier les styles
|
||||||
|
|
||||||
Les styles sont dans `src/assets/css/styles.css`.
|
- **Tokens (couleurs, typo, espacements, motion)** : `src/lib/styles/oki-tokens.css` — ne jamais recopier une valeur de la charte à la main, utiliser les variables.
|
||||||
|
- **Primitives partagées** (boutons, cartes, tags, flag-bar, gate reduced-motion) : `src/lib/styles/base.css`.
|
||||||
### Variables CSS disponibles
|
- **Styles d'une section** : dans le `<style>` scopé de son composant (`src/lib/components/sections/`).
|
||||||
|
- Durées et courbes d'animation : toujours via les tokens (`--dur-tanbou`, `--ease-ka`…), jamais en dur. On n'anime que `transform` et `opacity`.
|
||||||
```css
|
- Le thème clair est géré par surcharge des tokens (`html.light-theme`) — ne pas écrire de sélecteur `html.light-theme` par composant.
|
||||||
--bleu-ocean: #003566;
|
|
||||||
--jaune-soleil: #FFB700;
|
|
||||||
--rouge-resistance: #D62828;
|
|
||||||
--vert-terre: #2A9134;
|
|
||||||
--orange-communaute: #FB8500;
|
|
||||||
--noir-profond: #0A0908;
|
|
||||||
--blanc-lumiere: #F7F7F2;
|
|
||||||
--violet-fedi: #6364FF;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Créer une nouvelle page
|
## Créer une nouvelle page
|
||||||
|
|
||||||
1. Créez un fichier `.njk` dans `src/` :
|
1. Créez `src/routes/ma-page/+page.svelte` (et sa contrepartie `src/routes/en/my-page/+page.svelte` si bilingue) :
|
||||||
|
|
||||||
```njk
|
```svelte
|
||||||
---
|
<script lang="ts">
|
||||||
layout: layouts/base.njk
|
import Seo from '$lib/components/Seo.svelte';
|
||||||
title: Titre de la page
|
import { getBundle } from '$lib/i18n';
|
||||||
description: Description pour le SEO
|
|
||||||
---
|
|
||||||
|
|
||||||
<section>
|
const t = getBundle('fr');
|
||||||
<h1>{{ title }}</h1>
|
</script>
|
||||||
<p>Contenu de votre page</p>
|
|
||||||
|
<Seo title="Titre de la page" description="Description pour le SEO" path="/ma-page/" locale="fr" />
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Titre de la page</h1>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Ajoutez le lien dans `src/_data/navigation.json` si nécessaire
|
2. Ajoutez la correspondance FR↔EN dans `PATH_PAIRS` (`src/lib/i18n/index.ts`) pour l'hreflang et la bascule de langue.
|
||||||
|
3. Ajoutez l'URL dans `static/sitemap.xml` et, si nécessaire, dans `navigation.json`.
|
||||||
|
4. Vérifiez : `npm run check && npm run build`.
|
||||||
|
|
||||||
3. Reconstruisez le site : `npm run build`
|
## Modifier un composant
|
||||||
|
|
||||||
## Modifier un partial
|
- Sections de l'accueil : `src/lib/components/sections/`
|
||||||
|
- Layout : `Nav.svelte`, `Footer.svelte`, `Seo.svelte`
|
||||||
|
- Motion : `src/lib/components/motion/` (KineticText, ScrollProgressBar)
|
||||||
|
- Icônes : sprite `static/icons.svg` (set OKI, charte §4) et `src/lib/components/icons/BrandIcon.svelte` (logos de marques)
|
||||||
|
|
||||||
Les partials sont dans `src/_includes/partials/` :
|
Après toute modification d'un composant, passez l'autofixer (voir `svelte-5_code_writer.md`) :
|
||||||
|
|
||||||
- `nav.njk` - Navigation
|
```bash
|
||||||
- `hero.njk` - Section hero (accueil)
|
npx @sveltejs/mcp svelte-autofixer ./src/lib/components/sections/MonComposant.svelte
|
||||||
- `fediverse.njk` - Services Fédiverse
|
```
|
||||||
- `projects.njk` - Projets web
|
|
||||||
- `mission.njk` - Mission et valeurs
|
|
||||||
- `hosting.njk` - Services d'hébergement
|
|
||||||
- `support.njk` - Section dons
|
|
||||||
- `contact.njk` - Informations de contact
|
|
||||||
- `footer.njk` - Pied de page
|
|
||||||
|
|
||||||
## Workflow de développement
|
## Workflow de développement
|
||||||
|
|
||||||
@@ -133,16 +109,16 @@ Les partials sont dans `src/_includes/partials/` :
|
|||||||
git checkout -b feature/ma-fonctionnalite
|
git checkout -b feature/ma-fonctionnalite
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Faites vos modifications
|
2. Faites vos modifications.
|
||||||
|
|
||||||
3. Testez localement :
|
3. Testez localement :
|
||||||
```bash
|
```bash
|
||||||
npm start
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Buildez pour vérifier :
|
4. Vérifiez types, accessibilité et build :
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run check && npm run build && npm run preview
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Commitez vos changements :
|
5. Commitez vos changements :
|
||||||
@@ -151,29 +127,31 @@ Les partials sont dans `src/_includes/partials/` :
|
|||||||
git commit -m "Description de vos changements"
|
git commit -m "Description de vos changements"
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Poussez et créez une pull request :
|
6. Poussez et créez une merge request :
|
||||||
```bash
|
```bash
|
||||||
git push origin feature/ma-fonctionnalite
|
git push origin feature/ma-fonctionnalite
|
||||||
```
|
```
|
||||||
|
|
||||||
## Standards de code
|
## Standards de code
|
||||||
|
|
||||||
### HTML/Nunjucks
|
### Svelte / TypeScript
|
||||||
- Indentation : 4 espaces
|
- Svelte 5 runes uniquement (`$state`, `$derived`, `$props`…) — voir `svelte_core_bestpractices.md`
|
||||||
- Toujours fermer les balises
|
- TypeScript strict, props typées
|
||||||
- Attributs entre guillemets doubles
|
- `onclick={...}` (jamais `on:click`), blocs `{#each}` à clé unique
|
||||||
|
- `svelte-autofixer` propre sur tout composant modifié, `npm run check` à zéro warning
|
||||||
|
|
||||||
### CSS
|
### CSS
|
||||||
- Indentation : 4 espaces
|
- Indentation : tabulations (convention du projet)
|
||||||
- Utiliser les variables CSS quand possible
|
- Variables des tokens quand possible, mobile-first
|
||||||
- Mobile-first pour le responsive
|
- Angles nets : `border-radius` ≤ `var(--radius-md)` (6 px)
|
||||||
|
|
||||||
### JavaScript
|
### JavaScript (fichiers statiques)
|
||||||
- Indentation : 4 espaces
|
- Vanilla JS, commentaires en français, aucun script inline dans le HTML (CSP `script-src 'self'`)
|
||||||
- Vanilla JS (pas de framework)
|
|
||||||
- Commentaires en français
|
### Sécurité
|
||||||
- Gestion des erreurs
|
- Ne pas ajouter de ressource externe (font, CDN, script) : tout se self-héberge. Si une exception est indispensable, ajuster la CSP dans `static/.htaccess` **et** `static/_headers`.
|
||||||
|
- Conserver les headers de sécurité lors d'une modification du serveur.
|
||||||
|
|
||||||
## Questions ?
|
## Questions ?
|
||||||
|
|
||||||
Contactez-nous à kontak@o-k-i.net ou ouvrez une issue sur Codeberg.
|
Contactez-nous à kontak@o-k-i.net ou ouvrez une issue sur [Labola](https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/o-k-i.net).
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -1,167 +1,79 @@
|
|||||||
# 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
|
## À 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. Applis web sur mesure à tarifs solidaires pour associations, collectifs et organisations militantes.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Technologies
|
## Technologies
|
||||||
|
|
||||||
- **11ty (Eleventy)** - Générateur de site statique
|
- **SvelteKit 2 / Svelte 5** — composants en runes, TypeScript strict
|
||||||
- **Nunjucks** - Moteur de templates
|
- **adapter-static** — pré-rendu intégral, aucun serveur Node requis
|
||||||
- **CSS personnalisé** - Sans framework
|
- **vite-imagetools** — images AVIF/WebP responsives générées au build
|
||||||
- **JavaScript Vanilla** - Interactions et animations
|
- **vite-plugin-pwa** — service worker + shell hors-ligne
|
||||||
|
- **CSS vanilla** — tokens de la charte OKI (`src/lib/styles/oki-tokens.css`), aucun framework
|
||||||
## Structure du projet
|
- **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
|
||||||
```
|
|
||||||
o-k-i.net-V2/
|
|
||||||
├── src/
|
|
||||||
│ ├── _data/ # Données JSON (services, projets, valeurs, etc.)
|
|
||||||
│ ├── _includes/
|
|
||||||
│ │ ├── layouts/ # Layouts de base
|
|
||||||
│ │ └── partials/ # Composants réutilisables
|
|
||||||
│ ├── assets/
|
|
||||||
│ │ ├── css/ # Feuilles de style
|
|
||||||
│ │ ├── js/ # Scripts JavaScript
|
|
||||||
│ │ └── images/ # Images
|
|
||||||
│ └── index.njk # Page d'accueil
|
|
||||||
├── _site/ # Site généré (git ignoré)
|
|
||||||
├── eleventy.config.js # Configuration 11ty
|
|
||||||
└── package.json
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Cloner le dépôt
|
|
||||||
git clone https://codeberg.org/OKI/o-k-i.net-V2.git
|
|
||||||
cd o-k-i.net-V2
|
|
||||||
|
|
||||||
# Installer les dépendances
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Développement
|
## Développement
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Lancer le serveur de développement avec hot reload
|
npm install
|
||||||
npm start
|
npm run dev # serveur de développement
|
||||||
|
npm run build # build de production → build/
|
||||||
# Le site sera disponible sur http://localhost:8080
|
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
|
src/
|
||||||
npm run build
|
app.html # coquille HTML (thème anti-FOUC, fonts, manifest)
|
||||||
|
hooks.server.ts # attribut lang fr/en selon la route
|
||||||
# Les fichiers seront dans le dossier _site/
|
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/` :
|
## Sécurité & déploiement
|
||||||
|
|
||||||
- `site.json` - Informations générales du site
|
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**.
|
||||||
- `services.json` - Services Fédiverse (KUTE, GADE, BOKANTAJ, MIZIK)
|
|
||||||
- `projects.json` - Projets web (FEDIVERSE OKI, OKI.RE, NUVEL.NU, JWE.OVH, GONG.GP)
|
|
||||||
- `partners.json` - Partenaires (Joukawouvè, Aktivist Vybz #AKV)
|
|
||||||
- `values.json` - Valeurs de l'organisation
|
|
||||||
- `contact.json` - Informations de contact
|
|
||||||
- `navigation.json` - Liens de navigation
|
|
||||||
|
|
||||||
### Modifier les styles
|
## Charte et documents de référence
|
||||||
|
|
||||||
Les styles sont dans `src/assets/css/styles.css`. La charte graphique est basée sur les couleurs du logo officiel d'OKI :
|
Ces documents de travail sont **locaux** (non versionnés, voir `.gitignore`) :
|
||||||
|
|
||||||
```css
|
- **`charte-oki-design-system.md`** — marque canonique : couleurs, typographie, motion (cadences gwoka), iconographie SVG (zéro emoji en interface), voix. Prime pour tout ce qui est marque.
|
||||||
:root {
|
- **`recette-sveltekit-playbook-agent.md`** — méthode technique : principes, budgets performance, règles motion, DoD (sa version longue : `recette-sveltekit-15-sites-awwwards.md`).
|
||||||
/* Couleurs du logo OKI */
|
- **`AGENTS.md`** — point d'entrée pour les agents de code : impose l'usage de l'outillage Svelte MCP.
|
||||||
--or-oki: #E8A625; /* Orange/Or du logo */
|
- **`svelte-5_code_writer.md`** — commandes `@sveltejs/mcp` (doc Svelte 5 + `svelte-autofixer`, à passer sur tout composant modifié).
|
||||||
--noir-oki: #000000; /* Noir du logo */
|
- **`svelte_core_bestpractices.md`** — conventions runes Svelte 5 ($state/$derived/$effect, events, snippets, styles).
|
||||||
--vert-oki: #00A854; /* Vert émeraude du logo */
|
- **`DEPLOY.md`** — déploiement o2switch (Apache) et Cloudflare Pages (versionné, lui).
|
||||||
--rouge-oki: #ED1C4C; /* Rouge/Rose du logo */
|
|
||||||
--blanc-creme: #F7F7F2; /* Blanc crème */
|
|
||||||
|
|
||||||
/* Couleurs dérivées pour le design */
|
Thème sombre par défaut, thème clair en opt-in persisté (`localStorage['oki-theme']`).
|
||||||
--noir-profond: #1A1A1A;
|
|
||||||
--gris-sombre: #2A2A2A;
|
|
||||||
--or-clair: #F5BD4F;
|
|
||||||
--vert-clair: #00C569;
|
|
||||||
--rouge-clair: #FF2D5F;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Fonctionnalités
|
|
||||||
|
|
||||||
- ✅ Design responsive et moderne
|
|
||||||
- ✅ 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
|
|
||||||
|
|
||||||
### SEO & Référencement
|
|
||||||
- ✅ Meta tags Open Graph (Facebook, LinkedIn)
|
|
||||||
- ✅ Twitter Cards
|
|
||||||
- ✅ JSON-LD Structured Data (Schema.org)
|
|
||||||
- ✅ Sitemap.xml
|
|
||||||
- ✅ Robots.txt
|
|
||||||
- ✅ Balises canonical
|
|
||||||
- ✅ 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. **Fédiverse** - Services alternatifs (GADE, BOKANTAJ, MIZIK, KUTE)
|
|
||||||
4. **Projets Web** - Nos projets (FEDIVERSE OKI, OKI.RE, NUVEL.NU, JWE.OVH, GONG.GP)
|
|
||||||
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. **FAQ** - Questions fréquentes avec storytelling sur le nom "Ka"
|
|
||||||
10. **Engagement** - Formulaire de contact et appels à l'action
|
|
||||||
11. **Contact** - Liens vers réseaux sociaux et plateformes
|
|
||||||
|
|
||||||
## Contribution
|
|
||||||
|
|
||||||
Les contributions sont les bienvenues ! N'hésitez pas à ouvrir une issue ou une pull request sur [Codeberg](https://codeberg.org/OKI/o-k-i.net-V2).
|
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
Ce projet est sous licence **GNU Affero General Public License v3.0 (AGPL-V3)**.
|
AGPL-3.0 — voir `LICENSE`.
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
© 2024 ORGANISATION KA INTERNATIONALE
|
|
||||||
|
|
||||||
## Liens
|
|
||||||
|
|
||||||
- 🌐 [Site web](https://o-k-i.net)
|
|
||||||
- 📺 [GADE (PeerTube)](https://gade.o-k-i.net/)
|
|
||||||
- 🐦 [BOKANTAJ (Pleroma)](https://bokantaj.o-k-i.net/)
|
|
||||||
- 🎵 [MIZIK (Funkwhale)](https://mizik.o-k-i.net/library)
|
|
||||||
- 🎙️ [KUTE (Castopod)](https://kute.o-k-i.net/)
|
|
||||||
- 💻 [Codeberg](https://codeberg.org/OKI)
|
|
||||||
- 📧 kontak@o-k-i.net
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# Guide SEO - o-k-i.net
|
# Guide SEO - o-k-i.net
|
||||||
|
|
||||||
Ce document décrit l'optimisation SEO mise en place sur le site o-k-i.net V2.
|
Ce document décrit l'optimisation SEO du site o-k-i.net (V3, SvelteKit statique).
|
||||||
|
|
||||||
## 📋 Fichiers SEO
|
## 📋 Fichiers SEO
|
||||||
|
|
||||||
### 1. robots.txt
|
### 1. robots.txt
|
||||||
**Emplacement :** `src/robots.txt`
|
**Emplacement :** `static/robots.txt` (servi à `/robots.txt`)
|
||||||
|
|
||||||
Permet aux moteurs de recherche d'indexer tout le contenu du site et indique l'emplacement du sitemap.
|
Permet aux moteurs de recherche d'indexer tout le contenu du site et indique l'emplacement du sitemap.
|
||||||
|
|
||||||
@@ -16,173 +16,116 @@ Sitemap: https://o-k-i.net/sitemap.xml
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 2. sitemap.xml
|
### 2. sitemap.xml
|
||||||
**Emplacement :** `src/sitemap.xml`
|
**Emplacement :** `static/sitemap.xml` (servi à `/sitemap.xml`)
|
||||||
|
|
||||||
Liste toutes les pages du site avec leurs priorités et fréquences de mise à jour.
|
Liste les 8 pages du site avec priorités et fréquences :
|
||||||
|
|
||||||
**Pages incluses :**
|
- `/` et `/en/` (priorité 1.0, hebdomadaire)
|
||||||
- Page d'accueil (priorité 1.0, hebdomadaire)
|
- `/dons/` et `/en/donate/` (0.6, mensuelle)
|
||||||
- Mentions légales (priorité 0.3, mensuelle)
|
- `/presse/` et `/en/press/` (0.5, mensuelle)
|
||||||
|
- `/mentions-legales/` et `/en/legal-notice/` (0.3, mensuelle)
|
||||||
|
|
||||||
**À mettre à jour :** Lors de l'ajout de nouvelles pages.
|
**À mettre à jour :** lors de l'ajout de nouvelles pages.
|
||||||
|
|
||||||
## 🏷️ Meta Tags
|
## 🏷️ Meta Tags
|
||||||
|
|
||||||
### Meta Tags de base
|
Tous les meta tags sont centralisés dans le composant **`src/lib/components/Seo.svelte`**, alimenté par `src/lib/i18n/{locale}/site.json`. Chaque page l'invoque avec ses props :
|
||||||
Définis dans `src/_includes/layouts/base.njk` :
|
|
||||||
|
|
||||||
- **Title** : Dynamique selon la page
|
```svelte
|
||||||
- **Description** : Personnalisable par page via front matter
|
<Seo title="Titre de la page" description="Description spécifique" path="/ma-page/" locale="fr" />
|
||||||
- **Keywords** : OKI, Fédiverse, PeerTube, Pleroma, Funkwhale, Castopod, Guadeloupe, logiciel libre, GAFAM
|
```
|
||||||
- **Author** : Organisation Ka Internationale
|
|
||||||
- **Canonical URL** : Évite le contenu dupliqué
|
|
||||||
|
|
||||||
### Open Graph (Facebook, LinkedIn)
|
- **Title** : `Titre | ORGANISATION KA INTERNATIONALE` (home : `… | OKI`)
|
||||||
|
- **Description** : par page, sinon `site.description`
|
||||||
|
- **Keywords** : `site.keywords` · **Author** : `site.name`
|
||||||
|
- **Canonical URL** : `https://o-k-i.net{path}` — évite le contenu dupliqué
|
||||||
|
- **Hreflang** : `fr`, `en`, `x-default`, calculés via `alternatePath()` (`src/lib/i18n/index.ts`)
|
||||||
|
|
||||||
|
### Open Graph (10 balises)
|
||||||
```html
|
```html
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:url" content="https://o-k-i.net">
|
<meta property="og:url" content="https://o-k-i.net/…">
|
||||||
<meta property="og:title" content="...">
|
<meta property="og:title" content="…">
|
||||||
<meta property="og:description" content="...">
|
<meta property="og:description" content="…">
|
||||||
<meta property="og:image" content="https://o-k-i.net/assets/images/logo-512x512.png">
|
<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="…">
|
||||||
<meta property="og:locale" content="fr_FR">
|
<meta property="og:locale" content="fr_FR">
|
||||||
|
<meta property="og:site_name" content="ORGANISATION KA INTERNATIONALE">
|
||||||
```
|
```
|
||||||
|
|
||||||
### Twitter Cards
|
### Twitter Cards
|
||||||
```html
|
`summary_large_image` + url, title, description, image, image:alt — self-hébergées comme tout le reste.
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
|
||||||
<meta name="twitter:title" content="...">
|
|
||||||
<meta name="twitter:description" content="...">
|
|
||||||
<meta name="twitter:image" content="...">
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Structured Data (JSON-LD)
|
## 📊 Structured Data (JSON-LD)
|
||||||
|
|
||||||
### Organization Schema
|
Deux blocs injectés par `Seo.svelte` sur toutes les pages :
|
||||||
Données structurées conformes à Schema.org pour améliorer l'affichage dans les résultats de recherche.
|
|
||||||
|
|
||||||
**Type :** Organization
|
1. **Organization** : nom (OKI), URL, logo, description, adresse (36 Rue Lethière, Basse-Terre 97100, GP), contact (kontak@o-k-i.net), sameAs (niyaj, kute, labola, liberapay), `foundingDate: 2024`, `nonprofitStatus`.
|
||||||
|
2. **MusicGroup** : DJANGOKAM (genres Reggae/Zouk/Konpa/K-pop, djangokam.pawol.nu, membre OKI).
|
||||||
|
|
||||||
**Informations incluses :**
|
## 🔍 Attributs lang
|
||||||
- Nom : ORGANISATION KA INTERNATIONALE
|
|
||||||
- Nom alternatif : OKI
|
|
||||||
- URL : https://o-k-i.net
|
|
||||||
- Logo
|
|
||||||
- Description
|
|
||||||
- Adresse physique (Basse-Terre, Guadeloupe)
|
|
||||||
- Email de contact
|
|
||||||
- Liens vers les plateformes sociales (GADE, BOKANTAJ, MIZIK, KUTE, Codeberg, Liberapay)
|
|
||||||
- Statut : Organisation à but non lucratif
|
|
||||||
|
|
||||||
## 🔍 Optimisations par page
|
L'attribut `lang` du `<html>` est posé au prerender par `src/hooks.server.ts` (`fr` par défaut, `en` sous `/en/`), et `data-locale` pilote la redirection de langue (`static/lang-redirect.js`). Les microcopies en kréyòl (404, hors-ligne) portent `lang="gcf"`.
|
||||||
|
|
||||||
### Page d'accueil
|
|
||||||
- **Title :** "ORGANISATION KA INTERNATIONALE | OKI"
|
|
||||||
- **Description :** Reprend site.description
|
|
||||||
- **Keywords :** Fédiverse, logiciel libre, alternatives GAFAM, Guadeloupe
|
|
||||||
|
|
||||||
### Mentions légales
|
|
||||||
- **Title :** "Mentions Légales | ORGANISATION KA INTERNATIONALE"
|
|
||||||
- **Description :** "Mentions légales et informations juridiques de l'Organisation Ka Internationale (OKI) - Hébergement, RGPD, licence AGPL-V3"
|
|
||||||
|
|
||||||
## 🎯 Bonnes pratiques
|
## 🎯 Bonnes pratiques
|
||||||
|
|
||||||
### Ajouter une nouvelle page
|
### Ajouter une nouvelle page
|
||||||
1. Créer le fichier `.njk` dans `src/`
|
1. Créer `src/routes/ma-page/+page.svelte` avec `<Seo>` renseigné
|
||||||
2. Ajouter le front matter avec title et description personnalisés
|
2. Ajouter la paire FR↔EN dans `PATH_PAIRS` (`src/lib/i18n/index.ts`)
|
||||||
3. Mettre à jour `src/sitemap.xml`
|
3. Mettre à jour `static/sitemap.xml`
|
||||||
4. Ajouter un lien dans la navigation si nécessaire
|
4. Ajouter un lien dans `src/lib/i18n/*/navigation.json` si nécessaire
|
||||||
|
|
||||||
### Personnaliser les meta tags
|
|
||||||
Dans le front matter de la page :
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
layout: layouts/base.njk
|
|
||||||
title: Titre de la page
|
|
||||||
description: Description spécifique à cette page
|
|
||||||
---
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Tests et validation
|
## 🚀 Tests et validation
|
||||||
|
|
||||||
### Outils recommandés
|
### Outils recommandés
|
||||||
1. **Google Search Console** - Soumettre le sitemap
|
1. **Google Search Console** - Soumettre le sitemap
|
||||||
2. **Facebook Sharing Debugger** - Tester les meta Open Graph
|
2. **Facebook Sharing Debugger** - https://developers.facebook.com/tools/debug/
|
||||||
- URL : https://developers.facebook.com/tools/debug/
|
3. **Schema Markup Validator** - https://validator.schema.org/
|
||||||
3. **Twitter Card Validator** - Tester les Twitter Cards
|
4. **Google Rich Results Test** - https://search.google.com/test/rich-results
|
||||||
- URL : https://cards-dev.twitter.com/validator
|
|
||||||
4. **Schema Markup Validator** - Valider le JSON-LD
|
|
||||||
- URL : https://validator.schema.org/
|
|
||||||
5. **Google Rich Results Test** - Tester les données structurées
|
|
||||||
- URL : https://search.google.com/test/rich-results
|
|
||||||
|
|
||||||
### Commandes utiles
|
### Commandes utiles (sur `npm run preview`)
|
||||||
```bash
|
```bash
|
||||||
# Vérifier robots.txt
|
curl http://localhost:4173/robots.txt
|
||||||
curl http://localhost:8080/robots.txt
|
curl http://localhost:4173/sitemap.xml
|
||||||
|
curl http://localhost:4173/ | grep -oE '(property|name)="(og:|twitter:)[^"]*"'
|
||||||
# Vérifier sitemap.xml
|
|
||||||
curl http://localhost:8080/sitemap.xml
|
|
||||||
|
|
||||||
# Vérifier les meta tags
|
|
||||||
curl http://localhost:8080/ | grep -E "(og:|twitter:)"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📈 Suivi et amélioration
|
## 📈 Suivi et amélioration
|
||||||
|
|
||||||
### Métriques à surveiller
|
|
||||||
- Position dans les résultats de recherche Google
|
|
||||||
- Taux de clics (CTR) depuis les résultats de recherche
|
|
||||||
- Nombre de pages indexées
|
|
||||||
- Erreurs d'exploration
|
|
||||||
|
|
||||||
### Améliorations futures possibles
|
### Améliorations futures possibles
|
||||||
- [ ] Ajouter un blog/actualités avec articles optimisés SEO
|
- [ ] Pages dédiées pour chaque service Fédiverse
|
||||||
- [ ] Créer des pages dédiées pour chaque service Fédiverse
|
- [ ] FAQ avec Schema.org FAQPage
|
||||||
- [ ] Ajouter des FAQ avec Schema.org FAQPage
|
- [ ] Breadcrumbs avec données structurées
|
||||||
- [ ] Optimiser les images avec attributs alt descriptifs
|
- [ ] Hreflang pour d'autres langues (créole…)
|
||||||
- [ ] Ajouter breadcrumbs avec données structurées
|
|
||||||
- [ ] Implémenter AMP (Accelerated Mobile Pages) si nécessaire
|
|
||||||
- [ ] Ajouter hreflang pour versions multilingues (créole, anglais, etc.)
|
|
||||||
|
|
||||||
## 🌍 Mots-clés ciblés
|
## 🌍 Mots-clés ciblés
|
||||||
|
|
||||||
### Primaires
|
### Primaires
|
||||||
- Organisation Ka Internationale
|
- Organisation Ka Internationale · OKI · Fédiverse Guadeloupe · Alternatives GAFAM
|
||||||
- OKI
|
|
||||||
- Fédiverse Guadeloupe
|
|
||||||
- PeerTube Guadeloupe
|
|
||||||
- Alternatives GAFAM
|
|
||||||
|
|
||||||
### Secondaires
|
### Secondaires
|
||||||
- Pleroma
|
- PeerTube / Mastodon / Nextcloud / Castopod Guadeloupe · Logiciel libre Antilles · Médias libres Caraïbes · Hébergement éthique · Langues créoles
|
||||||
- Funkwhale
|
|
||||||
- Castopod
|
|
||||||
- Logiciel libre Antilles
|
|
||||||
- Médias libres Caraïbes
|
|
||||||
- Hébergement éthique
|
|
||||||
- Langues créoles
|
|
||||||
- Réseau social libre
|
|
||||||
|
|
||||||
### Longue traîne
|
### Longue traîne
|
||||||
- "alternative libre à YouTube Guadeloupe"
|
- "alternative libre à YouTube Guadeloupe" · "hébergement PeerTube Martinique" · "réseau social décentralisé Antilles" · "promotion langues créoles numérique"
|
||||||
- "hébergement PeerTube Martinique"
|
|
||||||
- "réseau social décentralisé Antilles"
|
|
||||||
- "promotion langues créoles numérique"
|
|
||||||
|
|
||||||
## 📝 Checklist SEO
|
## 📝 Checklist SEO
|
||||||
|
|
||||||
- [x] Robots.txt configuré
|
- [x] Robots.txt configuré
|
||||||
- [x] Sitemap.xml créé et soumis
|
- [x] Sitemap.xml (8 URLs, dates à jour)
|
||||||
- [x] Meta description sur toutes les pages
|
- [x] Meta description sur toutes les pages
|
||||||
- [x] Balises title optimisées
|
- [x] Balises title optimisées
|
||||||
- [x] URL canoniques
|
- [x] URL canoniques
|
||||||
- [x] Open Graph tags
|
- [x] Open Graph (10 balises, og:image self-hébergée)
|
||||||
- [x] Twitter Cards
|
- [x] Twitter Cards
|
||||||
- [x] JSON-LD Organization
|
- [x] JSON-LD Organization + MusicGroup
|
||||||
- [x] Logo haute résolution (512x512)
|
- [x] Logo haute résolution (512×512)
|
||||||
- [ ] Images avec attributs alt
|
- [x] Images avec attributs alt
|
||||||
- [ ] Performance optimale (Lighthouse score)
|
- [x] Performance (Lighthouse mobile ≥ 90, mesuré à 91)
|
||||||
- [ ] Mobile-friendly (déjà responsive)
|
- [x] Mobile-friendly (responsive, vérifié Lighthouse)
|
||||||
- [ ] HTTPS activé (à configurer en production)
|
- [x] HTTPS activé et HSTS configuré
|
||||||
|
- [x] Hreflang FR/EN/x-default
|
||||||
- [ ] Google Search Console configuré
|
- [ ] Google Search Console configuré
|
||||||
- [ ] Analytics installé (optionnel, respectueux RGPD)
|
- [ ] Analytics installé (optionnel, respectueux RGPD — rester sans tiers)
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
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"]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Copy assets folders to output
|
|
||||||
eleventyConfig.addPassthroughCopy("src/assets");
|
|
||||||
|
|
||||||
// Copy SEO files
|
|
||||||
eleventyConfig.addPassthroughCopy("src/robots.txt");
|
|
||||||
eleventyConfig.addPassthroughCopy("src/sitemap.xml");
|
|
||||||
|
|
||||||
// Watch CSS files for changes
|
|
||||||
eleventyConfig.addWatchTarget("src/assets/css/");
|
|
||||||
|
|
||||||
// Watch JS files for changes
|
|
||||||
eleventyConfig.addWatchTarget("src/assets/js/");
|
|
||||||
|
|
||||||
// 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"
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,16 +1,31 @@
|
|||||||
{
|
{
|
||||||
"name": "o-k-i.net-v2",
|
"name": "o-k-i.net",
|
||||||
"version": "1.0.0",
|
"version": "3.0.0",
|
||||||
"description": "Applications web sur mesure pour associations et organisations militantes",
|
"private": true,
|
||||||
|
"license": "AGPL-3.0-only",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "eleventy --serve",
|
"dev": "vite dev",
|
||||||
"build": "eleventy",
|
"build": "vite build",
|
||||||
"clean": "rm -rf _site"
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||||
},
|
},
|
||||||
"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": {
|
"devDependencies": {
|
||||||
"@11ty/eleventy": "^3.1.2"
|
"@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",
|
||||||
|
"lenis": "^1.3.25"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
[
|
|
||||||
{ "text": "Solutions", "url": "#solutions" },
|
|
||||||
{ "text": "Our projects", "url": "#projets" },
|
|
||||||
{ "text": "DJANGOKAM", "url": "#djangokam" },
|
|
||||||
{ "text": "Services", "url": "#hebergement" },
|
|
||||||
{ "text": "Our work", "url": "#clients" },
|
|
||||||
{ "text": "Our instances", "url": "#services-libres" },
|
|
||||||
{ "text": "FAQ", "url": "#faq" },
|
|
||||||
{ "text": "Donate", "url": "#dons" },
|
|
||||||
{ "text": "Contact", "url": "#contact" },
|
|
||||||
{ "text": "Labola", "url": "https://labola.o-k-i.net/", "external": true }
|
|
||||||
]
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"section_title": "Our",
|
|
||||||
"section_title_accent": "Partners",
|
|
||||||
"section_subtitle": "They share our values and help enrich our free digital ecosystem",
|
|
||||||
"link_text": "Discover on GADE",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"name": "Joukawouvè",
|
|
||||||
"logo": "/assets/images/joukawouve.png",
|
|
||||||
"description": "Sharing Guadeloupean and international news.",
|
|
||||||
"url": "https://gade.o-k-i.net/accounts/joukawouve"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "cyber-mawonaj",
|
|
||||||
"logo": "/assets/images/cyber_mawonaj.jpg",
|
|
||||||
"description": "Towards digital mawonaj - Winning your freedom also runs through the digital world.",
|
|
||||||
"url": "https://gade.o-k-i.net/c/cyber_mawonaj/videos"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Aktivist Vybz #AKV",
|
|
||||||
"logo": "/assets/images/akv.png",
|
|
||||||
"description": "Committed selectors and DJs, with content focused on activism and cultural expression.",
|
|
||||||
"url": "https://gade.o-k-i.net/accounts/aktivist_vybz_akv"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[
|
|
||||||
{ "text": "Solutions", "url": "#solutions" },
|
|
||||||
{ "text": "Nos projets", "url": "#projets" },
|
|
||||||
{ "text": "DJANGOKAM", "url": "#djangokam" },
|
|
||||||
{ "text": "Services", "url": "#hebergement" },
|
|
||||||
{ "text": "Réalisations", "url": "#clients" },
|
|
||||||
{ "text": "Nos instances", "url": "#services-libres" },
|
|
||||||
{ "text": "FAQ", "url": "#faq" },
|
|
||||||
{ "text": "Dons", "url": "#dons" },
|
|
||||||
{ "text": "Contact", "url": "#contact" },
|
|
||||||
{ "text": "Labola", "url": "https://labola.o-k-i.net/", "external": true }
|
|
||||||
]
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="{{ 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="https://o-k-i.net/assets/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 {{ 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="https://o-k-i.net/assets/images/logo-512x512.png">
|
|
||||||
<meta name="twitter:image:alt" content="Logo {{ t.site.name }}">
|
|
||||||
|
|
||||||
<!-- Favicons -->
|
|
||||||
<link rel="icon" type="image/x-icon" href="/assets/images/favicon.ico">
|
|
||||||
<link rel="icon" type="image/png" sizes="512x512" href="/assets/images/logo-512x512.png">
|
|
||||||
<link rel="apple-touch-icon" href="/assets/images/logo-512x512.png">
|
|
||||||
|
|
||||||
<!-- 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": "https://o-k-i.net/assets/images/logo-512x512.png",
|
|
||||||
"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>
|
|
||||||
(function() {
|
|
||||||
// Récupérer le thème stocké ou détecter le thème système
|
|
||||||
const storedTheme = localStorage.getItem('oki-theme');
|
|
||||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
||||||
|
|
||||||
let theme = storedTheme;
|
|
||||||
|
|
||||||
// Si pas de préférence stockée, utiliser le thème système
|
|
||||||
if (!storedTheme) {
|
|
||||||
theme = systemPrefersDark ? 'dark' : 'light';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Appliquer le thème immédiatement
|
|
||||||
if (theme === 'light') {
|
|
||||||
document.documentElement.classList.add('light-theme');
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
// Redirection automatique selon la langue du navigateur, sauf choix explicite déjà mémorisé
|
|
||||||
var pref = localStorage.getItem('oki-lang-pref');
|
|
||||||
if (pref) return;
|
|
||||||
|
|
||||||
var browserLang = (navigator.language || navigator.userLanguage || '').toLowerCase();
|
|
||||||
var wantsEnglish = browserLang.indexOf('en') === 0;
|
|
||||||
var currentLocale = '{{ locale }}';
|
|
||||||
|
|
||||||
if (wantsEnglish && currentLocale !== 'en') {
|
|
||||||
window.location.replace('/en/' + window.location.hash);
|
|
||||||
} else if (!wantsEnglish && currentLocale === 'en') {
|
|
||||||
window.location.replace('/' + window.location.hash);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{% include "partials/nav.njk" %}
|
|
||||||
|
|
||||||
{{ content | safe }}
|
|
||||||
|
|
||||||
{% include "partials/footer.njk" %}
|
|
||||||
|
|
||||||
<script src="/assets/js/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -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 }}" 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>
|
|
||||||
@@ -1,22 +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-grid">
|
|
||||||
{% for section in t.contact.sections %}
|
|
||||||
<div class="contact-card">
|
|
||||||
<h3>{{ section.title }}</h3>
|
|
||||||
<div class="contact-links">
|
|
||||||
{% for link in section.links %}
|
|
||||||
<a href="{{ link.url }}"{% if link.url.startsWith('http') %} target="_blank"{% endif %}>
|
|
||||||
{{ link.text }}
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@@ -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" 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 }}" 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 }}" 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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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" onclick="this.parentElement.classList.toggle('active')">
|
|
||||||
<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>
|
|
||||||
@@ -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">{{ service.icon }}</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>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<footer>
|
|
||||||
<div class="flag-bar"></div>
|
|
||||||
<div class="footer-content">
|
|
||||||
<p class="footer-name">
|
|
||||||
{{ t.site.name }}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{ t.footer.tagline_line }}
|
|
||||||
</p>
|
|
||||||
<p class="footer-meta">
|
|
||||||
{{ t.site.copyright }} | {{ t.site.tagline }}
|
|
||||||
</p>
|
|
||||||
<p class="footer-legal">
|
|
||||||
<a href="/mentions-legales/">
|
|
||||||
{{ t.footer.legal_link }}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<nav>
|
|
||||||
<div class="nav-container">
|
|
||||||
<a href="/" class="logo">
|
|
||||||
<img src="/assets/images/logo-512x512.png" alt="{{ t.site.shortName }} Logo" class="logo-image">
|
|
||||||
<span>{{ t.site.shortName }}</span>
|
|
||||||
</a>
|
|
||||||
<ul class="nav-links">
|
|
||||||
{% for item in t.navigation %}
|
|
||||||
<li>
|
|
||||||
<a href="{{ item.url }}"{% if item.external %} target="_blank"{% endif %}>
|
|
||||||
{{ item.text }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% if locale == 'en' %}
|
|
||||||
<a href="/" class="lang-switch" onclick="localStorage.setItem('oki-lang-pref','fr')">{{ t.ui.lang_switch_label }}</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="/en/" class="lang-switch" onclick="localStorage.setItem('oki-lang-pref','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>
|
|
||||||
@@ -1,23 +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 %}
|
|
||||||
<a href="{{ partner.url }}" target="_blank" rel="noopener noreferrer" class="partner-card">
|
|
||||||
<div class="partner-logo-container">
|
|
||||||
<img src="{{ partner.logo }}" alt="Logo {{ partner.name }}" class="partner-logo">
|
|
||||||
</div>
|
|
||||||
<h3>{{ partner.name }}</h3>
|
|
||||||
<p class="partner-description">{{ partner.description }}</p>
|
|
||||||
<span class="partner-link">
|
|
||||||
{{ t.partners.link_text }} →
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 onclick="handleCustomDonation('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 onclick="handleCustomDonation('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>
|
|
||||||
@@ -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 {};
|
||||||
@@ -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>
|
||||||
@@ -1,316 +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 le menu mobile si ouvert
|
|
||||||
const navLinks = document.querySelector('.nav-links');
|
|
||||||
if (navLinks && navLinks.style.display === 'flex') {
|
|
||||||
const background = newTheme === 'light' ? 'rgba(255, 248, 231, 0.98)' : 'rgba(0, 0, 0, 0.98)';
|
|
||||||
navLinks.style.background = background;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Smooth scrolling
|
|
||||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
||||||
anchor.addEventListener('click', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const target = document.querySelector(this.getAttribute('href'));
|
|
||||||
if (target) {
|
|
||||||
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;
|
|
||||||
if (menuOpen) {
|
|
||||||
// Déterminer le fond selon le thème actuel
|
|
||||||
const isLightTheme = document.documentElement.classList.contains('light-theme');
|
|
||||||
const background = isLightTheme ? 'rgba(255, 248, 231, 0.98)' : 'rgba(0, 0, 0, 0.98)';
|
|
||||||
|
|
||||||
navLinks.style.display = 'flex';
|
|
||||||
navLinks.style.position = 'absolute';
|
|
||||||
navLinks.style.top = '100%';
|
|
||||||
navLinks.style.left = '0';
|
|
||||||
navLinks.style.right = '0';
|
|
||||||
navLinks.style.background = background;
|
|
||||||
navLinks.style.flexDirection = 'column';
|
|
||||||
navLinks.style.padding = '1rem';
|
|
||||||
navLinks.style.borderTop = '2px solid #E8A625';
|
|
||||||
|
|
||||||
// Animate menu toggle
|
|
||||||
menuToggle.children[0].style.transform = 'rotate(45deg) translateY(8px)';
|
|
||||||
menuToggle.children[1].style.opacity = '0';
|
|
||||||
menuToggle.children[2].style.transform = 'rotate(-45deg) translateY(-8px)';
|
|
||||||
} else {
|
|
||||||
navLinks.style.display = 'none';
|
|
||||||
|
|
||||||
// Reset menu toggle
|
|
||||||
menuToggle.children[0].style.transform = '';
|
|
||||||
menuToggle.children[1].style.opacity = '1';
|
|
||||||
menuToggle.children[2].style.transform = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close mobile menu when clicking outside
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
if (menuOpen && !menuToggle.contains(e.target) && !navLinks.contains(e.target)) {
|
|
||||||
menuOpen = false;
|
|
||||||
navLinks.style.display = 'none';
|
|
||||||
menuToggle.children[0].style.transform = '';
|
|
||||||
menuToggle.children[1].style.opacity = '1';
|
|
||||||
menuToggle.children[2].style.transform = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
// Dynamic typing effect for hero subtitle
|
|
||||||
const subtitle = document.querySelector('.hero-subtitle');
|
|
||||||
if (subtitle) {
|
|
||||||
const text = subtitle.textContent;
|
|
||||||
subtitle.textContent = '';
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
function typeWriter() {
|
|
||||||
if (index < text.length) {
|
|
||||||
subtitle.textContent += text.charAt(index);
|
|
||||||
index++;
|
|
||||||
setTimeout(typeWriter, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(typeWriter, 800);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"locale": "en"
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
layout: layouts/base.njk
|
|
||||||
---
|
|
||||||
|
|
||||||
{% include "partials/hero.njk" %}
|
|
||||||
{% include "partials/publics.njk" %}
|
|
||||||
{% include "partials/projects.njk" %}
|
|
||||||
{% include "partials/djangokam.njk" %}
|
|
||||||
{% include "partials/mission.njk" %}
|
|
||||||
{% include "partials/hosting.njk" %}
|
|
||||||
{% include "partials/clients.njk" %}
|
|
||||||
{% include "partials/fediverse.njk" %}
|
|
||||||
{% include "partials/support.njk" %}
|
|
||||||
{% include "partials/faq.njk" %}
|
|
||||||
{% include "partials/engagement.njk" %}
|
|
||||||
{% include "partials/contact.njk" %}
|
|
||||||
@@ -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)
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
layout: layouts/base.njk
|
|
||||||
---
|
|
||||||
|
|
||||||
{% include "partials/hero.njk" %}
|
|
||||||
{% include "partials/publics.njk" %}
|
|
||||||
{% include "partials/projects.njk" %}
|
|
||||||
{% include "partials/djangokam.njk" %}
|
|
||||||
{% include "partials/mission.njk" %}
|
|
||||||
{% include "partials/hosting.njk" %}
|
|
||||||
{% include "partials/clients.njk" %}
|
|
||||||
{% include "partials/fediverse.njk" %}
|
|
||||||
{% include "partials/support.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 |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 45 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 |
@@ -0,0 +1,111 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
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 legalPath = $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={resolve(legalPath)}>{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>
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
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);
|
||||||
|
let activeSection = $state('');
|
||||||
|
|
||||||
|
const altLocale: Locale = $derived(locale === 'fr' ? 'en' : 'fr');
|
||||||
|
const altHref = $derived(alternatePath(page.url.pathname, altLocale));
|
||||||
|
|
||||||
|
/** Extrait l'ancre d'une URL de nav (`/#solutions` → `solutions`). */
|
||||||
|
function anchorOf(url: string): string {
|
||||||
|
const i = url.indexOf('#');
|
||||||
|
return i >= 0 ? url.slice(i + 1) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section courante surlignée : la bande médiane du viewport désigne l'actif
|
||||||
|
onMount(() => {
|
||||||
|
const sections = [...document.querySelectorAll('section[id]')];
|
||||||
|
if (sections.length === 0) return;
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting) activeSection = entry.target.id;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ rootMargin: '-40% 0px -55% 0px' }
|
||||||
|
);
|
||||||
|
for (const section of sections) io.observe(section);
|
||||||
|
return () => io.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
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={resolve(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>
|
||||||
|
{#if child.external}
|
||||||
|
<a
|
||||||
|
href={child.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="external noopener noreferrer"
|
||||||
|
onclick={closeMenu}>{child.text}</a
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<a href={resolve(child.url)} onclick={closeMenu}>{child.text}</a>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{:else}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={resolve(item.url)}
|
||||||
|
class:active={anchorOf(item.url) !== '' && anchorOf(item.url) === activeSection}
|
||||||
|
aria-current={anchorOf(item.url) === activeSection ? 'true' : undefined}
|
||||||
|
onclick={closeMenu}>{item.text}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<a href={resolve(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 {
|
||||||
|
position: relative;
|
||||||
|
color: var(--blanc-creme);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Souligné or qui se dessine depuis la gauche (hover + section active) */
|
||||||
|
.nav-links a::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: -6px;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--or-oki);
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: left center;
|
||||||
|
transition: transform var(--dur-tanbou) var(--ease-syncope);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a:hover,
|
||||||
|
.nav-links a.active {
|
||||||
|
color: var(--or-oki);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a:hover::after,
|
||||||
|
.nav-links a.active::after {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<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'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// {@html} sur contenu first-party (JSON-LD généré au build, aucune donnée utilisateur).
|
||||||
|
// `<\/script>` : échappement requis pour ne pas fermer le bloc <script> du composant.
|
||||||
|
const orgJsonLdScript = $derived(`<script type="application/ld+json">${orgJsonLd}<\/script>`);
|
||||||
|
const djangokamJsonLdScript = `<script type="application/ld+json">${djangokamJsonLd}<\/script>`;
|
||||||
|
</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 orgJsonLdScript}
|
||||||
|
{@html djangokamJsonLdScript}
|
||||||
|
</svelte:head>
|
||||||
@@ -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,53 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { reveal } from '$lib/motion/reveal';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FlagChip — ornement de marque au-dessus des titres de section.
|
||||||
|
* Version réduite de la flag-bar (charte : ≤ 1 flag-bar pleine largeur
|
||||||
|
* par écran) : 4 segments francs noir/or/vert/rouge, 56 × 6 px.
|
||||||
|
* Se dessine en scaleX 0 → 1 (origin left) à l'entrée du viewport via
|
||||||
|
* use:reveal — état caché uniquement si html.js ET motion autorisée.
|
||||||
|
* Les titres de section de l'accueil sont tous centrés : le chip aussi.
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="flag-chip" aria-hidden="true" use:reveal></span>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.flag-chip {
|
||||||
|
display: block;
|
||||||
|
width: 56px;
|
||||||
|
height: 6px;
|
||||||
|
margin: 0 auto var(--space-2);
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
#0d0d0d 0 25%,
|
||||||
|
var(--or-oki) 25% 50%,
|
||||||
|
var(--vert-oki) 50% 75%,
|
||||||
|
var(--rouge-oki) 75% 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.light-theme) .flag-chip {
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
#000 0 25%,
|
||||||
|
var(--or-oki) 25% 50%,
|
||||||
|
var(--vert-oki) 50% 75%,
|
||||||
|
var(--rouge-oki) 75% 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dessin scaleX : remplace la translation du reveal de base.
|
||||||
|
La transition (dur-mesure, ease-ka, délai --d) vient de base.css. */
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
:global(html.js) .flag-chip {
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: left center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.js) .flag-chip:global(.in) {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IntroLoader — cérémonie d'entrée (P5 : chaque état est un moment designé).
|
||||||
|
*
|
||||||
|
* Visibilité pilotée par la classe `intro-pending` posée par theme.js
|
||||||
|
* (externe, CSP-safe) : première visite de la session, JS actif, pas de
|
||||||
|
* reduced-motion. Sans ces conditions, l'overlay est `display: none` et
|
||||||
|
* la page est complète en dessous. La chorégraphie est 100 % CSS
|
||||||
|
* (keyframes à délais syncopés) ; le JS ne gère que la sortie et le skip.
|
||||||
|
*/
|
||||||
|
const DURATION_MS = 1500; // début de la sortie (keyframes CSS)
|
||||||
|
const EXIT_MS = 480; // durée de la sortie (--dur-mesure)
|
||||||
|
|
||||||
|
function finish() {
|
||||||
|
sessionStorage.setItem('oki-intro', '1');
|
||||||
|
document.documentElement.classList.remove('intro-pending');
|
||||||
|
}
|
||||||
|
|
||||||
|
function skip() {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') skip();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!document.documentElement.classList.contains('intro-pending')) return;
|
||||||
|
document.addEventListener('keydown', onKeydown);
|
||||||
|
// Fin de séquence : la sortie CSS est déjà jouée, on nettoie l'état.
|
||||||
|
const timer = setTimeout(finish, DURATION_MS + EXIT_MS + 100);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
document.removeEventListener('keydown', onKeydown);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="intro-loader" role="dialog" aria-label="Introduction">
|
||||||
|
<div class="intro-inner">
|
||||||
|
<div class="intro-flags" aria-hidden="true">
|
||||||
|
<span class="seg-noir"></span>
|
||||||
|
<span class="seg-or"></span>
|
||||||
|
<span class="seg-vert"></span>
|
||||||
|
<span class="seg-rouge"></span>
|
||||||
|
</div>
|
||||||
|
<img src="/images/logo-512x512.png" alt="" class="intro-logo" width="96" height="96" />
|
||||||
|
<p class="intro-title" aria-hidden="true">
|
||||||
|
<span class="intro-word w1">ORGANISATION KA</span>
|
||||||
|
<span class="intro-word w2">INTERNATIONALE</span>
|
||||||
|
</p>
|
||||||
|
<p class="intro-ka" lang="gcf">An nou maché</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="intro-skip" onclick={skip}>Passer l'intro</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.intro-loader {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chorégraphie : uniquement quand theme.js a posé intro-pending */
|
||||||
|
:global(html.intro-pending) .intro-loader {
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2000;
|
||||||
|
background: var(--noir-oki);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
animation: intro-out var(--dur-mesure) var(--ease-ka) 1.5s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-flags {
|
||||||
|
display: flex;
|
||||||
|
width: min(420px, 70vw);
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-flags span {
|
||||||
|
flex: 1;
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: left center;
|
||||||
|
animation: flag-draw 300ms var(--ease-ka) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-flags span:nth-child(2) {
|
||||||
|
animation-delay: 100ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-flags span:nth-child(3) {
|
||||||
|
animation-delay: 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-flags span:nth-child(4) {
|
||||||
|
animation-delay: 300ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seg-noir {
|
||||||
|
background: #0d0d0d;
|
||||||
|
outline: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seg-or {
|
||||||
|
background: var(--or-oki);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seg-vert {
|
||||||
|
background: var(--vert-oki);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seg-rouge {
|
||||||
|
background: var(--rouge-oki);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-logo {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
animation: rise var(--dur-mesure) var(--ease-ka) 450ms forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
line-height: 1.05;
|
||||||
|
font-size: clamp(1.6rem, 5vw, 3rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-word {
|
||||||
|
display: block;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(110%);
|
||||||
|
animation: rise var(--dur-mesure) var(--ease-ka) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w1 {
|
||||||
|
animation-delay: 700ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w2 {
|
||||||
|
color: var(--or-oki);
|
||||||
|
animation-delay: 820ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-ka {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
opacity: 0;
|
||||||
|
animation: fade var(--dur-mesure) var(--ease-ka) 1100ms forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-skip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--space-4);
|
||||||
|
right: var(--space-4);
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--muted);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.4rem 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
animation: fade 300ms var(--ease-ka) 800ms forwards;
|
||||||
|
transition:
|
||||||
|
color var(--dur-tanbou) var(--ease-ka),
|
||||||
|
border-color var(--dur-tanbou) var(--ease-ka);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-skip:hover {
|
||||||
|
color: var(--or-oki);
|
||||||
|
border-color: var(--or-oki);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flag-draw {
|
||||||
|
to {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rise {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes intro-out {
|
||||||
|
to {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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} {/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,104 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Bundle } from '$lib/i18n';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marquee kréyòl — bande cinétique typographique.
|
||||||
|
* Défilement 100 % CSS (contenu dupliqué aria-hidden pour la boucle),
|
||||||
|
* pause au hover/focus, coupée en reduced-motion (gate globale base.css
|
||||||
|
* + règle dédiée ci-dessous).
|
||||||
|
*/
|
||||||
|
let { t }: { t: Bundle } = $props();
|
||||||
|
|
||||||
|
const items: string[] = $derived([
|
||||||
|
t.site.tagline,
|
||||||
|
...t.values.map((v: { title: string }) => v.title),
|
||||||
|
'ORGANISATION KA INTERNATIONALE'
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="marquee" aria-hidden="true">
|
||||||
|
<div class="marquee-track">
|
||||||
|
{#each [0, 1] as copy (copy)}
|
||||||
|
<div class="marquee-group">
|
||||||
|
{#each items as item, i (i)}
|
||||||
|
<span class="marquee-item">{item}</span>
|
||||||
|
<svg class="icon marquee-star" aria-hidden="true"><use href="/icons.svg#zetwal" /></svg>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.marquee {
|
||||||
|
overflow: hidden;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: var(--noir-profond);
|
||||||
|
padding: var(--space-2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-track {
|
||||||
|
display: flex;
|
||||||
|
width: max-content;
|
||||||
|
animation: marquee-scroll 36s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee:hover .marquee-track,
|
||||||
|
.marquee:focus-within .marquee-track {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-item {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
font-size: clamp(1.1rem, 2.5vw, 1.6rem);
|
||||||
|
color: var(--muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 var(--space-3);
|
||||||
|
transition: color var(--dur-tanbou) var(--ease-ka);
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee:hover .marquee-item {
|
||||||
|
color: var(--blanc-creme);
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-item:first-child {
|
||||||
|
color: var(--or-oki);
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-star {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
color: var(--or-oki);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes marquee-scroll {
|
||||||
|
to {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.marquee-track {
|
||||||
|
animation: none;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pas de doublon à l'arrêt */
|
||||||
|
.marquee-group:last-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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>
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import FlagChip from '$lib/components/motion/FlagChip.svelte';
|
||||||
|
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||||
|
import ResponsiveImage from '$lib/components/ResponsiveImage.svelte';
|
||||||
|
import type { Bundle, Locale } from '$lib/i18n';
|
||||||
|
import { reveal } from '$lib/motion/reveal';
|
||||||
|
|
||||||
|
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">
|
||||||
|
<FlagChip />
|
||||||
|
<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, i (client.domain)}
|
||||||
|
<a
|
||||||
|
href={client.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="external noopener noreferrer"
|
||||||
|
class="client-card"
|
||||||
|
use:reveal={i}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
// {@html} sur contenu first-party (JSON i18n du dépôt), aucune donnée utilisateur.
|
||||||
|
import BrandIcon from '$lib/components/icons/BrandIcon.svelte';
|
||||||
|
import FlagChip from '$lib/components/motion/FlagChip.svelte';
|
||||||
|
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||||
|
import type { Bundle, Locale } from '$lib/i18n';
|
||||||
|
import { reveal } from '$lib/motion/reveal';
|
||||||
|
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">
|
||||||
|
<FlagChip />
|
||||||
|
<h2 class="section-title">
|
||||||
|
<KineticText as="span" text={t.contact.title} />
|
||||||
|
</h2>
|
||||||
|
<!-- Contenu éditorial du bundle i18n (lien mailto balisé). -->
|
||||||
|
<p class="contact-lede" use:reveal={0}>{@html t.contact.lede}</p>
|
||||||
|
</div>
|
||||||
|
<div class="contact-socials container">
|
||||||
|
{#each t.contact.sections as group, i (group.title)}
|
||||||
|
{@const title = splitLeadingEmoji(group.title)}
|
||||||
|
<div class="contact-socials-group" use:reveal={i}>
|
||||||
|
<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, j (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}
|
||||||
|
use:reveal={j + 1}
|
||||||
|
>
|
||||||
|
{#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,406 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
// {@html} sur contenu first-party (JSON i18n du dépôt), aucune donnée utilisateur.
|
||||||
|
import FlagChip from '$lib/components/motion/FlagChip.svelte';
|
||||||
|
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||||
|
import ResponsiveImage from '$lib/components/ResponsiveImage.svelte';
|
||||||
|
import type { Bundle, Locale } from '$lib/i18n';
|
||||||
|
import { reveal } from '$lib/motion/reveal';
|
||||||
|
import { tilt } from '$lib/motion/tilt';
|
||||||
|
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>
|
||||||
|
<FlagChip />
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="djangokam-portrait-wrap" use:reveal={0}>
|
||||||
|
<ResponsiveImage
|
||||||
|
picture={portrait}
|
||||||
|
alt="Portrait officiel de DJANGOKAM"
|
||||||
|
sizes="220px"
|
||||||
|
class="djangokam-portrait"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Masque vertical : la citation monte depuis le bas (dur-phrase) -->
|
||||||
|
<div class="djangokam-highlight-mask" use:reveal={1}>
|
||||||
|
<blockquote class="djangokam-highlight">
|
||||||
|
{t.djangokam.message_cle}
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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" use:reveal={1} use:tilt>
|
||||||
|
<a href={t.djangokam.duo.url} target="_blank" rel="external 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, i (fait.chiffre)}
|
||||||
|
<div class="djangokam-fact-card" use:reveal={i}>
|
||||||
|
<div class="djangokam-fact-icon reveal-pop" use:reveal={i + 1}>
|
||||||
|
<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, i (clip.titre)}
|
||||||
|
<figure class="djangokam-clip" use:reveal={i} use:tilt>
|
||||||
|
<a href={clip.url} target="_blank" rel="external 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="external 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wrapper-masque de la citation : reprend les marges du blockquote
|
||||||
|
(le masque doit rogner la translation, pas laisser les marges visibles) */
|
||||||
|
.djangokam-highlight-mask {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.djangokam-highlight {
|
||||||
|
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;
|
||||||
|
position: relative;
|
||||||
|
transition: transform var(--dur-tanbou) var(--ease-ka);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reflet or discret qui pivote avec le tilt (--ry hérité de la figure) */
|
||||||
|
.djangokam-duo a::after,
|
||||||
|
.djangokam-clip a::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: linear-gradient(
|
||||||
|
calc(105deg + var(--ry, 0deg) * 4),
|
||||||
|
transparent 35%,
|
||||||
|
color-mix(in srgb, var(--or-oki) 70%, transparent) 50%,
|
||||||
|
transparent 65%
|
||||||
|
);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--dur-tanbou) var(--ease-ka);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.djangokam-duo a:hover::after,
|
||||||
|
.djangokam-clip a:hover::after {
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Chorégraphie d'entrée (même gate que le reveal global : html.js
|
||||||
|
+ motion autorisée — sinon tout est visible sans transition) ── */
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
/* Pochettes : wipe clip-path (masque DOM) + tilt 3D piloté par use:tilt.
|
||||||
|
La transformation de base du reveal est neutralisée : seul le
|
||||||
|
clip-path révèle, la perspective reste disponible en permanence. */
|
||||||
|
:global(html.js) .djangokam-duo,
|
||||||
|
:global(html.js) .djangokam-clip {
|
||||||
|
opacity: 1;
|
||||||
|
transform: perspective(700px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
|
||||||
|
clip-path: inset(0 100% 0 0);
|
||||||
|
transition:
|
||||||
|
clip-path var(--dur-mesure) var(--ease-ka) var(--d, 0ms),
|
||||||
|
transform var(--dur-tanbou) var(--ease-ka);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.js) .djangokam-duo:global(.in),
|
||||||
|
:global(html.js) .djangokam-clip:global(.in) {
|
||||||
|
clip-path: inset(0 0 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blockquote lore : le masque ne bouge pas, la citation monte */
|
||||||
|
:global(html.js) .djangokam-highlight-mask {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.js) .djangokam-highlight-mask .djangokam-highlight {
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: transform var(--dur-phrase) var(--ease-ka) var(--d, 0ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.js) .djangokam-highlight-mask:global(.in) .djangokam-highlight {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,101 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import FlagChip from '$lib/components/motion/FlagChip.svelte';
|
||||||
|
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||||
|
import type { Bundle, Locale } from '$lib/i18n';
|
||||||
|
import { reveal } from '$lib/motion/reveal';
|
||||||
|
import { pictoFor } from './pictos';
|
||||||
|
|
||||||
|
let { t }: { t: Bundle; locale?: Locale } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section id="engagement" class="section">
|
||||||
|
<div class="container">
|
||||||
|
<FlagChip />
|
||||||
|
<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, i (card.title)}
|
||||||
|
<div class="engagement-card" use:reveal={i}>
|
||||||
|
<div class="engagement-icon reveal-pop" use:reveal={i + 1}>
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import FlagChip from '$lib/components/motion/FlagChip.svelte';
|
||||||
|
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||||
|
import type { Bundle, Locale } from '$lib/i18n';
|
||||||
|
import { reveal } from '$lib/motion/reveal';
|
||||||
|
|
||||||
|
let { t }: { t: Bundle; locale?: Locale } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section id="faq" class="section">
|
||||||
|
<div class="container">
|
||||||
|
<FlagChip />
|
||||||
|
<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, i (item.question)}
|
||||||
|
<details class="faq-item" use:reveal={i}>
|
||||||
|
<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,45 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import FlagChip from '$lib/components/motion/FlagChip.svelte';
|
||||||
|
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">
|
||||||
|
<FlagChip />
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
<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-bg" aria-hidden="true">
|
||||||
|
<div class="hero-watermark"></div>
|
||||||
|
<div class="hero-stripes s1"></div>
|
||||||
|
<div class="hero-stripes s2"></div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-content container">
|
||||||
|
<h1 class="hero-title">
|
||||||
|
<span class="mask"><span class="w w1">ORGANISATION</span></span>
|
||||||
|
<span class="mask"><span class="w w2">KA</span></span>
|
||||||
|
</h1>
|
||||||
|
<h2 class="hero-subtitle">
|
||||||
|
<span class="mask"><span class="w w3">INTERNATIONALE</span></span>
|
||||||
|
</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-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-watermark {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: -5%;
|
||||||
|
width: 55%;
|
||||||
|
height: 100%;
|
||||||
|
background: url('/images/logo-512x512.png') center / contain no-repeat;
|
||||||
|
opacity: 0.05;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filets drapeau en diagonale, très basse opacité (profondeur) */
|
||||||
|
.hero-stripes {
|
||||||
|
position: absolute;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent 0 10%,
|
||||||
|
var(--or-oki) 10% 40%,
|
||||||
|
var(--vert-oki) 40% 65%,
|
||||||
|
var(--rouge-oki) 65% 85%,
|
||||||
|
transparent 85% 100%
|
||||||
|
);
|
||||||
|
transform: rotate(-12deg);
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stripes.s1 {
|
||||||
|
top: 22%;
|
||||||
|
height: 3px;
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stripes.s2 {
|
||||||
|
bottom: 18%;
|
||||||
|
height: 2px;
|
||||||
|
opacity: 0.07;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
width: 100%;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(3rem, 9vw, 7rem);
|
||||||
|
font-weight: 900;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
line-height: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: clamp(1.6rem, 4.5vw, 3rem);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
color: var(--or-oki);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reveal masqué au chargement (pattern titrage des sites primés) :
|
||||||
|
uniquement si JS actif et motion autorisée — sinon texte visible */
|
||||||
|
.mask {
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w {
|
||||||
|
display: inline-block;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
:global(html.js) .w {
|
||||||
|
transform: translateY(110%);
|
||||||
|
animation: word-rise var(--dur-phrase) var(--ease-ka) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.js) .w1 {
|
||||||
|
animation-delay: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.js) .w2 {
|
||||||
|
animation-delay: 270ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.js) .w3 {
|
||||||
|
animation-delay: 510ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Si la cérémonie d'intro joue, le titrage attend sa sortie */
|
||||||
|
:global(html.js.intro-pending) .w1 {
|
||||||
|
animation-delay: 1400ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.js.intro-pending) .w2 {
|
||||||
|
animation-delay: 1520ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.js.intro-pending) .w3 {
|
||||||
|
animation-delay: 1760ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes word-rise {
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
color: var(--muted);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parallaxe et sortie scrubée : scroll-driven animations natives,
|
||||||
|
enhancement progressif — sans support, le hero reste statique */
|
||||||
|
@supports (animation-timeline: scroll()) {
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.hero-watermark {
|
||||||
|
animation: wm-drift linear both;
|
||||||
|
animation-timeline: scroll(root);
|
||||||
|
animation-range: 0 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stripes.s1 {
|
||||||
|
animation: s1-drift linear both;
|
||||||
|
animation-timeline: scroll(root);
|
||||||
|
animation-range: 0 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stripes.s2 {
|
||||||
|
animation: s2-drift linear both;
|
||||||
|
animation-timeline: scroll(root);
|
||||||
|
animation-range: 0 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
animation: hero-exit linear both;
|
||||||
|
animation-timeline: scroll(root);
|
||||||
|
animation-range: 0 75vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wm-drift {
|
||||||
|
to {
|
||||||
|
transform: translateY(14%) scale(1.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes s1-drift {
|
||||||
|
to {
|
||||||
|
transform: rotate(-12deg) translateX(-8%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes s2-drift {
|
||||||
|
to {
|
||||||
|
transform: rotate(-12deg) translateX(6%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hero-exit {
|
||||||
|
to {
|
||||||
|
transform: translateY(-8vh);
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.cta-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-buttons .btn {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import FlagChip from '$lib/components/motion/FlagChip.svelte';
|
||||||
|
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||||
|
import type { Bundle, Locale } from '$lib/i18n';
|
||||||
|
import { reveal } from '$lib/motion/reveal';
|
||||||
|
|
||||||
|
let { t }: { t: Bundle; locale?: Locale } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section id="hebergement" class="section">
|
||||||
|
<div class="container">
|
||||||
|
<FlagChip />
|
||||||
|
<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" use:reveal={0}>
|
||||||
|
<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, i (card.title)}
|
||||||
|
<div class="offer-card{card.modifier ? ` offer-card--${card.modifier}` : ''}" use:reveal={i}>
|
||||||
|
<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" use:reveal={3}>
|
||||||
|
<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" use:reveal={4}>
|
||||||
|
<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,541 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Bundle, Locale } from '$lib/i18n';
|
||||||
|
import { reveal } from '$lib/motion/reveal';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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="external 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="external 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="external 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="external 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, i (item.name)}
|
||||||
|
<li class="card vf-card" use:reveal={i}>
|
||||||
|
<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="external 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,101 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import FlagChip from '$lib/components/motion/FlagChip.svelte';
|
||||||
|
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||||
|
import type { Bundle, Locale } from '$lib/i18n';
|
||||||
|
import { reveal } from '$lib/motion/reveal';
|
||||||
|
import { pictoFor } from './pictos';
|
||||||
|
|
||||||
|
let { t }: { t: Bundle; locale?: Locale } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section id="mission" class="section">
|
||||||
|
<div class="container">
|
||||||
|
<FlagChip />
|
||||||
|
<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, i (value.title)}
|
||||||
|
<div class="value-card" use:reveal={i}>
|
||||||
|
<div class="value-icon reveal-pop" use:reveal={i + 1}>
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import FlagChip from '$lib/components/motion/FlagChip.svelte';
|
||||||
|
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||||
|
import ResponsiveImage from '$lib/components/ResponsiveImage.svelte';
|
||||||
|
import type { Bundle, Locale } from '$lib/i18n';
|
||||||
|
import { reveal } from '$lib/motion/reveal';
|
||||||
|
|
||||||
|
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">
|
||||||
|
<FlagChip />
|
||||||
|
<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, i (partner.name)}
|
||||||
|
<div class="partner-card" use:reveal={i}>
|
||||||
|
<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="external 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>
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import FlagChip from '$lib/components/motion/FlagChip.svelte';
|
||||||
|
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||||
|
import type { Bundle, Locale } from '$lib/i18n';
|
||||||
|
import { reveal } from '$lib/motion/reveal';
|
||||||
|
import { pictoFor } from './pictos';
|
||||||
|
|
||||||
|
let { t }: { t: Bundle; locale?: Locale } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section id="projets" class="section">
|
||||||
|
<div class="container">
|
||||||
|
<FlagChip />
|
||||||
|
<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, i (item.name)}
|
||||||
|
<a
|
||||||
|
href={item.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="external noopener noreferrer"
|
||||||
|
class="project-card"
|
||||||
|
use:reveal={i}
|
||||||
|
>
|
||||||
|
<div class="project-icon reveal-pop" use:reveal={i + 1}>
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import FlagChip from '$lib/components/motion/FlagChip.svelte';
|
||||||
|
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||||
|
import type { Bundle, Locale } from '$lib/i18n';
|
||||||
|
import { reveal } from '$lib/motion/reveal';
|
||||||
|
import { pictoFor } from './pictos';
|
||||||
|
|
||||||
|
let { t }: { t: Bundle; locale?: Locale } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section id="solutions" class="section">
|
||||||
|
<div class="container">
|
||||||
|
<FlagChip />
|
||||||
|
<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, i (item.title)}
|
||||||
|
<div class="public-card" use:reveal={i}>
|
||||||
|
<div class="public-icon reveal-pop" use:reveal={i + 1}>
|
||||||
|
<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" use:reveal={3}>
|
||||||
|
<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>
|
||||||
@@ -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+1F3FB–U+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)
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -8,21 +8,21 @@
|
|||||||
"name": "KA UBUNTU",
|
"name": "KA UBUNTU",
|
||||||
"domain": "kaubuntu.com",
|
"domain": "kaubuntu.com",
|
||||||
"url": "https://kaubuntu.com",
|
"url": "https://kaubuntu.com",
|
||||||
"logo": "https://kaubuntu.com/wp-content/uploads/2026/04/logo-ka-ubuntu.png",
|
"logo": "/assets/images/clients/kaubuntu-com.png",
|
||||||
"description": "Pan-Africanist and pro-independence political movement from Réunion. Fighting for the self-determination of Réunion and African unity. \"I am because we are.\""
|
"description": "Pan-Africanist and pro-independence political movement from Réunion. Fighting for the self-determination of Réunion and African unity. \"I am because we are.\""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "KA UBUNTU — Media hub",
|
"name": "KA UBUNTU — Media hub",
|
||||||
"domain": "kaubuntu.re",
|
"domain": "kaubuntu.re",
|
||||||
"url": "https://kaubuntu.re",
|
"url": "https://kaubuntu.re",
|
||||||
"logo": "https://kaubuntu.re/img/logo.png",
|
"logo": "/assets/images/clients/kaubuntu-re.png",
|
||||||
"description": "Centralised media aggregator for Ka Ubuntu: YouTube videos, articles, Instagram and TikTok brought together on a single page. Progressive web application (PWA)."
|
"description": "Centralised media aggregator for Ka Ubuntu: YouTube videos, articles, Instagram and TikTok brought together on a single page. Progressive web application (PWA)."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "KBM – Ka Pou Nou Bwè é Manjé",
|
"name": "KBM – Ka Pou Nou Bwè é Manjé",
|
||||||
"domain": "kbm.gp",
|
"domain": "kbm.gp",
|
||||||
"url": "https://kbm.gp",
|
"url": "https://kbm.gp",
|
||||||
"logo": "https://kbm.gp/images/logo-1024.webp",
|
"logo": "/assets/images/clients/kbm.gp.webp",
|
||||||
"description": "Movement for the emancipation and food sovereignty of the Guadeloupean people. Community radio Radyo Bòkaz, podcasts, lakou and local farming."
|
"description": "Movement for the emancipation and food sovereignty of the Guadeloupean people. Community radio Radyo Bòkaz, podcasts, lakou and local farming."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -9,23 +9,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "💬 Social networks",
|
"title": "📢 PAWOL.NU channels",
|
||||||
"links": [
|
"links": [
|
||||||
{ "text": "Telegram", "url": "https://t.me/OrganisationKA" },
|
{ "text": "WhatsApp channel", "url": "https://whatsapp.com/channel/0029VbCv7N9AjPXTxygrRu1w" },
|
||||||
{ "text": "X (Twitter)", "url": "https://x.com/OrganisationKA" }
|
{ "text": "Telegram channel", "url": "https://t.me/OrganisationKA" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "💬 Community",
|
"title": "💬 Community",
|
||||||
"links": [
|
"links": [
|
||||||
{ "text": "Discord", "url": "https://discord.gg/a2r9NYNM" },
|
{ "text": "Discord", "url": "https://discord.gg/DPvUCA8dD" }
|
||||||
{ "text": "Revolt", "url": "https://rvlt.gg/e1zJrg3p" }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "💻 Code & Development",
|
"title": "💻 Code & Development",
|
||||||
"links": [
|
"links": [
|
||||||
{ "text": "Labola (Gitea)", "url": "https://labola.o-k-i.net/" }
|
{ "text": "Labola (Gitea)", "url": "https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[
|
||||||
|
{ "text": "Solutions", "url": "/en/#solutions" },
|
||||||
|
{ "text": "Our projects", "url": "/en/#projets" },
|
||||||
|
{ "text": "Partners", "url": "/en/#partenaires" },
|
||||||
|
{ "text": "DJANGOKAM", "url": "/en/#djangokam" },
|
||||||
|
{ "text": "Services", "url": "/en/#hebergement" },
|
||||||
|
{ "text": "Donate", "url": "/en/donate/" },
|
||||||
|
{
|
||||||
|
"text": "Resources",
|
||||||
|
"children": [
|
||||||
|
{ "text": "Our work", "url": "/en/#clients" },
|
||||||
|
{ "text": "Our instances", "url": "/en/#services-libres" },
|
||||||
|
{ "text": "FAQ", "url": "/en/#faq" },
|
||||||
|
{ "text": "Press", "url": "/en/press/" },
|
||||||
|
{ "text": "Labola", "url": "https://labola.o-k-i.net/", "external": true }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "text": "Contact", "url": "/en/#contact" }
|
||||||
|
]
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"section_title": "Our",
|
||||||
|
"section_title_accent": "Partners",
|
||||||
|
"section_subtitle": "They share our values and help enrich our free digital ecosystem",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "Joukawouvè",
|
||||||
|
"logo": "/assets/images/joukawouve.png",
|
||||||
|
"description": "Sharing Guadeloupean and international news.",
|
||||||
|
"links": [
|
||||||
|
{ "url": "https://kute.o-k-i.net/@joukawouve", "text": "Discover on KUTE" },
|
||||||
|
{ "url": "https://bokante.o-k-i.net/@joukawouve", "text": "Discover on BOKANTE" },
|
||||||
|
{ "url": "https://gade.o-k-i.net/accounts/joukawouve", "text": "Discover on GADE" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Aktivist Vybz #AKV",
|
||||||
|
"logo": "/assets/images/akv.png",
|
||||||
|
"description": "Committed selectors and DJs, with content focused on activism and cultural expression.",
|
||||||
|
"links": [
|
||||||
|
{ "url": "https://mizik.o-k-i.net/channels/aktivistvybzakv", "text": "Discover on MIZIK" },
|
||||||
|
{ "url": "https://gade.o-k-i.net/c/aktivist_vybz-1_channel/videos", "text": "Discover on GADE" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cyber-mawonaj",
|
||||||
|
"logo": "/assets/images/cyber_mawonaj.jpg",
|
||||||
|
"description": "Towards digital mawonaj - Winning your freedom also runs through the digital world.",
|
||||||
|
"links": [
|
||||||
|
{ "url": "https://bokante.o-k-i.net/@cybermawonaj", "text": "Discover on BOKANTE" },
|
||||||
|
{ "url": "https://gade.o-k-i.net/c/cyber.mawonaj/videos", "text": "Discover on GADE" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"title": "Press",
|
||||||
|
"title_accent": "& Media",
|
||||||
|
"lede": "Press resources for DJANGOKAM, the first Caribbean AI artist, produced by ORGANISATION KA INTERNATIONALE (OKI). HD visuals and listening links, ready to use for newsrooms and radio programmers.",
|
||||||
|
"facts_title": "The essentials",
|
||||||
|
"facts": [
|
||||||
|
{ "label": "Artist", "value": "DJANGOKAM, the first Caribbean AI artist, joined by NYYOKA in duets" },
|
||||||
|
{ "label": "Producer", "value": "ORGANISATION KA INTERNATIONALE (OKI), a cultural association based in Basse-Terre, Guadeloupe" },
|
||||||
|
{ "label": "Discography", "value": "12 releases available on every platform, listed on MusicBrainz" },
|
||||||
|
{ "label": "Official videos", "value": "\"I Want You Girl\" and \"Ish Mwen KUTE\"" },
|
||||||
|
{ "label": "Language", "value": "Sings in KA, a language known as \"Creole\", with lyrics and translations on PAWÒL-NU" }
|
||||||
|
],
|
||||||
|
"visuals_title": "HD visuals",
|
||||||
|
"visuals_intro": "Free to use for editorial coverage of DJANGOKAM. Click a cover for a preview, or download the high-resolution PDF.",
|
||||||
|
"visuals": [
|
||||||
|
{ "image": "/assets/images/djangokam-she-told-me.webp", "caption": "\"She Told Me (Fanm-la di mwen)\" — DJANGOKAM x NYYOKA", "pdf": "DJANGOKAM_She_Told_Me_HD.pdf" },
|
||||||
|
{ "image": "/assets/images/djangokam-i-want-you-girl.webp", "caption": "\"I Want You Girl\"", "pdf": "DJANGOKAM_I_Want_You_Girl_HD.pdf" },
|
||||||
|
{ "image": "/assets/images/djangokam-ish-mwen-kute.jpg", "caption": "\"Ish Mwen KUTE\"", "pdf": "DJANGOKAM_Ish_Mwen_Kute_HD.pdf" }
|
||||||
|
],
|
||||||
|
"visual_pdf_label": "HD PDF",
|
||||||
|
"visuals_pdf_label": "Download all 3 HD visuals (PDF)",
|
||||||
|
"visuals_pdf_file": "DJANGOKAM_Visuels_HD.pdf",
|
||||||
|
"links_title": "Listen & watch",
|
||||||
|
"links": [
|
||||||
|
{ "text": "Listen on PAWÒL-NU (lyrics & translations)", "url": "https://pawol.nu" },
|
||||||
|
{ "text": "DJANGOKAM on every platform", "url": "https://djangokam.pawol.nu" },
|
||||||
|
{ "text": "NYYOKA on every platform", "url": "https://nyyoka.pawol.nu" },
|
||||||
|
{ "text": "Videos on GADE (PeerTube, sovereign hosting)", "url": "https://gade.o-k-i.net" }
|
||||||
|
],
|
||||||
|
"contact_title": "Press contact",
|
||||||
|
"contact_text": "Localised press kit, additional visuals, broadcast WAV or interview requests: get in touch, we reply quickly.",
|
||||||
|
"contact_email": "kontak@o-k-i.net"
|
||||||
|
}
|
||||||
@@ -16,6 +16,13 @@
|
|||||||
"domain": "gong.gp",
|
"domain": "gong.gp",
|
||||||
"description": "Historical documents from the GONG",
|
"description": "Historical documents from the GONG",
|
||||||
"url": "https://gong.gp/"
|
"url": "https://gong.gp/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FEDIVERSE.OKI",
|
||||||
|
"icon": "🎬",
|
||||||
|
"domain": "fediverse.o-k-i.net",
|
||||||
|
"description": "Independent multimedia platform: PeerTube videos, Mastodon feed, Castopod podcasts and WordPress articles",
|
||||||
|
"url": "https://fediverse.o-k-i.net/"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"name": "BOKANTE",
|
"name": "BOKANTE",
|
||||||
"icon": "🐘",
|
|
||||||
"platform": "Mastodon",
|
"platform": "Mastodon",
|
||||||
"description": "Our free social network instance. Exchange, share and connect without surveillance or algorithms, far from X or Facebook.",
|
"description": "Our free social network instance. Exchange, share and connect without surveillance or algorithms, far from X or Facebook.",
|
||||||
"url": "https://bokante.o-k-i.net",
|
"url": "https://bokante.o-k-i.net",
|
||||||
@@ -13,7 +12,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "GADE",
|
"name": "GADE",
|
||||||
"icon": "📺",
|
|
||||||
"platform": "PeerTube",
|
"platform": "PeerTube",
|
||||||
"description": "Our free and sovereign video instance. Stream your videos without algorithms or censorship, dedicated to Afro-diasporic languages, cultures and productions.",
|
"description": "Our free and sovereign video instance. Stream your videos without algorithms or censorship, dedicated to Afro-diasporic languages, cultures and productions.",
|
||||||
"url": "https://gade.o-k-i.net",
|
"url": "https://gade.o-k-i.net",
|
||||||
@@ -21,7 +19,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "NIYAJ",
|
"name": "NIYAJ",
|
||||||
"icon": "☁️",
|
|
||||||
"platform": "Nextcloud",
|
"platform": "Nextcloud",
|
||||||
"description": "Your personal cloud, far from Google's servers. Files, calendar, contacts, collaborative documents, everything Google Drive offers, but your data stays yours.",
|
"description": "Your personal cloud, far from Google's servers. Files, calendar, contacts, collaborative documents, everything Google Drive offers, but your data stays yours.",
|
||||||
"url": "https://niyaj.o-k-i.net",
|
"url": "https://niyaj.o-k-i.net",
|
||||||
@@ -29,7 +26,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "KUTE",
|
"name": "KUTE",
|
||||||
"icon": "🎙️",
|
|
||||||
"platform": "Castopod",
|
"platform": "Castopod",
|
||||||
"description": "Our free podcasting instance. Create, host and distribute your podcasts fully autonomously, without algorithms or censorship.",
|
"description": "Our free podcasting instance. Create, host and distribute your podcasts fully autonomously, without algorithms or censorship.",
|
||||||
"url": "https://kute.o-k-i.net/",
|
"url": "https://kute.o-k-i.net/",
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
"description": "We build custom web applications for grassroots associations, collectives and activist organisations, with rates that match your means.",
|
"description": "We build custom web applications for grassroots associations, collectives and activist organisations, with rates that match your means.",
|
||||||
"url": "https://o-k-i.net",
|
"url": "https://o-k-i.net",
|
||||||
"email": "kontak@o-k-i.net",
|
"email": "kontak@o-k-i.net",
|
||||||
"copyright": "© 2025 ORGANISATION KA INTERNATIONALE",
|
|
||||||
"tagline": "Digital tools in service of your struggles",
|
"tagline": "Digital tools in service of your struggles",
|
||||||
"og_tagline": "Taking back control of our internet",
|
"og_tagline": "Taking back control of our internet",
|
||||||
"keywords": "OKI, ORGANISATION KA INTERNATIONALE, custom web application, activist associations, web development, digital sovereignty, solidarity pricing, free software, Guadeloupe, Martinique, Caribbean, ethical digital, Castopod, KUTE, ethical hosting, GAFAM alternative",
|
"keywords": "OKI, ORGANISATION KA INTERNATIONALE, custom web application, activist associations, web development, digital sovereignty, solidarity pricing, free software, Guadeloupe, Martinique, Caribbean, ethical digital, Castopod, KUTE, ethical hosting, GAFAM alternative",
|
||||||
@@ -8,21 +8,21 @@
|
|||||||
"name": "KA UBUNTU",
|
"name": "KA UBUNTU",
|
||||||
"domain": "kaubuntu.com",
|
"domain": "kaubuntu.com",
|
||||||
"url": "https://kaubuntu.com",
|
"url": "https://kaubuntu.com",
|
||||||
"logo": "https://kaubuntu.com/wp-content/uploads/2026/04/logo-ka-ubuntu.png",
|
"logo": "/assets/images/clients/kaubuntu-com.png",
|
||||||
"description": "Mouvement politique panafricaniste et indépendantiste réunionnais. Lutte pour l'autodétermination de La Réunion et l'unité africaine. \"Je suis parce que nous sommes.\""
|
"description": "Mouvement politique panafricaniste et indépendantiste réunionnais. Lutte pour l'autodétermination de La Réunion et l'unité africaine. \"Je suis parce que nous sommes.\""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "KA UBUNTU — Hub multimédia",
|
"name": "KA UBUNTU — Hub multimédia",
|
||||||
"domain": "kaubuntu.re",
|
"domain": "kaubuntu.re",
|
||||||
"url": "https://kaubuntu.re",
|
"url": "https://kaubuntu.re",
|
||||||
"logo": "https://kaubuntu.re/img/logo.png",
|
"logo": "/assets/images/clients/kaubuntu-re.png",
|
||||||
"description": "Agrégateur multimédia centralisé pour Ka Ubuntu : vidéos YouTube, articles, Instagram et TikTok réunis en une seule page. Application web progressive (PWA)."
|
"description": "Agrégateur multimédia centralisé pour Ka Ubuntu : vidéos YouTube, articles, Instagram et TikTok réunis en une seule page. Application web progressive (PWA)."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "KBM – Ka Pou Nou Bwè é Manjé",
|
"name": "KBM – Ka Pou Nou Bwè é Manjé",
|
||||||
"domain": "kbm.gp",
|
"domain": "kbm.gp",
|
||||||
"url": "https://kbm.gp",
|
"url": "https://kbm.gp",
|
||||||
"logo": "https://kbm.gp/images/logo-1024.webp",
|
"logo": "/assets/images/clients/kbm.gp.webp",
|
||||||
"description": "Mouvement pour l'émancipation et la souveraineté alimentaire du peuple guadeloupéen. Radio communautaire Radyo Bòkaz, podcasts, lakou et agriculture locale."
|
"description": "Mouvement pour l'émancipation et la souveraineté alimentaire du peuple guadeloupéen. Radio communautaire Radyo Bòkaz, podcasts, lakou et agriculture locale."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -9,23 +9,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "💬 Réseaux sociaux",
|
"title": "📢 Chaînes PAWOL.NU",
|
||||||
"links": [
|
"links": [
|
||||||
{ "text": "Telegram", "url": "https://t.me/OrganisationKA" },
|
{ "text": "WhatsApp channel", "url": "https://whatsapp.com/channel/0029VbCv7N9AjPXTxygrRu1w" },
|
||||||
{ "text": "X (Twitter)", "url": "https://x.com/OrganisationKA" }
|
{ "text": "Telegram channel", "url": "https://t.me/OrganisationKA" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "💬 Communauté",
|
"title": "💬 Communauté",
|
||||||
"links": [
|
"links": [
|
||||||
{ "text": "Discord", "url": "https://discord.gg/a2r9NYNM" },
|
{ "text": "Discord", "url": "https://discord.gg/DPvUCA8dD" }
|
||||||
{ "text": "Revolt", "url": "https://rvlt.gg/e1zJrg3p" }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "💻 Code & Développement",
|
"title": "💻 Code & Développement",
|
||||||
"links": [
|
"links": [
|
||||||
{ "text": "Labola (Gitea)", "url": "https://labola.o-k-i.net/" }
|
{ "text": "Labola (Gitea)", "url": "https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"message_cle": "DJANGOKAM n'est pas simplement un artiste virtuel. Il est le premier projet musical porté par ORGANISATION KA INTERNATIONALE (OKI), une association culturelle guadeloupéenne qui explore les possibilités offertes par l'intelligence artificielle pour promouvoir les langues dites « créoles », les cultures et la création artistique Caribéennes. À travers ce projet, OKI démontre que les technologies émergentes peuvent être mises au service du patrimoine culturel plutôt que de s'y opposer.",
|
"message_cle": "DJANGOKAM n'est pas simplement un artiste virtuel. Il est le premier projet musical porté par ORGANISATION KA INTERNATIONALE (OKI), une association culturelle guadeloupéenne qui explore les possibilités offertes par l'intelligence artificielle pour promouvoir les langues dites « créoles », les cultures et la création artistique Caribéennes. À travers ce projet, OKI démontre que les technologies émergentes peuvent être mises au service du patrimoine culturel plutôt que de s'y opposer.",
|
||||||
"histoire": [
|
"histoire": [
|
||||||
"DJANGOKAM est né il y a 29 ans à Wanakaera - le nom que les Kalinas, premiers habitants de l'île, donnaient à ce qu'on appelle aujourd'hui la Martinique. Il porte le reggae, le zouk et le konpa de scène en scène, jusqu'à la K-pop qu'il chante en coréen. La langue de son cœur est le KA, celle qu'on appelle à tort « créole » ; son arme, la musique. Ses scènes préférées : Wanakaera et Karukera, l'île de son grand-père - la Guadeloupe.",
|
"DJANGOKAM est né il y a 29 ans à Wanakaera - le nom que les Kalinas, premiers habitants de l'île, donnaient à ce qu'on appelle aujourd'hui la Martinique. Il porte le reggae, le zouk et le konpa de scène en scène, jusqu'à la K-pop qu'il chante en coréen. La langue de son cœur est le KA, celle qu'on appelle à tort « créole » ; son arme, la musique. Ses scènes préférées : Wanakaera et Karukera, l'île de son grand-père - la Guadeloupe.",
|
||||||
"À ses côtés, NYYOKA, artiste IA Caribéenne du même collectif, mène le même combat depuis la mer - en solo ou en duo, comme sur le single « She Told Me (Fanm-la di mwen) »."
|
"À ses côtés, NYYOKA, artiste IA Caribéenne du même collectif, mène le même combat - en solo ou en duo, comme sur le single « She Told Me (Fanm-la di mwen) »."
|
||||||
],
|
],
|
||||||
"duo": {
|
"duo": {
|
||||||
"image": "/assets/images/djangokam-she-told-me.webp",
|
"image": "/assets/images/djangokam-she-told-me.webp",
|
||||||