Compare commits

...

21 Commits

Author SHA1 Message Date
Yvv
088333e4d4 Monnaie libre : réécriture dans la voix du livre, liens forums, purge co-créer
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- monnaie-libre.yml : texte entièrement réécrit (cavalerie, double symétrie, DU,
  toile de confiance, mesure de gratitude) + 3 nouveaux liens (duniter.org,
  forum.duniter.org, forum.monnaie-libre.fr)
- Suppression de "co-créer/co-créée/co-création" dans tous les fichiers :
  economique.yml, home.yml, authentification-wot.yml, cloud-libre.yml,
  content/book/05-trm.md, content/book/11-annexes.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 22:51:28 +01:00
Yvv
07449de187 Pages détail numérique : sommaire flottant, nav ctx, shadoks geek, contenu enrichi
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- [slug].vue : sommaire sticky (overflow:clip sur parent), prev/next en haut, 6 shadoks geek (pinguin+USB, web-of-trust, rubber-duck, caféine, debugger loupe, rack serveur)
- Nouveaux types de sections : territoire (bouquet sweethomeCloud, 2 modèles éco, tableau matériel dépliable), projet (carte gestation)
- cloud-libre.yml : section sweethomeCloud complète avec infra 50 000 hab. (~2€/an/hab)
- authentification-wot.yml : trustWallet, correction WoT Duniter (Ed25519+Scrypt, sigQty=5, stepMax=3), DID/VC standards
- logiciel-libre.yml : carte projet wishBounty
- home.yml + numerique.yml : cloud-libre → sweethomeCloud, description RGPD/local-first
- AxisBlock.vue : bulles de présentation inline dans les cards (plus de tooltip absolu)
- Analytics : useTracking.ts (Umami), docker-compose.umami.yml, /api/stats fédération
- nuxt.config.ts : config Umami runtime

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 04:40:48 +01:00
Yvv
9d92c4a5b3 Fix typos blanches admin lightmode + hero audience
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Remplace color:white → hsl(var(--color-text)) dans tous les composants admin
  (AdminFieldText, AdminFieldTextarea, AdminFormSection, AdminMarkdownEditor,
  AdminMediaBrowser, AdminSidebar, book/index, book/[slug], login, messages, site, songs)
- Conserve color:white uniquement sur fond primary (AdminSaveButton, login-btn)
- Hero home : ajout bloc audience/addressees (clé distincte pour éviter conflit YAML)
- home.yml : réordonne axes (citoyenne en premier — effet triangle)
- TypewriterText : affiche le second bloc avec séparateur fin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 16:08:03 +01:00
Yvv
8f548afb17 Palette dynamique dans BookPlayer et admin
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- BookPlayer : toutes les couleurs HSL en dur remplacées par variables CSS palette
- Admin (sidebar, formulaires, pages, book, songs, messages, media, login) : idem
- L'ambiance graphique suit maintenant la palette active partout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 16:41:23 +01:00
Yvv
9caf11c8ab Restructuration sections, contenu administrable, shadoks, palette été
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Structure par section : /numerique, /economique, /citoyenne (plus de /gestation)
- Chaque section a index + sous-pages avec contenu YAML administrable
- API content supporte les chemins imbriqués ([...path])
- Admin : liste des pages + éditeur par section
- Page /economique : monnaie libre (picto Ğ1), modèle éco, productions collectives, commande livre
- Page /citoyenne : decision (CTA Glibredecision), tarifs-eau (CTA SejeteralO)
- BookActions : composant partagé (player, PDF, chapitres, commande) sur home, eco et modele-eco
- GrateWizard remonté dans la section économique de la home
- Palette été par défaut, choix persisté en localStorage
- Fix lisibilité été (text-white/65 + variables CSS)
- Shadoks thématiques sur toutes les pages (8-10 par page, métiers variés)
- Redirections 301 : /gestation/*, /modele-eco/*, /decision, /lire/*
- README, CONTRIBUTING, CLAUDE.md mis à jour

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 16:13:46 +01:00
Yvv
c564e7be5f GrateWizard bloc dédié, messagerie libre, page numérique 3 piliers
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- GrateWizard : lancement URL simple (plus de popup embed), bloc
  dédié violet sur la home entre axes et événement
- Messagerie : plus de champs obligatoires, plus de champ email
  séparé, hint email dans le message, remerciement onboarding
- Page /numerique : 3 piliers (Logiciel libre, WoT, Cloud libre)
  avec projets associés, remplace les extraits livre hors-sujet
- Admin : carte Messages ajoutée au dashboard
- Safelist icônes complétée

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 03:06:48 +01:00
Yvv
3a5c40a886 Décision : picto gavel titre + parchemin docs, GrateWizard embed sans double scroll
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Page décision : icône gavel pour le titre, scroll-text pour Documents de référence
- GrateWizard : ouverture en mode embed (?embed=true&hideTabBar=true) avec
  scrollbars=no pour supprimer le double scroll

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 06:48:52 +01:00
Yvv
fbc2867163 Refonte accueil : hero typo statique, axes icônes, menu italic, page numérique
- Hero : 5 lignes typographiques alternées (bold/light/accent/caps/italic),
  citations et axes dans un bloc discret dépliable
- Icônes axes : Ğ1 custom, balance (éco don), graphe (WoT), marteau (décision),
  pictos plus lumineux (glow)
- Menu : Autonomie en italique + grand, Événement majuscule
- Page /autonomie renommée /numerique avec redirect 301
- Sceau hexagramme 益 Yì dans le layout, BookSection dans /modele-eco
- Fonts Syne + Space Grotesk, dark theme éclairci
- Popup GrateWizard agrandie (480×860)
- Actions AxisBlock : primary côte à côte, secondary séparé dessous

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 06:34:30 +01:00
Yvv
082a17d09b Fix accueil : hero fade doux, icônes safelist, blocs cliquables, menu, dark fort
- Hero : réécriture composable timeout pur (plus de Transition callbacks)
  Animation fade opacity 1s très douce, lisible
- Icônes : safelist UnoCSS dans nuxt.config.ts (résout pastilles vides)
- Menu : mis à jour site.yml (Numérique/Économique/Citoyenne/Événement)
- Blocs : card entière cliquable, zone actions séparée (border-top)
- Économie du don : lié à /modele-eco (page chapitres préservée)
- Tarifs de l'eau : bouton SejeteralO (localhost:3009 / collectivites.librodrome.org)
- Dark theme fort : bg 220 12% 15%, surface 19%, surface-light 24%
- Config SejeteralO + Glibredecision dans app.config.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 04:08:47 +01:00
Yvv
97ba6dd04c Redesign accueil : grille 3 axes, hero fade/swipe, pages gestation et décision
- Hero : animation fade-in/fade-out + swipe (useTypewriter composable + TypewriterText)
- 3 axes : Autonomie numérique, économique, citoyenne (AxisBlock + AxisGrid)
- Pages gestation avec présentations (wishBounty, trustWallet, Cloud libre)
- Page /decision : plateforme Décision collective (lien Glibredecision)
- Bloc événement distinct en bas des axes
- Nav : Numérique / Économique / Citoyenne / Événement
- Dark theme éclairci (bg 7→10%, surface 12→14%)
- Suppression BookSection + GrateWizardTeaser (remplacés par AxisGrid)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 03:49:07 +01:00
Yvv
f0338cca5e Fix déroulant PDF en production : polyfills DOMMatrix + worker pdfjs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
pdfjs-dist en Node.js pur (hors Vite) nécessite :
- DOMMatrix, Path2D, ImageData polyfills (pas de DOM en Node)
- pdf.worker.mjs copié dans le build (traceInclude dans nitro config)
Testé : 61 entrées retournées en mode production.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 23:24:59 +01:00
Yvv
c6b9abf2f3 Fix chemin PDF en production pour l'API pdf-outline
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
En prod le PDF est dans .output/public/, pas dans public/.
Cherche dans les deux emplacements (dev et prod).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:55:14 +01:00
Yvv
df0409fec3 Fix déroulant sommaire PDF : onMounted au lieu de import.meta.client
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Le $fetch dans import.meta.client ne se ré-exécutait pas après hydratation SSR.
onMounted garantit l'exécution côté client après le montage du composant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:42:58 +01:00
Yvv
1af00cc64c Admin : déroulant sommaire PDF par chapitre, transitions pages, URL GrateWizard
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Ajout API /api/admin/pdf-outline (extraction sommaire PDF côté serveur via pdfjs-dist)
- Déroulant <select> dans chaque ligne de chapitre admin avec les 61 titres/sous-titres du PDF
- Sauvegarde des associations chapitre→page PDF via config YAML
- Transition douce (fondu 1s/1.2s) pour le changement de pages dans le viewer PDF
- Correction des numéros de pages réels dans chapterPages (extraits du sommaire PDF)
- URL GrateWizard prod → gratewizard.axiom-team.fr

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:29:54 +01:00
Yvv
8a38c86794 Fix prod 404 : retrait volumes git sync qui cassent le conteneur
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Les volumes ../site, ../content, ../public montés dans docker-compose
écrasaient les fichiers du conteneur avec des chemins host inexistants.
Retour à la config d'origine. Le git sync sera configuré ultérieurement
quand le serveur aura un clone avec accès push.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 19:44:44 +01:00
Yvv
17f39e735d Fix build prod : postinstall tolérant + copy-pdfjs dans Dockerfile
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- postinstall : test -f avant d'appeler copy-pdfjs.sh (absent pendant pnpm install Docker)
- Dockerfile : RUN copy-pdfjs.sh après COPY . . pour copier les fichiers PDF.js
- Dockerfile : COPY content/ en production pour Nuxt Content

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 19:26:05 +01:00
Yvv
7ea19e2247 Viewer PDF.js mode livre avec signets, fix hydratation SSR
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Viewer PDF.js mode livre : double page côte à côte, navigation prev/next visuelle et clavier
- Panneau signets (outline) avec tout déplier/replier, highlight du spread courant
- Page 1 = couverture seule, puis paires 2-3, 4-5, etc.
- Navigation clavier : flèches, espace, Home/End
- Redimensionnement auto des canvas au resize
- Fix hydratation SSR : bookData.init() sans await dans ChapterHeader et SongBadges
- BookPdfReader : iframe vers /pdfjs/viewer.html au lieu du viewer natif
- Script postinstall pour copier pdf.min.mjs depuis node_modules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 18:16:47 +01:00
Yvv
9525ed3953 Bouton PDF par chapitre, badges morceaux améliorés, PDF configurable admin, git sync admin→prod
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Bouton PDF blanc par chapitre avec numéro de page (ChapterHeader)
- Badges morceaux plus visibles (bordure, poids, hover) dans ChapterHeader et SongBadges
- PDF viewer : page cible + panneau signets ouverts par défaut (BookPdfReader)
- Config YAML : pdfFile dans book, chapterPages pour le mapping chapitre→page
- Admin book : section PDF du livre avec chemin éditable et sauvegarde
- Git sync automatique : chaque sauvegarde admin commit+push en prod (ADMIN_GIT_SYNC=true)
- Docker : git installé en prod, volumes pour .git/site/content/public

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 15:32:38 +01:00
Yvv
b02368a15b Fix build prod : Pinia CJS default import crash Node 22+
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Pinia résout vers pinia.prod.cjs en production, Rollup convertit
require('vue') en default import ESM invalide. Script post-build
remplace par un namespace import (* as).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 20:36:37 +01:00
1f47533c77 Merge branch 'develop'
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-27 19:40:43 +01:00
07bf07a942 update app src
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-27 18:38:41 +01:00
107 changed files with 9988 additions and 1229 deletions

4
.gitignore vendored
View File

@@ -18,6 +18,10 @@ logs
.fleet .fleet
.idea .idea
# PDF.js (copié depuis node_modules par postinstall)
public/pdfjs/pdf.min.mjs
public/pdfjs/pdf.worker.min.mjs
# Sources originales (PDF, JPG — pas servies par l'appli) # Sources originales (PDF, JPG — pas servies par l'appli)
sources/ sources/

View File

@@ -14,26 +14,36 @@ Site vitrine du projet Le Librodrome — livre + chansons sur l'économie du don
``` ```
app/ app/
pages/ # Routes : index, lire/, ecouter/, autonomie, evenement, gratewizard, a-propos, admin/ pages/
components/ # admin, book, content, home, layout, player, song, ui numerique/ # Autonomie numérique (index + [slug] détail)
composables/ # useAudioPlayer, useBookData, useGrateWizard, usePlaylist, usePageContent, useSiteContent... economique/ # Autonomie économique (index, monnaie-libre, commande, productions-collectives)
assets/css/ # main.css (UnoCSS) modele-eco/ # Livre : sommaire + chapitres [slug]
data/ citoyenne/ # Autonomie citoyenne (index + [slug] détail)
pages/ # Contenu YAML par page (home, lire, ecouter, autonomie, evenement...) en-musique/ # Player audio
site.yml # Config globale du site evenement.vue # Événement
admin/ # Back-office (pages/, book/, songs, messages, media)
components/
book/Actions.vue # Boutons partagés livre (player, PDF, chapitres, commande)
home/ # BookSection, AxisBlock, AxisGrid, HeroSection, Messages
admin/, player/, song/, ui/
composables/ # useAudioPlayer, useBookData, useGrateWizard, usePageContent...
stores/palette.ts # 4 palettes saisonnières (été par défaut, persisté localStorage)
assets/css/ # main.css (UnoCSS + overrides light mode)
site/
pages/ # Contenu YAML par section (numerique/, economique/, citoyenne/)
site.yml # Config globale (nav, footer, GrateWizard)
bookplayer.config.yml # Config player/chapitres
server/ server/
api/ # Endpoints API (admin, health) api/content/pages/[...path].get.ts # GET pages YAML (chemins imbriqués)
middleware/ # Auth middleware api/admin/content/pages/[...path].put.ts # PUT pages YAML
api/admin/content/pages.get.ts # Liste toutes les pages
middleware/redirects.ts # 301 : /gestation, /modele-eco, /decision, /lire
docker/ docker/
Dockerfile # Build multi-stage (dev + prod) Dockerfile, docker-compose.yml, docker-compose.dev.yml
docker-compose.yml # Production (Traefik)
docker-compose.dev.yml # Dev Docker
``` ```
## Ports dev (CRITIQUE) ## Ports dev (CRITIQUE)
Chaque projet a un port fixe pour éviter les conflits Nuxt auto-increment :
| Projet | Port | Config | | Projet | Port | Config |
|--------|------|--------| |--------|------|--------|
| **librodrome** | **3000** | `nuxt.config.ts``devServer.port: 3000` | | **librodrome** | **3000** | `nuxt.config.ts``devServer.port: 3000` |
@@ -41,32 +51,32 @@ Chaque projet a un port fixe pour éviter les conflits Nuxt auto-increment :
| **SejeteralO frontend** | **3009** | `frontend/nuxt.config.ts``devServer.port: 3009` | | **SejeteralO frontend** | **3009** | `frontend/nuxt.config.ts``devServer.port: 3009` |
| **SejeteralO backend** | **8000** | Makefile → `uvicorn --port 8000` | | **SejeteralO backend** | **8000** | Makefile → `uvicorn --port 8000` |
Script de gestion : `/home/yvv/Documents/PROD/DEV/dev-ports.sh` (status/kill/clean/start) **Ne jamais changer ces ports.**
**Ne jamais changer ces ports.** Si un port est occupé, tuer le process parasite plutôt que laisser Nuxt auto-incrémenter.
## Intégration GrateWizard ## Intégration GrateWizard
- URL dev configurée dans `app/app.config.ts``localhost:3001` - URL dev : `app/app.config.ts``localhost:3001`
- URL prod : `https://gratewizard.ml` - URL prod : `https://gratewizard.axiom-team.fr`
- Ouverture en popup via `composables/useGrateWizard.ts` - Bloc GrateWizard dans la section économique de la home
- GrateWizard est un projet Next.js séparé (`/home/yvv/Documents/PROD/DEV/GrateWizard`)
## Contenu ## Contenu administrable
Le contenu des pages est dans `data/pages/*.yml` et chargé via `composables/usePageContent.ts`. Le contenu riche (articles) utilise Nuxt Content avec des fichiers Markdown. - YAML dans `site/pages/` organisé par section (sous-dossiers)
- API supporte les chemins imbriqués (`numerique/logiciel-libre`)
- Admin : `/admin/pages` liste toutes les pages, `/admin/pages/{path}` édite en YAML
- Git sync auto en prod (ADMIN_GIT_SYNC=true)
## Commandes ## Commandes
```bash ```bash
pnpm dev # Dev server sur :3000 pnpm dev # Dev server sur :3000
pnpm build # Build production pnpm build # Build production
pnpm generate # Génération statique
``` ```
## Conventions ## Conventions
- Langue du site : français - Langue du site : français
- Commits en français, style concis - Commits en français, style concis
- CSS via UnoCSS (utility-first), pas de framework CSS externe - CSS via UnoCSS (utility-first) + variables CSS palettes
- Composants Vue SFC avec `<script setup lang="ts">` - Composants Vue SFC avec `<script setup lang="ts">`
- Shadoks SVG inline thématiques sur chaque page (hidden mobile)

101
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,101 @@
# Contributing — Le Librodrome
## Architecture
```
app/
pages/
numerique/ # Section autonomie numérique
index.vue # Vue d'ensemble (3 piliers)
[slug].vue # Détail pilier (contenu YAML)
economique/ # Section autonomie économique
index.vue # Vue d'ensemble (monnaie libre, livre, productions)
monnaie-libre.vue
commande.vue # Commande du livre (Bookelis + librairie)
productions-collectives.vue
modele-eco/
index.vue # Couverture livre + sommaire chapitres
[slug].vue # Lecteur de chapitre (Nuxt Content)
citoyenne/ # Section autonomie citoyenne
index.vue # Vue d'ensemble (decision, tarifs-eau)
[slug].vue # Détail (contenu YAML)
en-musique/ # Player audio
evenement.vue
admin/ # Back-office protégé
pages/
index.vue # Liste toutes les pages YAML
[...path].vue # Éditeur YAML (supporte les sous-dossiers)
components/
book/Actions.vue # Boutons partagés du livre (player, PDF, chapitres, commande)
home/BookSection.vue # Bloc couverture + CTAs (utilise BookActions)
home/AxisBlock.vue # Bloc axe sur la home (supporte actions lien via `to`)
home/AxisGrid.vue # Grille 3 axes + GrateWizard
stores/palette.ts # 4 palettes saisonnières (été par défaut)
```
## Contenu YAML
API : `GET /api/content/pages/{path}` — supporte les chemins imbriqués (`numerique/logiciel-libre`).
Admin : `PUT /api/admin/content/pages/{path}` + `GET /api/admin/content/pages` (liste).
Les fichiers YAML sont dans `site/pages/`, organisés en sous-dossiers par section.
## Palettes
4 palettes dans `stores/palette.ts` : automne, hiver, printemps, été.
- Défaut : **été** (premier visiteur)
- Persisté en `localStorage('palette')` pour les visites suivantes
- CSS : variables `--color-primary`, `--color-accent`, etc. + classe `.palette-light`/`.palette-dark`
- Overrides light mode dans `main.css` (`.palette-light .text-white` → couleur adaptive)
- Admin : tous les `color: white` dans les composants admin sont remplacés par `hsl(var(--color-text))` sauf les boutons sur fond `hsl(var(--color-primary))` (`AdminSaveButton`, `.login-btn`)
## Navigation contextuelle — pages détail
`[slug].vue` (numérique, citoyenne) embarque :
- **Prev/next** (`ctx-nav`) en haut de page — liens vers les pages adjacentes dans la section
- **Sommaire flottant** (`.sommaire-sidebar`, `position: sticky`) dans un aside absolu à droite — visible ≥ 1300px, `overflow: clip` sur le parent pour ne pas casser le sticky
Les ancres sont des `<div :id="\`s${si}\`" class="section-anchor">` avec `scroll-margin-top: 5.5rem` (offset header fixe).
## Types de sections YAML
`[slug].vue` supporte : `arguments`, `fiche`, `equivalents`, `llm`, `atelier`, `links`, `insight`, `tiers`, `projet`, `territoire`.
`type: territoire` : économies alternatives (euro/Ğ1), grille bouquet de services, tableau matériel dépliable (`<details>`).
`type: projet` : carte gestation avec grille de features.
## Shadoks
Illustrations SVG inline sur chaque page, thématiques par section :
- Numérique index : métiers tech/industrie (codeuse, électricien, soudeuse, cryptographe...)
- Numérique détail : postures geek (pinguin+USB, web-of-trust, rubber duck, caféine, rack serveur, debugger loupe)
- Économique : artisanat/agriculture (boulangère, potier, forgeron...)
- Citoyenne : navigation/justice/théâtre (capitaine, avocate, comédien...)
- Modèle éco : livre/édition (typographe, calligraphe, conteuse...)
- Événement : spectacle (funambule, accordéoniste, jongleur...)
Règles : corps compact, longues pattes, grands pieds plats, bec pointu, postures variées.
CSS : `position: absolute`, `pointer-events: none`, `opacity 0.18-0.28`, animation float, `display: none` sur mobile.
Parent avec `overflow: clip` (pas `overflow: hidden` qui casserait `position: sticky`).
## Analytics
`app/composables/useTracking.ts` — wrapper Umami. Activé si `NUXT_PUBLIC_UMAMI_WEBSITE_ID` est défini.
`server/api/stats/index.get.ts` — endpoint public pour la fédération inter-instances (observatoires territoire).
`docker/docker-compose.umami.yml` — stack Umami + PostgreSQL avec labels Traefik.
## Redirections
`server/middleware/redirects.ts` :
- `/gestation/*` → section appropriée
- `/modele-eco/*` → `/economique/modele-eco/*`
- `/decision` → `/citoyenne/decision`
- `/lire/*` → `/economique/modele-eco/*`
- `/ecouter/*` → `/en-musique/*`
## Conventions
- Commits en français, concis
- Build prod vérifié avant push (`PORT=3099 node .output/server/index.mjs`)
- Port dev : 3000 (fixe, jamais de fallback)

124
README.md
View File

@@ -1,75 +1,73 @@
# Nuxt Minimal Starter # Le Librodrome
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. Site vitrine du projet Le Librodrome — livre + chansons sur l'économie du don.
## Setup ## Navigation
Make sure to install dependencies: Le site est organisé en 3 grandes sections :
| Section | Route | Contenu |
|---------|-------|---------|
| **Numérique** | `/numerique` | Autonomie numérique : logiciel libre, authentification WoT, cloud libre |
| **Économique** | `/economique` | Création monétaire, monnaie libre, modèle économique (livre + chapitres), productions collectives |
| **Citoyenne** | `/citoyenne` | Décision collective (Glibredecision), tarifs de l'eau (SejeteralO) |
Autres pages : `/en-musique` (player audio), `/evenement`, `/a-propos`, `/messages`.
Chaque section a une page index et des sous-pages de détail (`/numerique/logiciel-libre`, `/economique/modele-eco`, etc.).
## Le livre
- **Lire** : lecteur PDF intégré, chapitres Markdown sous `/economique/modele-eco/[slug]`
- **Écouter** : player audio avec 9 morceaux sous `/en-musique`
- **Commander** : page `/economique/commande` (Bookelis + librairie)
## Stack
- **Nuxt 4** (Vue 3, TypeScript, Nitro)
- **UnoCSS** (utility-first) + palettes saisonnières (été par défaut)
- **Nuxt Content** pour les chapitres du livre
- **Pinia** pour l'état (palette, player)
- **pnpm** comme package manager
## Contenu administrable
Le contenu des pages est dans `site/pages/` en YAML, organisé par section :
```
site/pages/
home.yml # Page d'accueil
numerique.yml # Index numérique
numerique/*.yml # Sous-pages
economique.yml # Index économique
economique/*.yml # Sous-pages (modele-eco, monnaie-libre, commande...)
citoyenne.yml # Index citoyenne
citoyenne/*.yml # Sous-pages (decision, tarifs-eau)
en-musique.yml, evenement.yml, gratewizard.yml
```
Administration via `/admin/pages` (éditeur YAML, authentifié).
Le hero de la home (`home.yml`) supporte deux blocs dépliables :
- `approach` + `axes` : approche par dimension (numérique → code source, etc.)
- `audience` + `addressees` : à qui s'adresse le projet (collectifs, entreprises, collectivités)
## Développement
```bash ```bash
# npm
npm install
# pnpm
pnpm install pnpm install
pnpm dev # Dev server sur :3000
# yarn pnpm build # Build production
yarn install
# bun
bun install
``` ```
## Development Server Port réservé : **3000** (ne pas changer).
Start the development server on `http://localhost:3000`: ## Analytics
```bash Umami self-hosted (optionnel). Configurer `NUXT_PUBLIC_UMAMI_WEBSITE_ID` et `NUXT_PUBLIC_UMAMI_URL` dans l'environnement.
# npm Déploiement séparé : `docker/docker-compose.umami.yml``stats.librodrome.org`.
npm run dev Stats publiques exposées via `/api/stats` pour la fédération inter-instances.
# pnpm ## Déploiement
pnpm dev
# yarn Docker + Traefik, CI via Woodpecker. Domaine : `librodrome.org`.
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

View File

@@ -6,13 +6,6 @@ export default defineAppConfig({
}, },
header: { header: {
height: '4rem', height: '4rem',
nav: [
{ label: 'Autonomie', to: '/autonomie' },
{ label: 'Modèle éco', to: '/modele-eco' },
{ label: 'En musique', to: '/en-musique' },
{ label: 'Évènement', to: '/evenement' },
{ label: 'À propos', to: '/a-propos' },
],
}, },
footer: { footer: {
credits: '© 2026 Le Librodrome — Productions collectives', credits: '© 2026 Le Librodrome — Productions collectives',
@@ -21,10 +14,16 @@ export default defineAppConfig({
], ],
}, },
gratewizard: { gratewizard: {
url: import.meta.dev ? 'http://localhost:3001' : 'https://gratewizard.ml', url: import.meta.dev ? 'http://localhost:3001' : 'https://gratewizard.axiom-team.fr',
popup: { popup: {
width: 420, width: 480,
height: 720, height: 860,
}, },
}, },
libredecision: {
url: import.meta.dev ? 'http://localhost:3002' : 'https://decision.laplank.org',
},
sejeteral0: {
url: import.meta.dev ? 'http://localhost:3009' : 'https://collectivites.librodrome.org',
},
}) })

View File

@@ -17,6 +17,18 @@
const paletteStore = usePaletteStore() const paletteStore = usePaletteStore()
onMounted(() => paletteStore.applyToDOM()) onMounted(() => paletteStore.applyToDOM())
// Umami analytics — inject script only when configured
const runtimeConfig = useRuntimeConfig()
if (runtimeConfig.public.umamiWebsiteId && runtimeConfig.public.umamiUrl) {
useHead({
script: [{
src: `${runtimeConfig.public.umamiUrl}/script.js`,
defer: true,
'data-website-id': runtimeConfig.public.umamiWebsiteId,
}],
})
}
useHead({ useHead({
titleTemplate: (title) => { titleTemplate: (title) => {
return title ? `${title} — Le Librodrome` : 'Le librodrome' return title ? `${title} — Le Librodrome` : 'Le librodrome'

View File

@@ -2,11 +2,11 @@
/* This file provides fallback and utility classes */ /* This file provides fallback and utility classes */
.font-display { .font-display {
font-family: 'Outfit', system-ui, sans-serif; font-family: 'Syne', system-ui, sans-serif;
} }
.font-sans { .font-sans {
font-family: 'Inter', system-ui, sans-serif; font-family: 'Space Grotesk', system-ui, sans-serif;
} }
.font-mono { .font-mono {

View File

@@ -5,9 +5,9 @@
:root { :root {
--color-primary: 18 80% 45%; --color-primary: 18 80% 45%;
--color-accent: 32 85% 50%; --color-accent: 32 85% 50%;
--color-bg: 20 10% 7%; --color-bg: 215 8% 22%;
--color-surface: 20 10% 12%; --color-surface: 213 7% 27%;
--color-surface-light: 20 8% 17%; --color-surface-light: 210 6% 32%;
--color-text: 0 0% 100%; --color-text: 0 0% 100%;
--color-text-muted: 0 0% 65%; --color-text-muted: 0 0% 65%;
@@ -15,8 +15,8 @@
--player-height: 0rem; --player-height: 0rem;
--sidebar-width: 280px; --sidebar-width: 280px;
--font-display: 'Outfit', sans-serif; --font-display: 'Syne', sans-serif;
--font-sans: 'Inter', sans-serif; --font-sans: 'Space Grotesk', sans-serif;
--font-mono: 'JetBrains Mono', monospace; --font-mono: 'JetBrains Mono', monospace;
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
@@ -104,6 +104,7 @@ a {
.palette-light .text-white\/45 { color: hsl(var(--color-text) / 0.52) !important; } .palette-light .text-white\/45 { color: hsl(var(--color-text) / 0.52) !important; }
.palette-light .text-white\/50 { color: hsl(var(--color-text) / 0.58) !important; } .palette-light .text-white\/50 { color: hsl(var(--color-text) / 0.58) !important; }
.palette-light .text-white\/60 { color: hsl(var(--color-text) / 0.68) !important; } .palette-light .text-white\/60 { color: hsl(var(--color-text) / 0.68) !important; }
.palette-light .text-white\/65 { color: hsl(var(--color-text) / 0.73) !important; }
.palette-light .text-white\/70 { color: hsl(var(--color-text) / 0.78) !important; } .palette-light .text-white\/70 { color: hsl(var(--color-text) / 0.78) !important; }
.palette-light .text-white\/80 { color: hsl(var(--color-text) / 0.88) !important; } .palette-light .text-white\/80 { color: hsl(var(--color-text) / 0.88) !important; }
.palette-light .text-white\/85 { color: hsl(var(--color-text) / 0.92) !important; } .palette-light .text-white\/85 { color: hsl(var(--color-text) / 0.92) !important; }

View File

@@ -58,7 +58,7 @@ function addItem() {
.field-label { .field-label {
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 500; font-weight: 500;
color: hsl(20 8% 60%); color: hsl(var(--color-text-muted));
} }
.list-item { .list-item {
@@ -66,9 +66,9 @@ function addItem() {
align-items: flex-start; align-items: flex-start;
gap: 0.5rem; gap: 0.5rem;
padding: 0.5rem; padding: 0.5rem;
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.5rem; border-radius: 0.5rem;
background: hsl(20 8% 5%); background: hsl(var(--color-bg));
} }
.list-item > :deep(*:first-child) { .list-item > :deep(*:first-child) {
@@ -101,9 +101,9 @@ function addItem() {
gap: 0.375rem; gap: 0.375rem;
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px dashed hsl(20 8% 22%); border: 1px dashed hsl(var(--color-surface-light));
background: none; background: none;
color: hsl(20 8% 50%); color: hsl(var(--color-text-muted));
font-size: 0.8rem; font-size: 0.8rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
@@ -111,7 +111,7 @@ function addItem() {
} }
.list-add:hover { .list-add:hover {
border-color: hsl(12 76% 48% / 0.4); border-color: hsl(var(--color-primary) / 0.4);
color: hsl(12 76% 68%); color: hsl(var(--color-primary));
} }
</style> </style>

View File

@@ -36,21 +36,21 @@ const id = computed(() => `field-${props.label.toLowerCase().replace(/\s+/g, '-'
.field-label { .field-label {
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 500; font-weight: 500;
color: hsl(20 8% 60%); color: hsl(var(--color-text-muted));
} }
.field-input { .field-input {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-size: 0.875rem; font-size: 0.875rem;
transition: border-color 0.2s; transition: border-color 0.2s;
} }
.field-input:focus { .field-input:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
} }
</style> </style>

View File

@@ -37,15 +37,15 @@ const id = computed(() => `field-${props.label.toLowerCase().replace(/\s+/g, '-'
.field-label { .field-label {
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 500; font-weight: 500;
color: hsl(20 8% 60%); color: hsl(var(--color-text-muted));
} }
.field-textarea { .field-textarea {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-size: 0.875rem; font-size: 0.875rem;
resize: vertical; resize: vertical;
min-height: 5rem; min-height: 5rem;
@@ -55,6 +55,6 @@ const id = computed(() => `field-${props.label.toLowerCase().replace(/\s+/g, '-'
.field-textarea:focus { .field-textarea:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
} }
</style> </style>

View File

@@ -19,7 +19,7 @@ defineProps<{
<style scoped> <style scoped>
.admin-section { .admin-section {
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.75rem; border-radius: 0.75rem;
overflow: hidden; overflow: hidden;
margin-bottom: 1rem; margin-bottom: 1rem;
@@ -32,14 +32,14 @@ defineProps<{
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
font-weight: 600; font-weight: 600;
font-size: 0.9rem; font-size: 0.9rem;
color: white; color: hsl(var(--color-text));
cursor: pointer; cursor: pointer;
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
user-select: none; user-select: none;
} }
.admin-section-header:hover { .admin-section-header:hover {
background: hsl(20 8% 8%); background: hsl(var(--color-surface));
} }
.admin-section[open] .section-arrow { .admin-section[open] .section-arrow {

View File

@@ -98,7 +98,7 @@ const renderedHtml = computed(() => {
<style scoped> <style scoped>
.md-editor { .md-editor {
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
} }
@@ -107,8 +107,8 @@ const renderedHtml = computed(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
border-bottom: 1px solid hsl(20 8% 14%); border-bottom: 1px solid hsl(var(--color-surface-light));
} }
.md-tabs { .md-tabs {
@@ -122,24 +122,24 @@ const renderedHtml = computed(() => {
padding: 0.5rem 0.875rem; padding: 0.5rem 0.875rem;
border: none; border: none;
background: none; background: none;
color: hsl(20 8% 50%); color: hsl(var(--color-text-muted));
font-size: 0.8rem; font-size: 0.8rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
} }
.md-tab--active { .md-tab--active {
color: white; color: hsl(var(--color-text));
background: hsl(20 8% 10%); background: hsl(var(--color-surface));
} }
.md-fullscreen { .md-fullscreen {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
color: hsl(20 8% 40%); color: hsl(var(--color-text-muted));
transition: color 0.2s; transition: color 0.2s;
} }
.md-fullscreen:hover, .md-fullscreen:hover,
.md-fullscreen--active { color: white; } .md-fullscreen--active { color: hsl(var(--color-text)); }
.md-body { .md-body {
display: flex; display: flex;
@@ -151,14 +151,14 @@ const renderedHtml = computed(() => {
} }
.md-body--split .md-preview { .md-body--split .md-preview {
border-left: 1px solid hsl(20 8% 14%); border-left: 1px solid hsl(var(--color-surface-light));
} }
.md-body--fullscreen { .md-body--fullscreen {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 50; z-index: 50;
background: hsl(20 8% 4%); background: hsl(var(--color-bg));
} }
.md-body--fullscreen .md-textarea, .md-body--fullscreen .md-textarea,
@@ -170,8 +170,8 @@ const renderedHtml = computed(() => {
width: 100%; width: 100%;
padding: 1rem; padding: 1rem;
border: none; border: none;
background: hsl(20 8% 4%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-family: var(--font-mono, monospace); font-family: var(--font-mono, monospace);
font-size: 0.85rem; font-size: 0.85rem;
line-height: 1.7; line-height: 1.7;
@@ -189,6 +189,6 @@ const renderedHtml = computed(() => {
min-height: 24rem; min-height: 24rem;
max-height: 70vh; max-height: 70vh;
overflow-y: auto; overflow-y: auto;
background: hsl(20 8% 4%); background: hsl(var(--color-bg));
} }
</style> </style>

View File

@@ -102,18 +102,18 @@ function formatSize(bytes: number): string {
.filter-btn { .filter-btn {
padding: 0.25rem 0.625rem; padding: 0.25rem 0.625rem;
border-radius: 9999px; border-radius: 9999px;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
background: none; background: none;
color: hsl(20 8% 55%); color: hsl(var(--color-text-muted));
font-size: 0.75rem; font-size: 0.75rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
} }
.filter-btn--active { .filter-btn--active {
background: hsl(12 76% 48% / 0.15); background: hsl(var(--color-primary) / 0.15);
border-color: hsl(12 76% 48% / 0.3); border-color: hsl(var(--color-primary) / 0.3);
color: hsl(12 76% 68%); color: hsl(var(--color-primary));
} }
.media-grid { .media-grid {
@@ -123,7 +123,7 @@ function formatSize(bytes: number): string {
} }
.media-card { .media-card {
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
@@ -131,18 +131,18 @@ function formatSize(bytes: number): string {
} }
.media-card:hover { .media-card:hover {
border-color: hsl(20 8% 22%); border-color: hsl(var(--color-surface-light));
} }
.media-card--selected { .media-card--selected {
border-color: hsl(12 76% 48%); border-color: hsl(var(--color-primary));
box-shadow: 0 0 0 1px hsl(12 76% 48% / 0.3); box-shadow: 0 0 0 1px hsl(var(--color-primary) / 0.3);
} }
.media-thumb { .media-thumb {
aspect-ratio: 1; aspect-ratio: 1;
overflow: hidden; overflow: hidden;
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
} }
.media-thumb img { .media-thumb img {
@@ -156,8 +156,8 @@ function formatSize(bytes: number): string {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
color: hsl(20 8% 40%); color: hsl(var(--color-text-muted));
} }
.media-info { .media-info {
@@ -169,7 +169,7 @@ function formatSize(bytes: number): string {
.media-name { .media-name {
font-size: 0.72rem; font-size: 0.72rem;
color: white; color: hsl(var(--color-text));
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -177,7 +177,7 @@ function formatSize(bytes: number): string {
.media-size { .media-size {
font-size: 0.65rem; font-size: 0.65rem;
color: hsl(20 8% 40%); color: hsl(var(--color-text-muted));
} }
.media-actions { .media-actions {
@@ -186,9 +186,9 @@ function formatSize(bytes: number): string {
justify-content: space-between; justify-content: space-between;
margin-top: 1rem; margin-top: 1rem;
padding: 0.75rem; padding: 0.75rem;
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.5rem; border-radius: 0.5rem;
background: hsl(20 8% 5%); background: hsl(var(--color-bg));
} }
.delete-btn { .delete-btn {

View File

@@ -73,7 +73,7 @@ async function upload(files: FileList) {
<style scoped> <style scoped>
.upload-zone { .upload-zone {
border: 2px dashed hsl(20 8% 22%); border: 2px dashed hsl(var(--color-surface-light));
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 2rem; padding: 2rem;
text-align: center; text-align: center;
@@ -82,12 +82,12 @@ async function upload(files: FileList) {
} }
.upload-zone:hover { .upload-zone:hover {
border-color: hsl(12 76% 48% / 0.4); border-color: hsl(var(--color-primary) / 0.4);
} }
.upload-zone--active { .upload-zone--active {
border-color: hsl(12 76% 48%); border-color: hsl(var(--color-primary));
background: hsl(12 76% 48% / 0.05); background: hsl(var(--color-primary) / 0.05);
} }
.upload-progress { .upload-progress {
@@ -95,7 +95,7 @@ async function upload(files: FileList) {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
color: hsl(20 8% 55%); color: hsl(var(--color-text-muted));
font-size: 0.85rem; font-size: 0.85rem;
} }

View File

@@ -31,7 +31,7 @@ defineEmits<{
padding: 0.625rem 1.25rem; padding: 0.625rem 1.25rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: none; border: none;
background: hsl(12 76% 48%); background: hsl(var(--color-primary));
color: white; color: white;
font-weight: 600; font-weight: 600;
font-size: 0.85rem; font-size: 0.85rem;
@@ -40,7 +40,7 @@ defineEmits<{
} }
.save-btn:hover:not(:disabled) { .save-btn:hover:not(:disabled) {
background: hsl(12 76% 42%); background: hsl(var(--color-primary));
} }
.save-btn:disabled { .save-btn:disabled {

View File

@@ -23,13 +23,25 @@
</NuxtLink> </NuxtLink>
<p class="sidebar-section">Pages</p> <p class="sidebar-section">Pages</p>
<NuxtLink to="/admin/pages" class="sidebar-link" exact-active-class="sidebar-link--active">
<div class="i-lucide-file-text h-4 w-4" />
Toutes les pages
</NuxtLink>
<NuxtLink to="/admin/pages/home" class="sidebar-link" active-class="sidebar-link--active"> <NuxtLink to="/admin/pages/home" class="sidebar-link" active-class="sidebar-link--active">
<div class="i-lucide-home h-4 w-4" /> <div class="i-lucide-home h-4 w-4" />
Accueil Accueil
</NuxtLink> </NuxtLink>
<NuxtLink to="/admin/pages/modele-eco" class="sidebar-link" active-class="sidebar-link--active"> <NuxtLink to="/admin/pages/numerique" class="sidebar-link" active-class="sidebar-link--active">
<div class="i-lucide-book-open h-4 w-4" /> <div class="i-lucide-monitor h-4 w-4" />
Modèle éco Numérique
</NuxtLink>
<NuxtLink to="/admin/pages/economique" class="sidebar-link" active-class="sidebar-link--active">
<div class="i-lucide-coins h-4 w-4" />
Économique
</NuxtLink>
<NuxtLink to="/admin/pages/citoyenne" class="sidebar-link" active-class="sidebar-link--active">
<div class="i-lucide-landmark h-4 w-4" />
Citoyenne
</NuxtLink> </NuxtLink>
<NuxtLink to="/admin/pages/en-musique" class="sidebar-link" active-class="sidebar-link--active"> <NuxtLink to="/admin/pages/en-musique" class="sidebar-link" active-class="sidebar-link--active">
<div class="i-lucide-headphones h-4 w-4" /> <div class="i-lucide-headphones h-4 w-4" />
@@ -79,8 +91,8 @@ async function logout() {
<style scoped> <style scoped>
.admin-sidebar { .admin-sidebar {
background: hsl(20 8% 5%); background: hsl(var(--color-bg));
border-right: 1px solid hsl(20 8% 14%); border-right: 1px solid hsl(var(--color-surface-light));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100dvh; height: 100dvh;
@@ -90,7 +102,7 @@ async function logout() {
.sidebar-header { .sidebar-header {
padding: 1.25rem 1rem; padding: 1.25rem 1rem;
border-bottom: 1px solid hsl(20 8% 14%); border-bottom: 1px solid hsl(var(--color-surface-light));
} }
.sidebar-nav { .sidebar-nav {
@@ -104,7 +116,7 @@ async function logout() {
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.1em; letter-spacing: 0.1em;
color: hsl(20 8% 40%); color: hsl(var(--color-text-muted));
padding: 1rem 0.75rem 0.375rem; padding: 1rem 0.75rem 0.375rem;
margin: 0; margin: 0;
} }
@@ -116,7 +128,7 @@ async function logout() {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border-radius: 0.5rem; border-radius: 0.5rem;
font-size: 0.85rem; font-size: 0.85rem;
color: hsl(20 8% 60%); color: hsl(var(--color-text-muted));
text-decoration: none; text-decoration: none;
transition: all 0.2s; transition: all 0.2s;
border: none; border: none;
@@ -126,18 +138,18 @@ async function logout() {
} }
.sidebar-link:hover { .sidebar-link:hover {
background: hsl(20 8% 10%); background: hsl(var(--color-surface));
color: white; color: hsl(var(--color-text));
} }
.sidebar-link--active { .sidebar-link--active {
background: hsl(12 76% 48% / 0.12); background: hsl(var(--color-primary) / 0.12);
color: hsl(12 76% 68%); color: hsl(var(--color-primary));
} }
.sidebar-footer { .sidebar-footer {
padding: 0.5rem; padding: 0.5rem;
border-top: 1px solid hsl(20 8% 14%); border-top: 1px solid hsl(var(--color-surface-light));
} }
@media (max-width: 768px) { @media (max-width: 768px) {

View File

@@ -0,0 +1,33 @@
<template>
<div class="mt-8 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:gap-4">
<UiBaseButton @click="$emit('open-player')">
<div class="i-lucide-play mr-2 h-5 w-5" />
Présentation musicale
</UiBaseButton>
<UiBaseButton variant="accent" @click="$emit('open-pdf')">
<div class="i-lucide-book-open mr-2 h-5 w-5" />
Lire le livre
</UiBaseButton>
<UiBaseButton v-if="showChapters" variant="ghost" to="/economique/modele-eco">
<div class="i-lucide-list mr-2 h-5 w-5" />
Présentation des chapitres
</UiBaseButton>
<UiBaseButton variant="ghost" to="/economique/commande">
<div class="i-lucide-shopping-bag mr-2 h-5 w-5" />
Commandez le livre
</UiBaseButton>
</div>
</template>
<script setup lang="ts">
withDefaults(defineProps<{
showChapters?: boolean
}>(), {
showChapters: true,
})
defineEmits<{
'open-player': []
'open-pdf': []
}>()
</script>

View File

@@ -22,6 +22,7 @@
<!-- PDF embed --> <!-- PDF embed -->
<div class="pdf-viewport"> <div class="pdf-viewport">
<iframe <iframe
:key="pdfUrl"
:src="pdfUrl" :src="pdfUrl"
class="pdf-frame" class="pdf-frame"
:title="bpContent?.pdf.iframeTitle" :title="bpContent?.pdf.iframeTitle"
@@ -33,10 +34,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ modelValue: boolean }>() const props = defineProps<{ modelValue: boolean; page?: number }>()
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>() const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
const { data: bpContent } = await usePageContent('book-player') const { data: bpContent } = await usePageContent('book-player')
const bookData = useBookData()
await bookData.init()
const overlayRef = ref<HTMLElement>() const overlayRef = ref<HTMLElement>()
@@ -45,7 +48,12 @@ const isOpen = computed({
set: (v) => emit('update:modelValue', v), set: (v) => emit('update:modelValue', v),
}) })
const pdfUrl = '/pdf/une-economie-du-don.pdf' const pdfUrl = computed(() => {
const base = bookData.getPdfUrl()
const viewerBase = `/pdfjs/viewer.html?file=${encodeURIComponent(base)}`
if (props.page) return `${viewerBase}#page=${props.page}`
return viewerBase
})
function close() { function close() {
isOpen.value = false isOpen.value = false

View File

@@ -447,7 +447,7 @@ onUnmounted(() => {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 60; z-index: 60;
background: hsl(20 8% 3%); background: hsl(var(--color-bg));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@@ -538,7 +538,7 @@ onUnmounted(() => {
transform: translateX(-50%); transform: translateX(-50%);
z-index: 10; z-index: 10;
font-size: 0.68rem; font-size: 0.68rem;
color: hsl(20 8% 28%); color: hsl(var(--color-text-muted));
white-space: nowrap; white-space: nowrap;
} }
@@ -582,7 +582,7 @@ onUnmounted(() => {
width: 2.25rem; height: 2.25rem; width: 2.25rem; height: 2.25rem;
border-radius: 0.5rem; border-radius: 0.5rem;
background: transparent; background: transparent;
color: hsl(20 8% 45%); color: hsl(var(--color-text-muted));
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
@@ -617,7 +617,7 @@ onUnmounted(() => {
.reader-bar-pages { .reader-bar-pages {
font-family: var(--font-mono, monospace); font-family: var(--font-mono, monospace);
font-size: 0.75rem; font-size: 0.75rem;
color: hsl(20 8% 40%); color: hsl(var(--color-text-muted));
flex-shrink: 0; flex-shrink: 0;
} }
@@ -626,16 +626,16 @@ onUnmounted(() => {
position: absolute; position: absolute;
inset: 0; inset: 0;
z-index: 72; z-index: 72;
background: hsl(20 8% 3% / 0.6); background: hsl(var(--color-bg) / 0.6);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
display: flex; display: flex;
} }
.sommaire-panel { .sommaire-panel {
width: min(300px, 80vw); width: min(300px, 80vw);
height: 100%; height: 100%;
background: hsl(20 8% 6% / 0.95); background: hsl(var(--color-surface) / 0.95);
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
border-right: 1px solid hsl(20 8% 14%); border-right: 1px solid hsl(var(--color-surface-light));
padding: 1.5rem; padding: 1.5rem;
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
@@ -646,7 +646,7 @@ onUnmounted(() => {
font-family: var(--font-display, 'Syne', sans-serif); font-family: var(--font-display, 'Syne', sans-serif);
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 700; font-weight: 700;
color: hsl(20 8% 45%); color: hsl(var(--color-text-muted));
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.1em; letter-spacing: 0.1em;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
@@ -658,7 +658,7 @@ onUnmounted(() => {
padding: 0.625rem 0.75rem; padding: 0.625rem 0.75rem;
border-radius: 0.5rem; border-radius: 0.5rem;
font-size: 0.85rem; font-size: 0.85rem;
color: hsl(20 8% 55%); color: hsl(var(--color-text-muted));
background: transparent; background: transparent;
border: none; border: none;
cursor: pointer; cursor: pointer;
@@ -667,7 +667,7 @@ onUnmounted(() => {
width: 100%; width: 100%;
} }
.sommaire-item:hover { .sommaire-item:hover {
background: hsl(20 8% 12%); background: hsl(var(--color-surface));
color: white; color: white;
} }
.sommaire-item--active { .sommaire-item--active {
@@ -683,8 +683,8 @@ onUnmounted(() => {
font-size: 0.65rem; font-size: 0.65rem;
font-weight: 700; font-weight: 700;
font-family: var(--font-mono, monospace); font-family: var(--font-mono, monospace);
color: hsl(20 8% 40%); color: hsl(var(--color-text-muted));
background: hsl(20 8% 12%); background: hsl(var(--color-surface));
flex-shrink: 0; flex-shrink: 0;
} }
.sommaire-item--active .sommaire-num { .sommaire-item--active .sommaire-num {
@@ -713,7 +713,7 @@ onUnmounted(() => {
width: 100%; width: 100%;
overflow: hidden auto; overflow: hidden auto;
border-radius: 0.75rem; border-radius: 0.75rem;
background: hsl(20 8% 5% / 0.4); background: hsl(var(--color-bg) / 0.4);
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
} }
@@ -809,7 +809,7 @@ onUnmounted(() => {
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
background: linear-gradient(to left, hsl(20 8% 3% / 0.4), transparent); background: linear-gradient(to left, hsl(var(--color-bg) / 0.4), transparent);
transition: opacity 0.15s; transition: opacity 0.15s;
border-radius: 0 0.75rem 0.75rem 0; border-radius: 0 0.75rem 0.75rem 0;
} }
@@ -833,7 +833,7 @@ onUnmounted(() => {
width: 2.5rem; height: 2.5rem; width: 2.5rem; height: 2.5rem;
border-radius: 50%; border-radius: 50%;
background: transparent; background: transparent;
color: hsl(20 8% 45%); color: hsl(var(--color-text-muted));
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
@@ -861,7 +861,7 @@ onUnmounted(() => {
border-radius: 50%; border-radius: 50%;
overflow: hidden; overflow: hidden;
flex-shrink: 0; flex-shrink: 0;
border: 2px solid hsl(20 8% 22%); border: 2px solid hsl(var(--color-surface-light));
box-shadow: 0 0 10px hsl(var(--scene-h1) 50% 40% / 0.15); box-shadow: 0 0 10px hsl(var(--scene-h1) 50% 40% / 0.15);
} }
.reader-disc.spinning { .reader-disc.spinning {
@@ -878,12 +878,12 @@ onUnmounted(() => {
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: 0.5rem; height: 0.5rem; width: 0.5rem; height: 0.5rem;
border-radius: 50%; border-radius: 50%;
background: hsl(20 8% 5%); background: hsl(var(--color-bg));
border: 1.5px solid hsl(20 8% 18%); border: 1.5px solid hsl(var(--color-surface-light));
} }
.reader-song-name { .reader-song-name {
font-size: 0.75rem; font-size: 0.75rem;
color: hsl(20 8% 50%); color: hsl(var(--color-text-muted));
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@@ -15,18 +15,28 @@
{{ description }} {{ description }}
</p> </p>
<!-- Songs + PDF actions row -->
<div class="mt-4 flex flex-wrap items-center gap-2">
<!-- Associated songs badges --> <!-- Associated songs badges -->
<div v-if="songs.length > 0" class="mt-4 flex flex-wrap gap-2">
<button <button
v-for="song in songs" v-for="song in songs"
:key="song.id" :key="song.id"
class="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary transition-colors hover:bg-primary/20" class="song-badge-btn"
@click="playSong(song)" @click="playSong(song)"
> >
<div class="i-lucide-music h-3 w-3" /> <div class="i-lucide-music h-3 w-3" />
{{ song.title }} {{ song.title }}
</button> </button>
<!-- PDF button -->
<button v-if="pdfPage" class="pdf-btn" @click="showPdf = true">
<div class="i-lucide-book-open h-3.5 w-3.5" />
<span>Lire dans le PDF</span>
<span class="pdf-page-badge">p.{{ pdfPage }}</span>
</button>
</div> </div>
<LazyBookPdfReader v-if="showPdf" v-model="showPdf" :page="pdfPage" />
</header> </header>
</template> </template>
@@ -44,9 +54,12 @@ const props = defineProps<{
const bookData = useBookData() const bookData = useBookData()
const { loadAndPlay } = useAudioPlayer() const { loadAndPlay } = useAudioPlayer()
await bookData.init() bookData.init()
const songs = computed(() => bookData.getChapterSongs(props.chapterSlug)) const songs = computed(() => bookData.getChapterSongs(props.chapterSlug))
const pdfPage = computed(() => bookData.getChapterPage(props.chapterSlug))
const showPdf = ref(false)
function playSong(song: Song) { function playSong(song: Song) {
loadAndPlay(song) loadAndPlay(song)
@@ -59,4 +72,57 @@ function playSong(song: Song) {
padding-bottom: 0.75rem; padding-bottom: 0.75rem;
border-bottom: 2px solid hsl(var(--color-primary) / 0.4); border-bottom: 2px solid hsl(var(--color-primary) / 0.4);
} }
.song-badge-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
border-radius: 9999px;
padding: 0.375rem 0.875rem;
font-size: 0.8rem;
font-weight: 600;
background: hsl(var(--color-primary) / 0.12);
color: hsl(var(--color-primary) / 0.85);
border: 1px solid hsl(var(--color-primary) / 0.25);
cursor: pointer;
transition: all 0.2s;
}
.song-badge-btn:hover {
background: hsl(var(--color-primary) / 0.2);
border-color: hsl(var(--color-primary) / 0.4);
box-shadow: 0 2px 8px hsl(var(--color-primary) / 0.15);
transform: translateY(-1px);
}
.pdf-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background: hsl(0 0% 96%);
color: hsl(0 0% 15%);
font-size: 0.8rem;
font-weight: 600;
border: 1px solid hsl(0 0% 88%);
cursor: pointer;
transition: all 0.2s;
}
.pdf-btn:hover {
background: white;
box-shadow: 0 2px 12px hsl(0 0% 100% / 0.25);
transform: translateY(-1px);
}
.pdf-page-badge {
font-family: var(--font-mono, monospace);
font-size: 0.7rem;
font-weight: 500;
padding: 0.1rem 0.4rem;
border-radius: 0.25rem;
background: hsl(0 0% 88%);
color: hsl(0 0% 35%);
}
</style> </style>

View File

@@ -6,7 +6,7 @@
<ul class="flex flex-col gap-1"> <ul class="flex flex-col gap-1">
<li v-for="chapter in chapters" :key="chapter.path"> <li v-for="chapter in chapters" :key="chapter.path">
<NuxtLink <NuxtLink
:to="`/modele-eco/${chapter.stem?.split('/').pop()}`" :to="`/economique/modele-eco/${chapter.stem?.split('/').pop()}`"
class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors hover:bg-white/5" class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors hover:bg-white/5"
active-class="bg-primary/10 text-primary font-medium" active-class="bg-primary/10 text-primary font-medium"
> >

View File

@@ -0,0 +1,159 @@
<template>
<div class="chapter-toc" ref="tocRoot">
<button class="toc-toggle" @click="open = !open" :aria-expanded="open">
<div class="i-lucide-list h-4 w-4" />
<span>Sommaire</span>
<div class="i-lucide-chevron-down h-3.5 w-3.5 toc-chevron" :class="{ 'toc-chevron--open': open }" />
</button>
<Transition name="toc-dropdown">
<div v-if="open" class="toc-panel">
<nav>
<a
v-for="item in headings"
:key="item.id"
:href="`#${item.id}`"
class="toc-item"
:class="{ 'toc-item--h2': item.level === 2 }"
@click="open = false"
>
{{ item.text }}
</a>
</nav>
</div>
</Transition>
</div>
</template>
<script setup lang="ts">
interface TocHeading {
id: string
text: string
level: number
}
const open = ref(false)
const headings = ref<TocHeading[]>([])
const tocRoot = ref<HTMLElement>()
function extractHeadings() {
const article = document.querySelector('.prose-lyrics')
if (!article) return
const nodes = article.querySelectorAll('h1, h2')
headings.value = Array.from(nodes)
.filter(el => el.id)
.map(el => ({
id: el.id,
text: el.textContent?.trim() ?? '',
level: parseInt(el.tagName[1]),
}))
}
// Close on click outside
function onClickOutside(e: MouseEvent) {
if (tocRoot.value && !tocRoot.value.contains(e.target as Node)) {
open.value = false
}
}
onMounted(() => {
// Wait for content to render
nextTick(() => {
extractHeadings()
// Retry after a short delay in case content loads async
setTimeout(extractHeadings, 500)
})
document.addEventListener('click', onClickOutside)
})
onUnmounted(() => {
document.removeEventListener('click', onClickOutside)
})
</script>
<style scoped>
.chapter-toc {
position: relative;
display: inline-block;
}
.toc-toggle {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
background: hsl(20 8% 10%);
border: 1px solid hsl(20 8% 18%);
color: hsl(20 8% 60%);
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.toc-toggle:hover {
border-color: hsl(20 8% 30%);
color: white;
}
.toc-chevron {
transition: transform 0.2s;
}
.toc-chevron--open {
transform: rotate(180deg);
}
.toc-panel {
position: absolute;
top: calc(100% + 0.5rem);
left: 0;
z-index: 30;
min-width: 280px;
max-width: 400px;
max-height: 60vh;
overflow-y: auto;
padding: 0.5rem;
border-radius: 0.5rem;
background: hsl(20 8% 8%);
border: 1px solid hsl(20 8% 16%);
box-shadow: 0 8px 32px hsl(0 0% 0% / 0.4);
}
.toc-item {
display: block;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.8rem;
color: hsl(20 8% 65%);
text-decoration: none;
transition: all 0.15s;
line-height: 1.3;
}
.toc-item:hover {
background: hsl(20 8% 12%);
color: white;
}
.toc-item--h2 {
padding-left: 1.5rem;
font-size: 0.75rem;
color: hsl(20 8% 50%);
}
/* Dropdown transition */
.toc-dropdown-enter-active {
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
.toc-dropdown-leave-active {
transition: all 0.15s ease-in;
}
.toc-dropdown-enter-from,
.toc-dropdown-leave-to {
opacity: 0;
transform: translateY(-4px);
}
</style>

View File

@@ -0,0 +1,353 @@
<template>
<div class="axis-block">
<!-- Header -->
<div class="flex items-center gap-3 mb-6">
<div class="axis-icon" :class="`axis-icon--${color}`">
<div :class="iconClass(icon)" class="h-6 w-6" />
</div>
<h2 class="font-display text-2xl font-bold text-white">{{ title }}</h2>
</div>
<!-- Items grid -->
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div
v-for="(item, i) in items"
:key="i"
class="axis-item card-surface"
:class="{ 'axis-item--gestation': item.gestation }"
>
<!-- Clickable card body -->
<component
:is="itemTag(item)"
v-bind="itemAttrs(item)"
class="axis-item-body"
>
<!-- Item icon -->
<div v-if="item.icon" class="axis-item-icon" :class="`axis-item-icon--${color}`">
<span v-if="item.icon === 'g1'" class="axis-item-icon-g1">Ğ1</span>
<div v-else :class="iconClass(item.icon)" class="h-5 w-5" />
</div>
<h3 class="font-display text-lg font-semibold text-white mb-1">
{{ item.label }}
<span v-if="item.gestation" class="gestation-badge">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
</h3>
<p class="text-sm text-white/60 leading-relaxed">{{ item.description }}</p>
<!-- Presentation inline (projet en gestation) -->
<div v-if="item.presentation" class="axis-presentation-inline">
<div class="axis-pi-header">
<div class="i-lucide-sparkles h-3 w-3" />
<span class="axis-pi-title">{{ item.presentation.title }}</span>
</div>
<p class="axis-pi-text">{{ item.presentation.text }}</p>
</div>
</component>
<!-- Actions zone (separate from card link) -->
<div v-if="item.actions?.length" class="axis-actions">
<!-- Primary row -->
<div class="axis-actions-row">
<button
v-for="action in primaryActions(item.actions)"
:key="action.id"
class="axis-action-btn"
:class="{ 'axis-action-btn--highlight': action.highlight }"
@click.stop="handleAction(action.id)"
>
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
{{ action.label }}
</button>
</div>
<!-- Secondary row -->
<div v-if="secondaryActions(item.actions).length" class="axis-actions-secondary">
<component
:is="action.to ? resolveComponent('NuxtLink') : 'button'"
v-for="action in secondaryActions(item.actions)"
:key="action.label"
:to="action.to"
class="axis-action-btn axis-action-btn--secondary"
@click.stop="!action.to && handleAction(action.id)"
>
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
{{ action.label }}
</component>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface AxisAction {
id: string
label: string
icon: string
highlight?: boolean
secondary?: boolean
to?: string
}
interface AxisItem {
label: string
description: string
to?: string
href?: string
gestation?: boolean
icon?: string
actions?: AxisAction[]
presentation?: { title: string; text: string }
}
defineProps<{
title: string
icon: string
color?: 'primary' | 'accent'
items: AxisItem[]
}>()
const emit = defineEmits<{
'open-player': []
'open-pdf': []
'launch-gratewizard': []
}>()
function primaryActions(actions: AxisAction[]) {
return actions.filter(a => !a.secondary)
}
function secondaryActions(actions: AxisAction[]) {
return actions.filter(a => a.secondary)
}
function handleAction(id: string) {
if (id === 'open-player') emit('open-player')
else if (id === 'open-pdf') emit('open-pdf')
else if (id === 'launch-gratewizard') emit('launch-gratewizard')
}
function iconClass(name: string) {
return `i-lucide-${name}`
}
function itemTag(item: AxisItem) {
if (item.href) return 'a'
if (item.to) return resolveComponent('NuxtLink')
return 'div'
}
function itemAttrs(item: AxisItem) {
if (item.href) return { href: item.href, target: '_blank', rel: 'noopener noreferrer' }
if (item.to) return { to: item.to }
return {}
}
</script>
<style scoped>
.axis-block {
padding: 0;
}
.axis-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.75rem;
height: 2.75rem;
border-radius: 0.75rem;
flex-shrink: 0;
}
.axis-icon--primary {
background: hsl(var(--color-primary) / 0.12);
border: 1px solid hsl(var(--color-primary) / 0.2);
color: hsl(var(--color-primary));
}
.axis-icon--accent {
background: hsl(var(--color-accent) / 0.12);
border: 1px solid hsl(var(--color-accent) / 0.2);
color: hsl(var(--color-accent));
}
.axis-item {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
background: hsl(var(--color-surface));
transition: border-color 0.2s, box-shadow 0.2s;
/* overflow: visible pour laisser le tooltip sortir du cadre */
}
.axis-item:hover {
border-color: hsl(var(--color-primary) / 0.25);
box-shadow: 0 4px 24px hsl(var(--color-primary) / 0.06);
}
.axis-item--gestation {
opacity: 0.75;
}
.axis-item--gestation:hover {
opacity: 0.9;
}
.axis-item-body {
display: flex;
flex-direction: column;
padding: 1.25rem;
flex: 1;
text-decoration: none;
color: inherit;
}
.axis-item-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.5rem;
margin-bottom: 0.75rem;
}
.axis-item-icon--primary {
background: hsl(var(--color-primary) / 0.18);
color: hsl(var(--color-primary));
box-shadow: 0 0 14px hsl(var(--color-primary) / 0.15);
}
.axis-item-icon--accent {
background: hsl(var(--color-accent) / 0.18);
color: hsl(var(--color-accent));
box-shadow: 0 0 14px hsl(var(--color-accent) / 0.15);
}
.axis-item-icon-g1 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.1rem;
line-height: 1;
}
.gestation-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
margin-left: 0.5rem;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
background: hsl(var(--color-accent) / 0.12);
color: hsl(var(--color-accent));
font-size: 0.7rem;
font-weight: 500;
font-family: var(--font-mono);
vertical-align: middle;
}
.axis-actions {
display: flex;
flex-direction: column;
gap: 0;
border-top: 1px solid hsl(var(--color-text) / 0.06);
background: hsl(var(--color-bg) / 0.4);
border-bottom-left-radius: 0.75rem;
border-bottom-right-radius: 0.75rem;
}
.axis-actions-row {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
padding: 0.75rem 1.25rem;
}
.axis-actions-secondary {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
padding: 0.5rem 1.25rem 0.75rem;
border-top: 1px solid hsl(var(--color-text) / 0.04);
}
.axis-action-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border-radius: 0.5rem;
font-size: 0.8rem;
font-weight: 500;
color: hsl(var(--color-text) / 0.7);
background: hsl(var(--color-text) / 0.05);
border: 1px solid hsl(var(--color-text) / 0.1);
transition: all 0.2s;
cursor: pointer;
}
.axis-action-btn:hover {
color: hsl(var(--color-text));
background: hsl(var(--color-primary) / 0.12);
border-color: hsl(var(--color-primary) / 0.3);
}
.axis-action-btn--highlight {
color: hsl(var(--color-primary));
background: hsl(var(--color-primary) / 0.12);
border-color: hsl(var(--color-primary) / 0.25);
}
.axis-action-btn--highlight:hover {
background: hsl(var(--color-primary) / 0.2);
border-color: hsl(var(--color-primary) / 0.4);
}
.axis-action-btn--secondary {
color: hsl(var(--color-text) / 0.45);
background: transparent;
border-color: hsl(var(--color-text) / 0.06);
font-size: 0.75rem;
}
.axis-action-btn--secondary:hover {
color: hsl(var(--color-accent));
background: hsl(var(--color-accent) / 0.08);
border-color: hsl(var(--color-accent) / 0.2);
}
/* Presentation inline — projet en gestation, affiché dans la card */
.axis-presentation-inline {
margin-top: 0.75rem;
padding: 0.5rem 0.75rem;
border-radius: 8px;
background: hsl(var(--color-accent) / 0.07);
border: 1px solid hsl(var(--color-accent) / 0.18);
}
.axis-pi-header {
display: flex;
align-items: center;
gap: 0.3rem;
margin-bottom: 0.2rem;
color: hsl(var(--color-accent));
}
.axis-pi-title {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.02em;
}
.axis-pi-text {
font-size: 0.72rem;
color: hsl(var(--color-text) / 0.55);
line-height: 1.45;
margin: 0;
}
</style>

View File

@@ -0,0 +1,237 @@
<template>
<section class="section-padding">
<div class="container-content flex flex-col gap-16">
<UiScrollReveal>
<div id="numerique">
<HomeAxisBlock
v-if="axes?.numerique"
:title="axes.numerique.title"
:icon="axes.numerique.icon"
color="primary"
:items="axes.numerique.items"
/>
</div>
</UiScrollReveal>
<UiScrollReveal :delay="100">
<div id="economique">
<HomeAxisBlock
v-if="axes?.economie"
:title="axes.economie.title"
:icon="axes.economie.icon"
color="accent"
:items="axes.economie.items"
@open-player="$emit('open-player')"
@open-pdf="$emit('open-pdf')"
@launch-gratewizard="launchGW"
/>
<!-- Bloc GrateWizard dans la section économique -->
<button v-if="gw" class="gw-block mt-4" @click="launchGW">
<div class="gw-icon">
<div class="i-lucide-sparkles h-5 w-5" />
</div>
<div class="gw-text">
<h3 class="font-display text-base font-bold text-white sm:text-lg">
{{ gw.title }}
</h3>
<p class="text-white/55 text-xs sm:text-sm mt-0.5">
{{ gw.subtitle }}
</p>
</div>
<div class="gw-arrow">
<div class="i-lucide-arrow-up-right h-4 w-4" />
</div>
</button>
</div>
</UiScrollReveal>
<UiScrollReveal :delay="200">
<div id="citoyenne">
<HomeAxisBlock
v-if="axes?.politique"
:title="axes.politique.title"
:icon="axes.politique.icon"
color="primary"
:items="axes.politique.items"
/>
</div>
</UiScrollReveal>
<!-- Bloc Événement -->
<UiScrollReveal v-if="evenement" :delay="350">
<NuxtLink :to="evenement.to" class="event-block">
<div class="event-content">
<div class="event-icon">
<div class="i-lucide-calendar-heart h-7 w-7" />
</div>
<div>
<h2 class="font-display text-2xl font-bold text-white sm:text-3xl">
{{ evenement.title }}
</h2>
<p class="font-display text-xl text-white/70 sm:text-2xl">
{{ evenement.subtitle }}
</p>
</div>
</div>
<span v-if="evenement.gestation" class="event-badge">
<div class="i-lucide-flask-conical h-3.5 w-3.5" />
En gestation
</span>
</NuxtLink>
</UiScrollReveal>
</div>
</section>
</template>
<script setup lang="ts">
defineEmits<{
'open-player': []
'open-pdf': []
}>()
const { data: content } = await usePageContent('home')
const { launch } = useGrateWizard()
const axes = computed(() => (content.value as any)?.axes)
const gw = computed(() => (content.value as any)?.gratewizard)
const evenement = computed(() => (content.value as any)?.evenement)
function launchGW() {
launch()
}
</script>
<style scoped>
.event-block {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
padding: 2rem 2.5rem;
border-radius: 1rem;
border: 2px solid hsl(var(--color-accent) / 0.25);
background: linear-gradient(135deg, hsl(var(--color-accent) / 0.08), hsl(var(--color-primary) / 0.04));
transition: border-color 0.3s, box-shadow 0.3s;
text-decoration: none;
}
.event-block:hover {
border-color: hsl(var(--color-accent) / 0.45);
box-shadow: 0 0 40px hsl(var(--color-accent) / 0.08);
}
.event-content {
display: flex;
align-items: center;
gap: 1.25rem;
}
.event-icon {
display: flex;
align-items: center;
justify-content: center;
width: 3.5rem;
height: 3.5rem;
border-radius: 0.75rem;
background: hsl(var(--color-accent) / 0.15);
color: hsl(var(--color-accent));
flex-shrink: 0;
}
.event-badge {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
background: hsl(var(--color-accent) / 0.12);
color: hsl(var(--color-accent));
font-size: 0.75rem;
font-weight: 500;
font-family: var(--font-mono);
white-space: nowrap;
flex-shrink: 0;
}
/* GrateWizard block */
.gw-block {
display: flex;
align-items: center;
gap: 1rem;
width: 100%;
padding: 1.25rem 1.5rem;
border-radius: 0.75rem;
border: none;
background: linear-gradient(135deg, hsl(280 50% 20% / 0.35), hsl(260 40% 15% / 0.25));
box-shadow: 0 0 40px hsl(280 60% 50% / 0.06), inset 0 1px 0 hsl(280 60% 70% / 0.08);
cursor: pointer;
transition: all 0.3s ease;
text-align: left;
color: inherit;
}
.gw-block:hover {
background: linear-gradient(135deg, hsl(280 50% 24% / 0.45), hsl(260 40% 18% / 0.35));
box-shadow: 0 0 60px hsl(280 60% 50% / 0.12), inset 0 1px 0 hsl(280 60% 70% / 0.12);
transform: translateY(-2px);
}
.gw-block:active {
transform: translateY(0);
}
.gw-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 0.625rem;
background: hsl(280 60% 55% / 0.18);
color: hsl(280 60% 72%);
flex-shrink: 0;
box-shadow: 0 0 20px hsl(280 60% 50% / 0.15);
}
.gw-text {
flex: 1;
min-width: 0;
}
.gw-arrow {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 50%;
background: hsl(280 60% 55% / 0.1);
color: hsl(280 60% 65%);
flex-shrink: 0;
transition: all 0.2s;
}
.gw-block:hover .gw-arrow {
background: hsl(280 60% 55% / 0.2);
color: hsl(280 60% 80%);
transform: translate(2px, -2px);
}
@media (max-width: 640px) {
.event-block {
flex-direction: column;
align-items: flex-start;
padding: 1.5rem;
}
.gw-block {
padding: 1.25rem;
gap: 1rem;
}
.gw-arrow {
display: none;
}
}
</style>

View File

@@ -5,23 +5,6 @@
<!-- Book cover --> <!-- Book cover -->
<UiScrollReveal> <UiScrollReveal>
<div class="book-cover-wrapper relative"> <div class="book-cover-wrapper relative">
<!-- Shadok pumper -->
<svg class="shadok-pumper" viewBox="0 0 200 240" fill="none" aria-hidden="true">
<ellipse cx="100" cy="130" rx="55" ry="65" fill="currentColor" opacity="0.9"/>
<ellipse cx="100" cy="60" rx="30" ry="28" fill="currentColor" opacity="0.85"/>
<circle cx="88" cy="54" r="6" fill="currentColor" opacity="0.2"/>
<circle cx="112" cy="54" r="6" fill="currentColor" opacity="0.2"/>
<circle cx="90" cy="53" r="2.5" fill="currentColor" opacity="0.5"/>
<circle cx="114" cy="53" r="2.5" fill="currentColor" opacity="0.5"/>
<polygon points="100,68 115,78 85,78" fill="currentColor" opacity="0.6"/>
<line x1="80" y1="192" x2="70" y2="230" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity="0.7"/>
<line x1="120" y1="192" x2="130" y2="230" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity="0.7"/>
<line x1="70" y1="230" x2="55" y2="232" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.5"/>
<line x1="130" y1="230" x2="145" y2="232" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.5"/>
<line x1="155" y1="110" x2="190" y2="90" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity="0.6"/>
<line x1="190" y1="90" x2="190" y2="120" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity="0.6"/>
<rect x="180" y="118" width="18" height="40" rx="3" fill="currentColor" opacity="0.4"/>
</svg>
<div class="book-cover-3d"> <div class="book-cover-3d">
<img <img
:src="content?.book.coverImage" :src="content?.book.coverImage"
@@ -48,16 +31,11 @@
</UiScrollReveal> </UiScrollReveal>
<UiScrollReveal :delay="200"> <UiScrollReveal :delay="200">
<div class="mt-8 flex flex-col gap-3 sm:flex-row sm:gap-4"> <BookActions
<UiBaseButton @click="$emit('open-player')"> :show-chapters="showChapters"
<div class="i-lucide-play mr-2 h-5 w-5" /> @open-player="$emit('open-player')"
{{ content?.book.cta.player }} @open-pdf="$emit('open-pdf')"
</UiBaseButton> />
<UiBaseButton variant="accent" @click="$emit('open-pdf')">
<div class="i-lucide-book-open mr-2 h-5 w-5" />
{{ content?.book.cta.pdf }}
</UiBaseButton>
</div>
</UiScrollReveal> </UiScrollReveal>
</div> </div>
</div> </div>
@@ -66,6 +44,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
withDefaults(defineProps<{
showChapters?: boolean
}>(), {
showChapters: true,
})
defineEmits<{ defineEmits<{
'open-player': [] 'open-player': []
'open-pdf': [] 'open-pdf': []
@@ -111,25 +95,4 @@ const { data: content } = await usePageContent('home')
.heading-section { .heading-section {
font-size: clamp(1.625rem, 4vw, 2.125rem); font-size: clamp(1.625rem, 4vw, 2.125rem);
} }
.shadok-pumper {
position: absolute;
right: 3%;
bottom: 8%;
width: clamp(90px, 12vw, 180px);
opacity: 0.28;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float 10s ease-in-out infinite;
z-index: 1;
}
@keyframes shadok-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@media (max-width: 768px) {
.shadok-pumper { display: none; }
}
</style> </style>

View File

@@ -1,110 +0,0 @@
<template>
<section class="section-padding">
<div class="container-content">
<UiScrollReveal>
<div class="gw-card relative overflow-hidden">
<!-- Shadok blob -->
<svg class="shadok-blob" viewBox="0 0 200 180" fill="none" aria-hidden="true">
<path d="M60 90 Q30 50 70 30 Q110 10 140 40 Q180 60 170 100 Q165 140 130 155 Q90 170 55 145 Q25 125 60 90Z" fill="currentColor" opacity="0.12"/>
<path d="M60 90 Q30 50 70 30 Q110 10 140 40 Q180 60 170 100 Q165 140 130 155 Q90 170 55 145 Q25 125 60 90Z" stroke="currentColor" stroke-width="1.5" opacity="0.2"/>
<circle cx="100" cy="80" r="8" fill="currentColor" opacity="0.08"/>
<circle cx="120" cy="110" r="6" fill="currentColor" opacity="0.06"/>
<circle cx="80" cy="105" r="5" fill="currentColor" opacity="0.07"/>
<circle cx="95" cy="72" r="3" fill="currentColor" opacity="0.3"/>
<circle cx="108" cy="70" r="3" fill="currentColor" opacity="0.3"/>
<circle cx="96" cy="71" r="1.2" fill="currentColor" opacity="0.5"/>
<circle cx="109" cy="69" r="1.2" fill="currentColor" opacity="0.5"/>
</svg>
<div class="flex flex-col items-center text-center gap-4 md:flex-row md:text-left md:gap-8 relative z-1">
<!-- Icon -->
<div class="gw-icon-wrapper">
<div class="i-lucide-sparkles h-8 w-8 text-amber-400" />
</div>
<!-- Content -->
<div class="flex-1">
<span class="inline-block mb-2 rounded-full bg-amber-400/15 px-3 py-0.5 font-mono text-xs tracking-widest text-amber-400 uppercase">
{{ content?.grateWizardTeaser.kicker }}
</span>
<h3 class="heading-h3 font-display font-bold text-white">
{{ content?.grateWizardTeaser.title }}
</h3>
<p class="mt-2 text-sm text-white/60 md:text-base leading-relaxed">
{{ content?.grateWizardTeaser.description }}
</p>
</div>
<!-- CTAs -->
<div class="shrink-0 flex flex-col gap-2">
<UiBaseButton :href="url" target="_blank" @click="launch">
<div class="i-lucide-external-link mr-2 h-4 w-4" />
{{ content?.grateWizardTeaser.cta.launch }}
</UiBaseButton>
<UiBaseButton variant="ghost" :to="content?.grateWizardTeaser.cta.more.to">
{{ content?.grateWizardTeaser.cta.more.label }}
<div class="i-lucide-arrow-right ml-2 h-4 w-4" />
</UiBaseButton>
</div>
</div>
</div>
</UiScrollReveal>
</div>
</section>
</template>
<script setup lang="ts">
const { url, launch } = useGrateWizard()
const { data: content } = await usePageContent('home')
</script>
<style scoped>
.gw-card {
border: 1px solid hsl(40 80% 50% / 0.2);
border-radius: 1rem;
padding: 1.5rem 2rem;
background: linear-gradient(135deg, hsl(40 80% 50% / 0.05), hsl(40 80% 50% / 0.02));
box-shadow: 0 0 40px hsl(40 80% 50% / 0.05);
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.gw-card:hover {
border-color: hsl(40 80% 50% / 0.35);
box-shadow: 0 0 60px hsl(40 80% 50% / 0.1);
}
.heading-h3 {
font-size: clamp(1.25rem, 3vw, 1.625rem);
}
.gw-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 3.5rem;
height: 3.5rem;
border-radius: 0.75rem;
background: hsl(40 80% 50% / 0.1);
border: 1px solid hsl(40 80% 50% / 0.15);
flex-shrink: 0;
}
.shadok-blob {
position: absolute;
right: -2%;
top: -20%;
width: clamp(120px, 16vw, 220px);
opacity: 0.35;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-drift 12s ease-in-out infinite;
}
@keyframes shadok-drift {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-8px) rotate(3deg); }
}
@media (max-width: 768px) {
.shadok-blob { display: none; }
}
</style>

View File

@@ -1,8 +1,8 @@
<template> <template>
<section class="relative overflow-hidden section-padding"> <section class="relative overflow-hidden section-padding hero-section">
<!-- Background gradient --> <!-- Background gradient -->
<div class="absolute inset-0 bg-gradient-to-b from-primary/10 via-transparent to-surface-bg" /> <div class="absolute inset-0 bg-gradient-to-b from-primary/10 via-transparent to-surface-bg" />
<div class="absolute inset-0 bg-[radial-gradient(ellipse_at_top,hsl(12_76%_48%/0.15),transparent_70%)]" /> <div class="absolute inset-0 bg-[radial-gradient(ellipse_at_top,hsl(12_76%_48%/0.12),transparent_70%)]" />
<!-- Shadok bird decoration --> <!-- Shadok bird decoration -->
<svg class="shadok-bird" viewBox="0 0 180 260" fill="none" aria-hidden="true"> <svg class="shadok-bird" viewBox="0 0 180 260" fill="none" aria-hidden="true">
@@ -23,7 +23,7 @@
<path d="M48 105 Q25 102 12 100" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3" fill="none"/> <path d="M48 105 Q25 102 12 100" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3" fill="none"/>
</svg> </svg>
<!-- Shadok boulanger: character with oven and bread --> <!-- Shadok boulanger -->
<svg class="shadok-boulanger" viewBox="0 0 240 300" fill="none" aria-hidden="true"> <svg class="shadok-boulanger" viewBox="0 0 240 300" fill="none" aria-hidden="true">
<ellipse cx="120" cy="155" rx="40" ry="48" fill="currentColor" opacity="0.85"/> <ellipse cx="120" cy="155" rx="40" ry="48" fill="currentColor" opacity="0.85"/>
<circle cx="120" cy="92" r="25" fill="currentColor" opacity="0.8"/> <circle cx="120" cy="92" r="25" fill="currentColor" opacity="0.8"/>
@@ -44,41 +44,11 @@
<!-- Content --> <!-- Content -->
<div class="container-content relative z-10 px-4"> <div class="container-content relative z-10 px-4">
<div class="mx-auto max-w-3xl text-center"> <div class="mx-auto max-w-2xl">
<UiScrollReveal> <HomeTypewriterText
<p class="mb-3 font-mono text-sm tracking-widest text-primary uppercase"> v-if="hero"
{{ content?.hero.kicker }} :hero="hero"
</p> />
</UiScrollReveal>
<UiScrollReveal :delay="100">
<h1 class="font-display font-extrabold leading-tight tracking-tight">
<span class="hero-title text-gradient">{{ content?.hero.title }}</span>
</h1>
</UiScrollReveal>
<UiScrollReveal :delay="200">
<p class="mt-6 text-lg leading-relaxed text-white/60 md:text-xl">
{{ content?.hero.subtitle }}
</p>
</UiScrollReveal>
<UiScrollReveal :delay="300">
<p class="mt-4 text-base leading-relaxed text-white/45">
{{ content?.hero.footnote }}
</p>
</UiScrollReveal>
<UiScrollReveal :delay="400">
<div class="mt-8 flex justify-center">
<UiBaseButton variant="ghost" :to="content?.hero.cta.to">
{{ content?.hero.cta.label }}
<div class="i-lucide-arrow-right ml-2 h-4 w-4" />
</UiBaseButton>
</div>
</UiScrollReveal>
<HomeMessages />
</div> </div>
</div> </div>
</section> </section>
@@ -86,11 +56,27 @@
<script setup lang="ts"> <script setup lang="ts">
const { data: content } = await usePageContent('home') const { data: content } = await usePageContent('home')
const hero = computed(() => {
const raw = (content.value as any)?.hero
if (!raw) return null
return {
heading: Array.isArray(raw.heading) ? raw.heading : [],
citations: Array.isArray(raw.citations) ? raw.citations : [],
approach: raw.approach || '',
axes: Array.isArray(raw.axes) ? raw.axes : [],
audience: raw.audience || '',
addressees: Array.isArray(raw.addressees) ? raw.addressees : [],
}
})
</script> </script>
<style scoped> <style scoped>
.hero-title { .hero-section {
font-size: clamp(2.25rem, 7vw, 4rem); min-height: 70vh;
display: flex;
align-items: center;
justify-content: center;
} }
.shadok-bird { .shadok-bird {

View File

@@ -1,51 +1,48 @@
<template> <template>
<div class="mt-16"> <section class="section-padding">
<div class="container-content mx-auto max-w-3xl">
<!-- Formulaire --> <!-- Formulaire -->
<UiScrollReveal :delay="500"> <UiScrollReveal>
<div class="message-form-card"> <div class="message-form-card">
<h3 class="font-display text-lg font-bold text-white mb-4">Laisser un message</h3> <h3 class="font-display text-lg font-bold text-white mb-4">Laisser un message</h3>
<form v-if="!submitted" class="space-y-3" @submit.prevent="send"> <form v-if="!submitted" class="space-y-3" @submit.prevent="send">
<div class="grid gap-3 sm:grid-cols-2">
<input <input
v-model="form.author" v-model="form.author"
type="text" type="text"
placeholder="Votre nom *" placeholder="Votre nom"
required
class="msg-input" class="msg-input"
/> />
<input <p class="text-white/30 text-xs -mt-1 px-1">Pour recevoir une réponse, laissez votre e-mail dans le message.</p>
v-model="form.email"
type="email"
placeholder="Email (optionnel)"
class="msg-input"
/>
</div>
<textarea <textarea
v-model="form.text" v-model="form.text"
placeholder="Votre message *" placeholder="Votre message"
required
rows="3" rows="3"
class="msg-input resize-none" class="msg-input resize-none"
/> />
<div class="flex justify-end"> <div class="flex justify-end">
<button type="submit" class="btn-primary text-sm" :disabled="sending"> <button type="submit" class="btn-primary text-sm" :disabled="sending || !canSend">
<div v-if="sending" class="i-lucide-loader-2 h-4 w-4 animate-spin mr-2" /> <div v-if="sending" class="i-lucide-loader-2 h-4 w-4 animate-spin mr-2" />
Envoyer Envoyer
</button> </button>
</div> </div>
</form> </form>
<div v-else class="text-center py-4"> <div v-else class="text-center py-6">
<div class="i-lucide-check-circle h-8 w-8 text-green-400 mx-auto mb-2" /> <div class="i-lucide-heart-handshake h-10 w-10 text-primary mx-auto mb-3" />
<p class="text-white/80">Merci pour votre message !</p> <p class="text-white/90 font-display text-lg font-semibold">Merci pour votre message !</p>
<p class="text-white/40 text-sm mt-1">Il sera visible après modération.</p> <p class="text-white/55 text-sm mt-2 max-w-md mx-auto leading-relaxed">
Il sera lu et traité dans un délai... humainement raisonnable.
</p>
<p class="text-primary/60 text-xs mt-4 italic">
Chaque message est un premier pas dans l'aventure.
</p>
</div> </div>
</div> </div>
</UiScrollReveal> </UiScrollReveal>
<!-- 2 derniers messages publiés --> <!-- 2 derniers messages publiés -->
<UiScrollReveal v-if="messages?.length" :delay="600"> <UiScrollReveal v-if="messages?.length" :delay="100">
<div class="mt-8 space-y-4"> <div class="mt-8 space-y-4">
<h3 class="font-display text-lg font-bold text-white/80 text-center">Derniers messages</h3> <h3 class="font-display text-lg font-bold text-white/80 text-center">Derniers messages</h3>
<div v-for="msg in messages.slice(0, 2)" :key="msg.id" class="message-card"> <div v-for="msg in messages.slice(0, 2)" :key="msg.id" class="message-card">
@@ -65,16 +62,20 @@
</div> </div>
</UiScrollReveal> </UiScrollReveal>
</div> </div>
</section>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { data: messages } = await useFetch('/api/messages') const { data: messages } = await useFetch('/api/messages')
const form = reactive({ author: '', email: '', text: '' }) const form = reactive({ author: '', text: '' })
const sending = ref(false) const sending = ref(false)
const submitted = ref(false) const submitted = ref(false)
const canSend = computed(() => form.text.trim().length > 0)
async function send() { async function send() {
if (!canSend.value) return
sending.value = true sending.value = true
try { try {
await $fetch('/api/messages', { method: 'POST', body: form }) await $fetch('/api/messages', { method: 'POST', body: form })

View File

@@ -0,0 +1,279 @@
<template>
<div class="hero-content">
<!-- 5-line heading with alternating typography -->
<div class="hero-heading">
<h1 v-if="hero.heading?.length" class="hero-lines">
<span
v-for="(line, i) in hero.heading"
:key="i"
class="hero-line"
:class="`hero-line--${i + 1}`"
>{{ line }}</span>
</h1>
</div>
<!-- Discrete aside block -->
<details class="hero-aside">
<summary class="hero-aside-toggle">En savoir plus</summary>
<div class="hero-aside-body">
<!-- Citations -->
<blockquote v-if="hero.citations?.length" class="hero-citations">
<p v-for="(cite, i) in hero.citations" :key="i" class="hero-cite">
{{ cite }}
</p>
</blockquote>
<!-- Approach + Axes -->
<div v-if="hero.approach" class="hero-approach">
<p class="hero-approach-text">{{ hero.approach }}</p>
<dl v-if="hero.axes?.length" class="hero-axes">
<div v-for="(axis, i) in hero.axes" :key="i" class="hero-axis">
<dt>{{ axis.label }}</dt>
<dd>{{ axis.value }}</dd>
</div>
</dl>
</div>
<!-- Audience + Addressees -->
<div v-if="hero.audience" class="hero-approach hero-audience">
<p class="hero-approach-text">{{ hero.audience }}</p>
<dl v-if="hero.addressees?.length" class="hero-axes">
<div v-for="(item, i) in hero.addressees" :key="i" class="hero-axis">
<dt>{{ item.label }}</dt>
<dd>{{ item.value }}</dd>
</div>
</dl>
</div>
</div>
</details>
</div>
</template>
<script setup lang="ts">
interface HeroAxis {
label: string
value: string
}
interface HeroData {
heading: string[]
citations: string[]
approach: string
axes: HeroAxis[]
audience: string
addressees: HeroAxis[]
}
defineProps<{
hero: HeroData
}>()
</script>
<style scoped>
.hero-content {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
}
/* ── Heading — 5 lines ── */
.hero-heading {
margin-bottom: 1.5rem;
}
.hero-lines {
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.1em;
}
.hero-line {
display: block;
font-family: var(--font-display);
line-height: 1.3;
}
/* l1: Construire une autonomie collective. — bold, large */
.hero-line--1 {
font-weight: 700;
font-size: clamp(1.45rem, 3.8vw, 2.5rem);
color: hsl(var(--color-text) / 0.95);
letter-spacing: -0.02em;
}
/* l2: à l'échelle des bassins de vie — light, same size, softer */
.hero-line--2 {
font-weight: 300;
font-size: clamp(1.3rem, 3.2vw, 2.1rem);
color: hsl(var(--color-text) / 0.55);
letter-spacing: -0.01em;
}
/* l3: Pousser les curseurs — accent color, medium */
.hero-line--3 {
font-weight: 600;
font-size: clamp(1.1rem, 2.6vw, 1.6rem);
color: hsl(var(--color-accent));
margin-top: 0.5em;
letter-spacing: 0.02em;
}
/* l4: Autonomie numérique, économique, citoyenne. — small-caps feel */
.hero-line--4 {
font-family: var(--font-sans);
font-weight: 500;
font-size: clamp(0.9rem, 2vw, 1.15rem);
color: hsl(var(--color-text) / 0.65);
letter-spacing: 0.06em;
text-transform: uppercase;
margin-top: 0.15em;
}
/* l5: — s'en donner les moyens — — italic, same tone as l4 */
.hero-line--5 {
font-style: italic;
font-weight: 400;
font-size: clamp(0.95rem, 2.2vw, 1.2rem);
color: hsl(var(--color-text) / 0.65);
margin-top: 0.3em;
}
/* ── Discrete aside block ── */
.hero-aside {
width: 100%;
max-width: 36em;
}
.hero-aside-toggle {
display: inline-flex;
align-items: center;
gap: 0.4rem;
margin: 0 auto;
padding: 0.35rem 0.85rem;
border-radius: 9999px;
font-family: var(--font-sans);
font-size: 0.78rem;
font-weight: 500;
color: hsl(var(--color-text) / 0.35);
border: 1px solid hsl(var(--color-text) / 0.08);
cursor: pointer;
transition: all 0.2s;
list-style: none;
}
.hero-aside-toggle::-webkit-details-marker {
display: none;
}
.hero-aside-toggle::before {
content: '▸';
font-size: 0.65em;
transition: transform 0.2s;
}
.hero-aside[open] .hero-aside-toggle::before {
transform: rotate(90deg);
}
.hero-aside-toggle:hover {
color: hsl(var(--color-text) / 0.55);
border-color: hsl(var(--color-text) / 0.15);
}
.hero-aside-body {
margin-top: 1.25rem;
animation: aside-reveal 0.3s ease;
}
@keyframes aside-reveal {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Citations ── */
.hero-citations {
margin: 0 0 1.25rem;
padding: 0.6rem 0 0.6rem 1rem;
border-left: 2px solid hsl(var(--color-accent) / 0.3);
text-align: left;
}
.hero-cite {
font-family: var(--font-display);
font-style: italic;
font-weight: 400;
font-size: 0.92rem;
line-height: 1.55;
color: hsl(var(--color-text) / 0.45);
margin: 0;
}
.hero-cite + .hero-cite {
margin-top: 0.25rem;
}
/* ── Approach + Axes ── */
.hero-approach {
text-align: center;
}
.hero-audience {
margin-top: 1rem;
padding-top: 0.75rem;
border-top: 1px solid hsl(var(--color-text) / 0.07);
}
.hero-approach-text {
font-family: var(--font-sans);
font-size: 0.82rem;
font-weight: 500;
letter-spacing: 0.03em;
text-transform: uppercase;
color: hsl(var(--color-text) / 0.35);
margin: 0 0 0.6rem;
}
.hero-axes {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
margin: 0;
}
.hero-axis {
display: flex;
align-items: baseline;
gap: 0.4rem;
justify-content: center;
}
.hero-axis dt {
font-family: var(--font-display);
font-weight: 600;
font-size: 0.9rem;
color: hsl(var(--color-primary));
}
.hero-axis dt::after {
content: ' →';
color: hsl(var(--color-text) / 0.2);
font-weight: 400;
}
.hero-axis dd {
font-family: var(--font-sans);
font-size: 0.88rem;
color: hsl(var(--color-text) / 0.5);
margin: 0;
}
</style>

View File

@@ -16,8 +16,10 @@
<!-- Desktop navigation --> <!-- Desktop navigation -->
<nav class="hidden md:flex items-center gap-1" aria-label="Navigation principale"> <nav class="hidden md:flex items-center gap-1" aria-label="Navigation principale">
<!-- Autonomie : prefix + axis buttons -->
<span class="nav-prefix">Autonomie&thinsp;:</span>
<NuxtLink <NuxtLink
v-for="item in site?.navigation" v-for="item in axes"
:key="item.to" :key="item.to"
:to="item.to" :to="item.to"
class="btn-ghost text-sm" class="btn-ghost text-sm"
@@ -25,6 +27,19 @@
> >
{{ item.label }} {{ item.label }}
</NuxtLink> </NuxtLink>
<!-- Separator + extra nav -->
<span class="nav-sep" />
<NuxtLink
v-for="item in extra"
:key="item.to"
:to="item.to"
class="btn-ghost btn-ghost--muted text-sm"
active-class="!text-[hsl(var(--color-text))] bg-[hsl(var(--color-text)/0.06)]"
>
{{ item.label }}
</NuxtLink>
<UiPaletteSelector /> <UiPaletteSelector />
</nav> </nav>
@@ -39,13 +54,17 @@
</div> </div>
<!-- Mobile menu --> <!-- Mobile menu -->
<LayoutNavMobile v-model:open="isMobileMenuOpen" :nav="site?.navigation ?? []" /> <LayoutNavMobile v-model:open="isMobileMenuOpen" :nav="allNav" />
</header> </header>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { data: site } = await useSiteContent() const { data: site } = await useSiteContent()
const isMobileMenuOpen = ref(false) const isMobileMenuOpen = ref(false)
const axes = computed(() => (site.value as any)?.navigation?.axes ?? [])
const extra = computed(() => (site.value as any)?.navigation?.extra ?? [])
const allNav = computed(() => [...axes.value, ...extra.value])
</script> </script>
<style scoped> <style scoped>
@@ -65,9 +84,34 @@ const isMobileMenuOpen = ref(false)
.logo-text { .logo-text {
font-family: var(--font-display); font-family: var(--font-display);
font-weight: 600; font-weight: 400;
font-size: 1.15rem; font-size: 1.25rem;
letter-spacing: 0.02em; letter-spacing: 0.04em;
color: hsl(var(--color-primary)); background-image: linear-gradient(to right, hsl(var(--color-primary)), hsl(var(--color-accent)));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.nav-prefix {
font-family: var(--font-display);
font-size: 0.92rem;
font-weight: 500;
font-style: italic;
letter-spacing: 0.03em;
color: hsl(var(--color-text) / 0.4);
margin-right: 0.125rem;
}
.nav-sep {
display: block;
width: 1px;
height: 1.25rem;
background: hsl(var(--color-text) / 0.12);
margin: 0 0.375rem;
}
.btn-ghost--muted {
color: hsl(var(--color-text) / 0.45);
} }
</style> </style>

View File

@@ -3,7 +3,7 @@
<span <span
v-for="song in songs" v-for="song in songs"
:key="song.id" :key="song.id"
class="inline-flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary/70" class="inline-flex items-center gap-1 rounded-full border border-primary/20 bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary/80"
> >
<div class="i-lucide-music h-2.5 w-2.5" /> <div class="i-lucide-music h-2.5 w-2.5" />
{{ song.title }} {{ song.title }}
@@ -17,7 +17,7 @@ const props = defineProps<{
}>() }>()
const bookData = useBookData() const bookData = useBookData()
await bookData.init() bookData.init()
const songs = computed(() => bookData.getChapterSongs(props.chapterSlug)) const songs = computed(() => bookData.getChapterSongs(props.chapterSlug))
</script> </script>

View File

@@ -1,5 +1,5 @@
import type { Song } from '~/types/song' import type { Song } from '~/types/song'
import type { ChapterSongLink, BookConfig } from '~/types/book' import type { ChapterSongLink, ChapterPageLink, BookConfig } from '~/types/book'
let _configCache: BookConfig | null = null let _configCache: BookConfig | null = null
@@ -13,9 +13,11 @@ async function loadConfig(): Promise<BookConfig> {
author: parsed.book.author, author: parsed.book.author,
description: parsed.book.description, description: parsed.book.description,
coverImage: parsed.book.coverImage, coverImage: parsed.book.coverImage,
pdfFile: parsed.book.pdfFile,
chapters: [], chapters: [],
songs: parsed.songs as Song[], songs: parsed.songs as Song[],
chapterSongs: parsed.chapterSongs as ChapterSongLink[], chapterSongs: parsed.chapterSongs as ChapterSongLink[],
chapterPages: (parsed.chapterPages ?? []) as ChapterPageLink[],
defaultPlaylistOrder: parsed.defaultPlaylistOrder as string[], defaultPlaylistOrder: parsed.defaultPlaylistOrder as string[],
} }
@@ -76,6 +78,14 @@ export function useBookData() {
return link?.chapterSlug return link?.chapterSlug
} }
function getChapterPage(chapterSlug: string): number | undefined {
return config.value?.chapterPages.find(cp => cp.chapterSlug === chapterSlug)?.page
}
function getPdfUrl(): string {
return config.value?.pdfFile || '/pdf/une-economie-du-don.pdf'
}
function getBookMeta() { function getBookMeta() {
if (!config.value) return null if (!config.value) return null
return { return {
@@ -98,5 +108,7 @@ export function useBookData() {
getChapterForSong, getChapterForSong,
getPlaylistOrder, getPlaylistOrder,
getBookMeta, getBookMeta,
getChapterPage,
getPdfUrl,
} }
} }

View File

@@ -1,16 +1,10 @@
export function useGrateWizard() { export function useGrateWizard() {
const appConfig = useAppConfig() const appConfig = useAppConfig()
const { url, popup } = appConfig.gratewizard as { url: string; popup: { width: number; height: number } } const { url } = appConfig.gratewizard as { url: string; popup: { width: number; height: number } }
function launch(e?: Event) { function launch(e?: Event) {
const left = Math.round((window.screen.width - popup.width) / 2) window.open(url, '_blank', 'noopener,noreferrer')
const top = Math.round((window.screen.height - popup.height) / 2) e?.preventDefault()
const win = window.open(
url,
'grateWizard',
`width=${popup.width},height=${popup.height},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=no,scrollbars=yes,resizable=yes`,
)
if (win) e?.preventDefault()
} }
return { url, launch } return { url, launch }

View File

@@ -0,0 +1,20 @@
/**
* Umami analytics wrapper — safe server-side, no-op when not configured.
* Usage: const { track } = useTracking()
* track('player:open')
* track('axis:navigate', { axis: 'numerique' })
*/
export function useTracking() {
const runtimeConfig = useRuntimeConfig()
const enabled = !!runtimeConfig.public.umamiWebsiteId
function track(event: string, data?: Record<string, unknown>) {
if (!import.meta.client || !enabled) return
const umami = (window as Record<string, unknown>).umami as
| { track: (event: string, data?: unknown) => void }
| undefined
umami?.track(event, data)
}
return { track, enabled }
}

View File

@@ -1,8 +1,27 @@
<template> <template>
<div class="app-layout grid grid-cols-1 min-h-dvh"> <div class="app-layout grid grid-cols-1 min-h-dvh">
<LayoutTheHeader /> <LayoutTheHeader />
<main> <main class="app-main">
<slot /> <slot />
<!-- (Increase, #42) sceau hexagramme -->
<svg class="app-seal" viewBox="0 0 130 100" fill="currentColor" aria-hidden="true">
<!-- Line 6 (top) yang -->
<rect x="5" y="5" width="120" height="5" rx="1"/>
<!-- Line 5 yang -->
<rect x="5" y="22" width="120" height="5" rx="1"/>
<!-- Line 4 yin -->
<rect x="5" y="39" width="49" height="5" rx="1"/>
<rect x="76" y="39" width="49" height="5" rx="1"/>
<!-- Line 3 yin -->
<rect x="5" y="56" width="49" height="5" rx="1"/>
<rect x="76" y="56" width="49" height="5" rx="1"/>
<!-- Line 2 yin -->
<rect x="5" y="73" width="49" height="5" rx="1"/>
<rect x="76" y="73" width="49" height="5" rx="1"/>
<!-- Line 1 (bottom) yang -->
<rect x="5" y="90" width="120" height="5" rx="1"/>
</svg>
</main> </main>
<LayoutTheFooter /> <LayoutTheFooter />
</div> </div>
@@ -12,4 +31,15 @@
.app-layout { .app-layout {
grid-template-rows: auto 1fr auto; grid-template-rows: auto 1fr auto;
} }
/* === Seal — 益 Yì (Increase) === */
.app-seal {
display: block;
width: 44px;
margin: 2rem 1.5rem 1rem auto;
color: hsl(var(--color-accent));
opacity: 0.28;
filter: drop-shadow(1px 1px 0.5px rgba(0,0,0,0.25))
drop-shadow(-0.5px -0.5px 0.5px rgba(255,255,255,0.15));
}
</style> </style>

View File

@@ -209,7 +209,7 @@ async function save() {
.field-label { .field-label {
display: block; display: block;
font-size: 0.75rem; font-size: 0.75rem;
color: hsl(20 8% 50%); color: hsl(var(--color-text-muted));
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
@@ -217,15 +217,15 @@ async function save() {
width: 100%; width: 100%;
padding: 0.5rem 0.625rem; padding: 0.5rem 0.625rem;
border-radius: 0.375rem; border-radius: 0.375rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-size: 0.85rem; font-size: 0.85rem;
} }
.field-input:focus { .field-input:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
} }
/* ── Song tags ── */ /* ── Song tags ── */
@@ -233,45 +233,45 @@ async function save() {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
border-radius: 9999px; border-radius: 9999px;
border: 1px solid hsl(20 8% 22%); border: 1px solid hsl(var(--color-surface-light));
transition: all 0.15s; transition: all 0.15s;
overflow: hidden; overflow: hidden;
} }
.song-tag:hover { .song-tag:hover {
border-color: hsl(12 76% 48% / 0.4); border-color: hsl(var(--color-primary) / 0.4);
} }
.song-tag--active { .song-tag--active {
border-color: hsl(12 76% 48% / 0.6); border-color: hsl(var(--color-primary) / 0.6);
background: hsl(12 76% 48% / 0.08); background: hsl(var(--color-primary) / 0.08);
} }
.song-tag--primary { .song-tag--primary {
border-color: hsl(45 90% 55%); border-color: hsl(var(--color-accent));
background: hsl(45 90% 55% / 0.08); background: hsl(var(--color-accent) / 0.08);
} }
.song-tag-label { .song-tag-label {
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
background: none; background: none;
border: none; border: none;
color: hsl(20 8% 50%); color: hsl(var(--color-text-muted));
font-size: 0.8rem; font-size: 0.8rem;
cursor: pointer; cursor: pointer;
transition: color 0.15s; transition: color 0.15s;
} }
.song-tag--active .song-tag-label { .song-tag--active .song-tag-label {
color: hsl(12 76% 68%); color: hsl(var(--color-primary));
} }
.song-tag--primary .song-tag-label { .song-tag--primary .song-tag-label {
color: hsl(45 90% 65%); color: hsl(var(--color-accent));
} }
.song-tag-label:hover { .song-tag-label:hover {
color: white; color: hsl(var(--color-text));
} }
.song-star { .song-star {
@@ -281,16 +281,16 @@ async function save() {
padding: 0.375rem 0 0.375rem 0.625rem; padding: 0.375rem 0 0.375rem 0.625rem;
background: none; background: none;
border: none; border: none;
color: hsl(20 8% 30%); color: hsl(var(--color-text-muted));
cursor: pointer; cursor: pointer;
transition: color 0.15s; transition: color 0.15s;
} }
.song-star:hover { .song-star:hover {
color: hsl(45 90% 55%); color: hsl(var(--color-accent));
} }
.song-star--active { .song-star--active {
color: hsl(45 90% 55%); color: hsl(var(--color-accent));
} }
</style> </style>

View File

@@ -1,10 +1,34 @@
<template> <template>
<div> <div>
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h1 class="font-display text-2xl font-bold text-white">Chapitres</h1> <h1 class="font-display text-2xl font-bold text-white">Livre &amp; chapitres</h1>
<AdminSaveButton :saving="saving" :saved="saved" @save="saveOrder" /> <AdminSaveButton :saving="saving" :saved="saved" @save="saveOrder" />
</div> </div>
<!-- PDF config -->
<div class="pdf-section">
<h2 class="font-display text-sm font-semibold text-white/60 mb-3">PDF du livre</h2>
<div class="flex items-end gap-3">
<div class="flex-1">
<label class="block text-xs text-white/40 mb-1">Chemin du fichier PDF</label>
<input
v-model="pdfPath"
class="admin-input w-full font-mono text-xs"
placeholder="/pdf/une-economie-du-don.pdf"
/>
</div>
<button class="save-pdf-btn" @click="savePdfConfig" :disabled="savingPdf">
<div v-if="savingPdf" class="i-lucide-loader-2 h-3.5 w-3.5 animate-spin" />
<div v-else-if="savedPdf" class="i-lucide-check h-3.5 w-3.5" />
<div v-else class="i-lucide-save h-3.5 w-3.5" />
{{ savedPdf ? 'Enregistré' : 'Enregistrer' }}
</button>
</div>
<p class="text-xs text-white/30 mt-2">
Uploadez le PDF via Médias, puis renseignez le chemin ici.
</p>
</div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div <div
v-for="(chapter, i) in chapters" v-for="(chapter, i) in chapters"
@@ -35,6 +59,20 @@
>{{ name }}</span> >{{ name }}</span>
</div> </div>
</div> </div>
<select
v-if="pdfOutline.length"
class="page-select"
:value="getChapterPage(chapter.slug) ?? ''"
@change="setChapterPage(chapter.slug, ($event.target as HTMLSelectElement).value)"
>
<option value=""> Page PDF </option>
<option
v-for="entry in pdfOutline"
:key="`${entry.page}-${entry.title}`"
:value="entry.page"
>{{ '\u00A0\u00A0'.repeat(entry.level) }}{{ entry.title }} (p.{{ entry.page }})</option>
</select>
<span v-else class="text-xs text-white/30"></span>
<button <button
class="delete-btn" class="delete-btn"
@click="removeChapter(chapter.slug)" @click="removeChapter(chapter.slug)"
@@ -91,6 +129,58 @@ const saved = ref(false)
const newTitle = ref('') const newTitle = ref('')
const newSlug = ref('') const newSlug = ref('')
// PDF path + outline
const pdfPath = ref(bookConfig.value?.book?.pdfFile ?? '/pdf/une-economie-du-don.pdf')
const savingPdf = ref(false)
const savedPdf = ref(false)
const pdfOutline = ref<Array<{ title: string; page: number; level: number }>>([])
const chapterPageMap = ref<Record<string, number | undefined>>({})
if (bookConfig.value?.chapterPages) {
for (const cp of bookConfig.value.chapterPages) {
chapterPageMap.value[cp.chapterSlug] = cp.page
}
}
function getChapterPage(slug: string): number | undefined {
return chapterPageMap.value[slug]
}
function setChapterPage(slug: string, val: string) {
const num = parseInt(val, 10)
if (num > 0) chapterPageMap.value[slug] = num
else delete chapterPageMap.value[slug]
}
// Charger le sommaire PDF côté client via l'API serveur
onMounted(() => {
$fetch<Array<{ title: string; page: number; level: number }>>('/api/admin/pdf-outline')
.then((data) => { pdfOutline.value = data })
.catch((err) => { console.warn('PDF outline load failed:', err) })
})
async function savePdfConfig() {
if (!bookConfig.value) return
savingPdf.value = true
savedPdf.value = false
try {
bookConfig.value.book.pdfFile = pdfPath.value
bookConfig.value.chapterPages = Object.entries(chapterPageMap.value)
.filter(([, page]) => page != null)
.map(([chapterSlug, page]) => ({ chapterSlug, page }))
await $fetch('/api/admin/content/config', {
method: 'PUT',
body: bookConfig.value,
})
savedPdf.value = true
setTimeout(() => { savedPdf.value = false }, 2000)
} finally {
savingPdf.value = false
}
}
// Drag & drop state // Drag & drop state
const dragIdx = ref<number | null>(null) const dragIdx = ref<number | null>(null)
const dropIdx = ref<number | null>(null) const dropIdx = ref<number | null>(null)
@@ -156,19 +246,52 @@ async function removeChapter(slug: string) {
</script> </script>
<style scoped> <style scoped>
.pdf-section {
margin-bottom: 1.5rem;
padding: 1rem;
border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.5rem;
background: hsl(var(--color-bg));
}
.save-pdf-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--color-surface-light));
background: none;
color: hsl(var(--color-text-muted));
font-size: 0.8rem;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.save-pdf-btn:hover:not(:disabled) {
border-color: hsl(var(--color-primary) / 0.5);
color: hsl(var(--color-primary));
}
.save-pdf-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.chapter-item { .chapter-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.5rem; border-radius: 0.5rem;
transition: all 0.2s; transition: all 0.2s;
} }
.chapter-item:hover { .chapter-item:hover {
border-color: hsl(12 76% 48% / 0.3); border-color: hsl(var(--color-primary) / 0.3);
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
} }
.chapter-item--dragging { .chapter-item--dragging {
@@ -176,13 +299,13 @@ async function removeChapter(slug: string) {
} }
.chapter-item--over { .chapter-item--over {
border-top: 2px solid hsl(12 76% 48%); border-top: 2px solid hsl(var(--color-primary));
} }
.drag-handle { .drag-handle {
cursor: grab; cursor: grab;
padding: 0.25rem; padding: 0.25rem;
color: hsl(20 8% 35%); color: hsl(var(--color-text-muted));
flex-shrink: 0; flex-shrink: 0;
} }
@@ -193,7 +316,7 @@ async function removeChapter(slug: string) {
.chapter-order { .chapter-order {
font-family: var(--font-mono, monospace); font-family: var(--font-mono, monospace);
font-size: 0.85rem; font-size: 0.85rem;
color: hsl(12 76% 48% / 0.5); color: hsl(var(--color-primary) / 0.5);
font-weight: 600; font-weight: 600;
width: 1.75rem; width: 1.75rem;
} }
@@ -205,13 +328,13 @@ async function removeChapter(slug: string) {
.chapter-title { .chapter-title {
display: block; display: block;
color: white; color: hsl(var(--color-text));
font-weight: 500; font-weight: 500;
text-decoration: none; text-decoration: none;
} }
.chapter-title:hover { .chapter-title:hover {
color: hsl(12 76% 68%); color: hsl(var(--color-primary));
} }
.chapter-songs { .chapter-songs {
@@ -225,9 +348,26 @@ async function removeChapter(slug: string) {
font-size: 0.65rem; font-size: 0.65rem;
padding: 0.1rem 0.5rem; padding: 0.1rem 0.5rem;
border-radius: 9999px; border-radius: 9999px;
background: hsl(12 76% 48% / 0.1); background: hsl(var(--color-primary) / 0.1);
color: hsl(12 76% 60%); color: hsl(var(--color-primary));
border: 1px solid hsl(12 76% 48% / 0.2); border: 1px solid hsl(var(--color-primary) / 0.2);
}
.page-select {
flex-shrink: 0;
max-width: 14rem;
padding: 0.25rem 0.4rem;
border-radius: 0.375rem;
border: 1px solid hsl(var(--color-surface-light));
background: hsl(var(--color-bg));
color: hsl(var(--color-text-muted));
font-size: 0.7rem;
cursor: pointer;
}
.page-select:focus {
outline: none;
border-color: hsl(var(--color-primary) / 0.5);
} }
.delete-btn { .delete-btn {
@@ -248,15 +388,15 @@ async function removeChapter(slug: string) {
.admin-input { .admin-input {
padding: 0.375rem 0.5rem; padding: 0.375rem 0.5rem;
border-radius: 0.375rem; border-radius: 0.375rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-size: 0.8rem; font-size: 0.8rem;
} }
.admin-input:focus { .admin-input:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
} }
.add-btn { .add-btn {
@@ -265,9 +405,9 @@ async function removeChapter(slug: string) {
gap: 0.5rem; gap: 0.5rem;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px solid hsl(20 8% 25%); border: 1px solid hsl(var(--color-surface-light));
background: none; background: none;
color: hsl(20 8% 55%); color: hsl(var(--color-text-muted));
font-size: 0.85rem; font-size: 0.85rem;
cursor: pointer; cursor: pointer;
transition: all 0.15s; transition: all 0.15s;
@@ -275,8 +415,8 @@ async function removeChapter(slug: string) {
} }
.add-btn:hover:not(:disabled) { .add-btn:hover:not(:disabled) {
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
color: hsl(12 76% 68%); color: hsl(var(--color-primary));
} }
.add-btn:disabled { .add-btn:disabled {

View File

@@ -9,8 +9,8 @@
<p class="text-sm text-white/50">Identité, navigation, footer</p> <p class="text-sm text-white/50">Identité, navigation, footer</p>
</NuxtLink> </NuxtLink>
<NuxtLink to="/admin/pages/home" class="dash-card"> <NuxtLink to="/admin/pages" class="dash-card">
<div class="i-lucide-home h-8 w-8 text-primary mb-2" /> <div class="i-lucide-file-text h-8 w-8 text-primary mb-2" />
<h2 class="text-lg font-semibold text-white">Pages</h2> <h2 class="text-lg font-semibold text-white">Pages</h2>
<p class="text-sm text-white/50">Contenus des pages publiques</p> <p class="text-sm text-white/50">Contenus des pages publiques</p>
</NuxtLink> </NuxtLink>
@@ -27,6 +27,12 @@
<p class="text-sm text-white/50">Métadonnées des pistes</p> <p class="text-sm text-white/50">Métadonnées des pistes</p>
</NuxtLink> </NuxtLink>
<NuxtLink to="/admin/messages" class="dash-card">
<div class="i-lucide-message-square h-8 w-8 text-accent mb-2" />
<h2 class="text-lg font-semibold text-white">Messages</h2>
<p class="text-sm text-white/50">Modération des messages visiteurs</p>
</NuxtLink>
<NuxtLink to="/admin/media" class="dash-card"> <NuxtLink to="/admin/media" class="dash-card">
<div class="i-lucide-image h-8 w-8 text-accent mb-2" /> <div class="i-lucide-image h-8 w-8 text-accent mb-2" />
<h2 class="text-lg font-semibold text-white">Médias</h2> <h2 class="text-lg font-semibold text-white">Médias</h2>
@@ -47,14 +53,14 @@ definePageMeta({
.dash-card { .dash-card {
padding: 1.5rem; padding: 1.5rem;
border-radius: 0.75rem; border-radius: 0.75rem;
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-surface-light));
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
text-decoration: none; text-decoration: none;
transition: all 0.2s; transition: all 0.2s;
} }
.dash-card:hover { .dash-card:hover {
border-color: hsl(12 76% 48% / 0.3); border-color: hsl(var(--color-primary) / 0.3);
background: hsl(20 8% 8%); background: hsl(var(--color-surface));
} }
</style> </style>

View File

@@ -72,16 +72,16 @@ async function login() {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: hsl(20 8% 3.5%); background: hsl(var(--color-bg));
} }
.login-form { .login-form {
width: 100%; width: 100%;
max-width: 24rem; max-width: 24rem;
padding: 2.5rem; padding: 2.5rem;
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 1rem; border-radius: 1rem;
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
} }
.login-error { .login-error {
@@ -97,7 +97,7 @@ async function login() {
display: block; display: block;
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 500; font-weight: 500;
color: hsl(20 8% 60%); color: hsl(var(--color-text-muted));
margin-bottom: 0.375rem; margin-bottom: 0.375rem;
} }
@@ -105,16 +105,16 @@ async function login() {
width: 100%; width: 100%;
padding: 0.625rem 0.75rem; padding: 0.625rem 0.75rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
background: hsl(20 8% 4%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-size: 0.9rem; font-size: 0.9rem;
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
} }
.login-input:focus { .login-input:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
} }
.login-btn { .login-btn {
@@ -126,7 +126,7 @@ async function login() {
padding: 0.625rem; padding: 0.625rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: none; border: none;
background: hsl(12 76% 48%); background: hsl(var(--color-primary));
color: white; color: white;
font-weight: 600; font-weight: 600;
font-size: 0.9rem; font-size: 0.9rem;
@@ -135,7 +135,7 @@ async function login() {
} }
.login-btn:hover:not(:disabled) { .login-btn:hover:not(:disabled) {
background: hsl(12 76% 42%); background: hsl(var(--color-primary));
} }
.login-btn:disabled { .login-btn:disabled {
@@ -147,12 +147,12 @@ async function login() {
margin-top: 1rem; margin-top: 1rem;
text-align: center; text-align: center;
font-size: 0.75rem; font-size: 0.75rem;
color: hsl(20 8% 40%); color: hsl(var(--color-text-muted));
} }
.dev-hint code { .dev-hint code {
color: hsl(12 76% 60%); color: hsl(var(--color-primary));
background: hsl(20 8% 10%); background: hsl(var(--color-surface));
padding: 0.125rem 0.375rem; padding: 0.125rem 0.375rem;
border-radius: 0.25rem; border-radius: 0.25rem;
} }

View File

@@ -130,14 +130,14 @@ function formatDate(iso: string) {
<style scoped> <style scoped>
.msg-row { .msg-row {
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
} }
.msg-row--draft { .msg-row--draft {
border-left: 3px solid hsl(36 80% 52%); border-left: 3px solid hsl(var(--color-accent));
} }
.status-badge { .status-badge {
@@ -155,22 +155,22 @@ function formatDate(iso: string) {
} }
.status-badge--draft { .status-badge--draft {
background: hsl(36 80% 52% / 0.15); background: hsl(var(--color-accent) / 0.15);
color: hsl(36 80% 66%); color: hsl(var(--color-accent));
} }
.admin-input { .admin-input {
padding: 0.375rem 0.5rem; padding: 0.375rem 0.5rem;
border-radius: 0.375rem; border-radius: 0.375rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-size: 0.8rem; font-size: 0.8rem;
} }
.admin-input:focus { .admin-input:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
} }
.action-btn { .action-btn {
@@ -180,16 +180,16 @@ function formatDate(iso: string) {
padding: 0.3rem 0.6rem; padding: 0.3rem 0.6rem;
border-radius: 0.375rem; border-radius: 0.375rem;
font-size: 0.75rem; font-size: 0.75rem;
color: hsl(20 8% 60%); color: hsl(var(--color-text-muted));
background: none; background: none;
border: 1px solid hsl(20 8% 16%); border: 1px solid hsl(var(--color-surface-light));
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
} }
.action-btn:hover { .action-btn:hover {
background: hsl(20 8% 10%); background: hsl(var(--color-surface));
color: white; color: hsl(var(--color-text));
} }
.action-btn--save { .action-btn--save {

View File

@@ -2,11 +2,11 @@
<div> <div>
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<div> <div>
<NuxtLink to="/admin" class="text-sm text-white/40 hover:text-white/60 transition-colors"> <NuxtLink to="/admin/pages" class="text-sm text-white/40 hover:text-white/60 transition-colors">
Dashboard Pages
</NuxtLink> </NuxtLink>
<h1 class="font-display text-2xl font-bold text-white mt-1"> <h1 class="font-display text-2xl font-bold text-white mt-1">
Page : {{ pageName }} Page : {{ pagePath }}
</h1> </h1>
</div> </div>
<AdminSaveButton :saving="saving" :saved="saved" @save="save" /> <AdminSaveButton :saving="saving" :saved="saved" @save="save" />
@@ -40,9 +40,12 @@ definePageMeta({
}) })
const route = useRoute() const route = useRoute()
const pageName = computed(() => route.params.name as string) const pagePath = computed(() => {
const p = route.params.path
return Array.isArray(p) ? p.join('/') : p
})
const { data, error } = await useFetch(() => `/api/content/pages/${pageName.value}`) const { data, error } = await useFetch(() => `/api/content/pages/${pagePath.value}`)
const yamlContent = ref('') const yamlContent = ref('')
@@ -60,7 +63,7 @@ async function save() {
saved.value = false saved.value = false
try { try {
const parsed = yaml.parse(yamlContent.value) const parsed = yaml.parse(yamlContent.value)
await $fetch(`/api/admin/content/pages/${pageName.value}`, { await $fetch(`/api/admin/content/pages/${pagePath.value}`, {
method: 'PUT', method: 'PUT',
body: parsed, body: parsed,
}) })
@@ -80,10 +83,10 @@ async function save() {
.yaml-textarea { .yaml-textarea {
width: 100%; width: 100%;
padding: 1rem; padding: 1rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.5rem; border-radius: 0.5rem;
background: hsl(20 8% 4%); background: hsl(var(--color-bg));
color: hsl(36 80% 76%); color: hsl(var(--color-accent));
font-family: var(--font-mono, monospace); font-family: var(--font-mono, monospace);
font-size: 0.85rem; font-size: 0.85rem;
line-height: 1.7; line-height: 1.7;
@@ -93,6 +96,6 @@ async function save() {
.yaml-textarea:focus { .yaml-textarea:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
} }
</style> </style>

View File

@@ -0,0 +1,97 @@
<template>
<div>
<div class="flex items-center justify-between mb-6">
<div>
<NuxtLink to="/admin" class="text-sm text-white/40 hover:text-white/60 transition-colors">
Dashboard
</NuxtLink>
<h1 class="font-display text-2xl font-bold text-white mt-1">Pages</h1>
</div>
</div>
<div v-if="pages" class="flex flex-col gap-6">
<!-- Root pages -->
<div v-if="rootPages.length">
<h2 class="font-display text-lg font-semibold text-white/80 mb-3">Pages principales</h2>
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<NuxtLink
v-for="page in rootPages"
:key="page.path"
:to="`/admin/pages/${page.path}`"
class="page-card"
>
<div class="i-lucide-file-text h-5 w-5 text-primary mb-1" />
<span class="font-mono text-sm text-white">{{ page.label }}</span>
</NuxtLink>
</div>
</div>
<!-- Section pages -->
<div v-for="[section, sectionPages] in sections" :key="section">
<h2 class="font-display text-lg font-semibold text-white/80 mb-3">
<span class="i-lucide-folder h-4 w-4 inline-block mr-1 text-accent" />
{{ section }}
</h2>
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<NuxtLink
v-for="page in sectionPages"
:key="page.path"
:to="`/admin/pages/${page.path}`"
class="page-card"
>
<div class="i-lucide-file-text h-5 w-5 text-primary/60 mb-1" />
<span class="font-mono text-sm text-white">{{ page.label }}</span>
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'admin',
middleware: 'admin',
})
interface PageEntry {
path: string
label: string
section?: string
}
const { data: pages } = await useFetch<PageEntry[]>('/api/admin/content/pages')
const rootPages = computed(() =>
(pages.value ?? []).filter(p => !p.section),
)
const sections = computed(() => {
const map = new Map<string, PageEntry[]>()
for (const p of pages.value ?? []) {
if (p.section) {
if (!map.has(p.section)) map.set(p.section, [])
map.get(p.section)!.push(p)
}
}
return Array.from(map.entries())
})
</script>
<style scoped>
.page-card {
display: flex;
flex-direction: column;
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--color-surface-light));
background: hsl(var(--color-bg));
text-decoration: none;
transition: all 0.2s;
}
.page-card:hover {
border-color: hsl(var(--color-primary) / 0.3);
background: hsl(var(--color-surface));
}
</style>

View File

@@ -103,14 +103,14 @@ async function save() {
.admin-input { .admin-input {
padding: 0.375rem 0.5rem; padding: 0.375rem 0.5rem;
border-radius: 0.375rem; border-radius: 0.375rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-size: 0.8rem; font-size: 0.8rem;
} }
.admin-input:focus { .admin-input:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
} }
</style> </style>

View File

@@ -163,7 +163,7 @@ async function save() {
align-items: flex-start; align-items: flex-start;
gap: 0.75rem; gap: 0.75rem;
padding: 0.75rem 0.5rem; padding: 0.75rem 0.5rem;
border-bottom: 1px solid hsl(20 8% 10%); border-bottom: 1px solid hsl(var(--color-surface));
transition: background 0.15s; transition: background 0.15s;
} }
@@ -172,13 +172,13 @@ async function save() {
} }
.song-row--over { .song-row--over {
border-top: 2px solid hsl(12 76% 48%); border-top: 2px solid hsl(var(--color-primary));
} }
.drag-handle { .drag-handle {
cursor: grab; cursor: grab;
padding: 0.25rem; padding: 0.25rem;
color: hsl(20 8% 35%); color: hsl(var(--color-text-muted));
flex-shrink: 0; flex-shrink: 0;
margin-top: 0.25rem; margin-top: 0.25rem;
} }
@@ -190,7 +190,7 @@ async function save() {
.song-num { .song-num {
font-family: var(--font-mono, monospace); font-family: var(--font-mono, monospace);
font-size: 0.8rem; font-size: 0.8rem;
color: hsl(20 8% 40%); color: hsl(var(--color-text-muted));
width: 1.25rem; width: 1.25rem;
text-align: right; text-align: right;
flex-shrink: 0; flex-shrink: 0;
@@ -200,16 +200,16 @@ async function save() {
.admin-input { .admin-input {
padding: 0.375rem 0.5rem; padding: 0.375rem 0.5rem;
border-radius: 0.375rem; border-radius: 0.375rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-size: 0.8rem; font-size: 0.8rem;
width: 100%; width: 100%;
} }
.admin-input:focus { .admin-input:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
} }
.lyrics-textarea { .lyrics-textarea {
@@ -240,16 +240,16 @@ async function save() {
margin-top: 1rem; margin-top: 1rem;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px dashed hsl(20 8% 25%); border: 1px dashed hsl(var(--color-surface-light));
background: none; background: none;
color: hsl(20 8% 55%); color: hsl(var(--color-text-muted));
font-size: 0.85rem; font-size: 0.85rem;
cursor: pointer; cursor: pointer;
transition: all 0.15s; transition: all 0.15s;
} }
.add-btn:hover { .add-btn:hover {
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
color: hsl(12 76% 68%); color: hsl(var(--color-primary));
} }
</style> </style>

View File

@@ -1,168 +0,0 @@
<template>
<div class="relative overflow-hidden section-padding">
<!-- Shadok jardinier: character with watering can and plant -->
<svg class="shadok-jardinier" viewBox="0 0 240 300" fill="none" aria-hidden="true">
<!-- Body -->
<ellipse cx="110" cy="160" rx="40" ry="48" fill="currentColor" opacity="0.85"/>
<!-- Head -->
<circle cx="110" cy="96" r="25" fill="currentColor" opacity="0.8"/>
<!-- Straw hat -->
<ellipse cx="110" cy="78" rx="35" ry="8" fill="currentColor" opacity="0.4"/>
<path d="M85 78 Q110 60 135 78" fill="currentColor" opacity="0.35"/>
<!-- Eyes (focused, looking down at plant) -->
<circle cx="102" cy="94" r="4" fill="currentColor" opacity="0.2"/>
<circle cx="120" cy="94" r="4" fill="currentColor" opacity="0.2"/>
<circle cx="103" cy="95" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="121" cy="95" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Smile -->
<path d="M103 106 Q110 111 118 106" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
<!-- Arm holding watering can -->
<line x1="70" y1="150" x2="40" y2="170" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Watering can -->
<rect x="20" y="165" width="30" height="20" rx="3" fill="currentColor" opacity="0.4"/>
<line x1="20" y1="168" x2="10" y2="160" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.4"/>
<!-- Water drops -->
<circle cx="12" cy="165" r="1.5" fill="currentColor" opacity="0.25"/>
<circle cx="8" cy="170" r="1.5" fill="currentColor" opacity="0.2"/>
<circle cx="15" cy="172" r="1.5" fill="currentColor" opacity="0.2"/>
<!-- Other arm -->
<line x1="150" y1="150" x2="170" y2="180" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Legs -->
<line x1="95" y1="205" x2="85" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="125" y1="205" x2="135" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Plant -->
<line x1="180" y1="220" x2="180" y2="180" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<path d="M180 195 Q195 185 190 175" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.35"/>
<path d="M180 205 Q165 195 168 185" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.35"/>
<path d="M180 185 Q192 172 188 165" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.3"/>
<!-- Pot -->
<path d="M170 220 L175 240 L185 240 L190 220 Z" fill="currentColor" opacity="0.35"/>
</svg>
<!-- Shadok bâtisseur: character with trowel building a wall -->
<svg class="shadok-batisseur" viewBox="0 0 260 300" fill="none" aria-hidden="true">
<!-- Body -->
<ellipse cx="130" cy="150" rx="40" ry="48" fill="currentColor" opacity="0.85"/>
<!-- Head -->
<circle cx="130" cy="86" r="25" fill="currentColor" opacity="0.8"/>
<!-- Hard hat -->
<ellipse cx="130" cy="68" rx="28" ry="6" fill="currentColor" opacity="0.4"/>
<rect x="108" y="60" width="44" height="10" rx="3" fill="currentColor" opacity="0.35"/>
<!-- Eyes (determined) -->
<circle cx="122" cy="84" r="4" fill="currentColor" opacity="0.2"/>
<circle cx="140" cy="84" r="4" fill="currentColor" opacity="0.2"/>
<circle cx="123" cy="83" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="141" cy="83" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Grin -->
<path d="M123 96 Q130 101 138 96" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
<!-- Arm with trowel -->
<line x1="170" y1="140" x2="210" y2="120" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Trowel -->
<polygon points="210,115 230,110 225,120 210,122" fill="currentColor" opacity="0.45"/>
<line x1="210" y1="118" x2="200" y2="125" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.4"/>
<!-- Other arm -->
<line x1="90" y1="145" x2="65" y2="170" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Legs -->
<line x1="115" y1="195" x2="105" y2="255" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="145" y1="195" x2="155" y2="255" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Wall (bricks) -->
<rect x="40" y="200" width="50" height="16" rx="1" fill="currentColor" opacity="0.3"/>
<rect x="45" y="183" width="40" height="16" rx="1" fill="currentColor" opacity="0.28"/>
<rect x="50" y="166" width="30" height="16" rx="1" fill="currentColor" opacity="0.25"/>
<!-- Brick lines -->
<line x1="65" y1="200" x2="65" y2="216" stroke="currentColor" stroke-width="1" opacity="0.15"/>
<line x1="55" y1="183" x2="55" y2="199" stroke="currentColor" stroke-width="1" opacity="0.15"/>
</svg>
<div class="container-content">
<header class="mb-12 text-center">
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-bold tracking-tight text-white">
{{ content?.title }}
</h1>
<p class="mt-4 mx-auto max-w-2xl text-white/60">
{{ content?.description }}
</p>
</header>
<div class="mx-auto max-w-3xl flex flex-col gap-6">
<div
v-for="(extract, i) in content?.extracts"
:key="i"
class="card-surface"
>
<p class="mb-2 font-mono text-xs tracking-widest text-accent uppercase">
{{ extract.chapter }}
</p>
<blockquote class="border-l-2 border-primary/30 pl-4 text-white/70 italic leading-relaxed whitespace-pre-line">
{{ extract.text }}
</blockquote>
<div class="mt-4">
<NuxtLink
:to="`/modele-eco/${extract.chapterSlug}`"
class="inline-flex items-center gap-1 text-sm text-primary hover:text-primary/80 transition-colors"
>
Lire le chapitre
<div class="i-lucide-arrow-right h-3.5 w-3.5" />
</NuxtLink>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'default',
})
const { data: content } = await usePageContent('autonomie')
useHead({
title: content.value?.meta?.title ?? 'Autonomie',
})
</script>
<style scoped>
.page-title {
font-size: clamp(2rem, 5vw, 2.75rem);
}
.shadok-jardinier {
position: absolute;
left: 2%;
top: 5%;
width: clamp(100px, 14vw, 200px);
opacity: 0.28;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-jardinier 10s ease-in-out infinite;
}
.shadok-batisseur {
position: absolute;
right: 2%;
bottom: 5%;
width: clamp(110px, 15vw, 210px);
opacity: 0.3;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-batisseur 11s ease-in-out infinite;
}
@keyframes shadok-float-jardinier {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes shadok-float-batisseur {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-8px) rotate(1deg); }
}
@media (max-width: 768px) {
.shadok-jardinier { display: none; }
.shadok-batisseur { display: none; }
}
</style>

View File

@@ -0,0 +1,159 @@
<template>
<div class="section-padding">
<div class="container-content">
<div class="mx-auto max-w-2xl">
<div class="section-icon mx-auto mb-6">
<div :class="`i-lucide-${content?.icon ?? 'landmark'}`" class="h-12 w-12" />
</div>
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase text-center">
{{ content?.kicker }}
</p>
<h1 class="font-display text-3xl font-bold mb-4 text-center" style="color: hsl(var(--color-text))">
{{ content?.title ?? slug }}
</h1>
<p class="text-lg leading-relaxed mb-8 text-center" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
<!-- Features grid (for decision page) -->
<div v-if="content?.features" class="grid gap-4 sm:grid-cols-2 mb-12">
<div v-for="feature in content.features" :key="feature.title" class="feature-card">
<div class="feature-icon">
<div :class="`i-lucide-${feature.icon} h-5 w-5`" />
</div>
<h3 class="font-display font-semibold mb-1" style="color: hsl(var(--color-text))">{{ feature.title }}</h3>
<p class="text-sm leading-relaxed" style="color: hsl(var(--color-text-muted))">{{ feature.text }}</p>
</div>
</div>
<!-- Project card -->
<div v-if="content?.project" class="project-card mb-8">
<div class="project-icon">
<div class="i-lucide-rocket h-5 w-5" />
</div>
<h2 class="font-display text-xl font-semibold mb-2" style="color: hsl(var(--color-text))">
{{ content.project.name }}
</h2>
<p style="color: hsl(var(--color-text-muted))" class="leading-relaxed">
{{ content.project.text }}
</p>
<span v-if="content?.gestation" class="gestation-badge mt-3">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
</div>
<!-- Extended content -->
<div v-if="content?.content" class="prose-block mb-10">
<p class="leading-relaxed whitespace-pre-line" style="color: hsl(var(--color-text-muted))">
{{ content.content }}
</p>
</div>
<div class="text-center flex flex-col items-center gap-3 sm:flex-row sm:justify-center">
<UiBaseButton v-if="slug === 'decision'" :href="decisionUrl" target="_blank">
<div class="i-lucide-external-link mr-2 h-4 w-4" />
Ouvrir Glibredecision
</UiBaseButton>
<UiBaseButton v-if="slug === 'tarifs-eau'" :href="sejeteral0Url" target="_blank">
<div class="i-lucide-external-link mr-2 h-4 w-4" />
Lancer SejeteralO
</UiBaseButton>
<UiBaseButton variant="ghost" to="/citoyenne">
<div class="i-lucide-arrow-left mr-2 h-4 w-4" />
Autonomie citoyenne
</UiBaseButton>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const slug = route.params.slug as string
const { data: content } = await usePageContent(`citoyenne/${slug}`)
const appConfig = useAppConfig()
const decisionUrl = (appConfig.libredecision as { url: string })?.url ?? '#'
const sejeteral0Url = (appConfig.sejeteral0 as { url: string })?.url ?? '#'
useHead({
title: content.value?.meta?.title ?? slug,
})
</script>
<style scoped>
.section-icon {
display: flex;
align-items: center;
justify-content: center;
width: 5rem;
height: 5rem;
border-radius: 1rem;
background: hsl(var(--color-primary) / 0.1);
border: 1px solid hsl(var(--color-primary) / 0.2);
color: hsl(var(--color-primary));
}
.feature-card {
padding: 1.25rem;
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
background: hsl(var(--color-surface));
}
.feature-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.5rem;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
margin-bottom: 0.75rem;
}
.project-card {
padding: 1.5rem;
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-primary) / 0.15);
background: hsl(var(--color-surface));
}
.project-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
margin-bottom: 1rem;
}
.gestation-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
background: hsl(var(--color-accent) / 0.12);
color: hsl(var(--color-accent));
font-size: 0.7rem;
font-weight: 500;
font-family: var(--font-mono);
}
.prose-block {
padding: 1.5rem;
border-radius: 0.75rem;
background: hsl(var(--color-surface));
}
</style>

View File

@@ -0,0 +1,631 @@
<template>
<div class="relative overflow-hidden section-padding">
<!-- 1. Shadok Capitaine (top-left, profile at ship's wheel, telescope) -->
<svg class="shadok shadok-capitaine" viewBox="0 0 170 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<!-- ship's wheel -->
<circle cx="50" cy="130" r="28" stroke="currentColor" stroke-width="3" fill="none" opacity="0.35"/>
<line x1="50" y1="102" x2="50" y2="158" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<line x1="22" y1="130" x2="78" y2="130" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<line x1="30" y1="110" x2="70" y2="150" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<line x1="70" y1="110" x2="30" y2="150" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<!-- wheel spokes knobs -->
<circle cx="50" cy="100" r="3" fill="currentColor" opacity="0.4"/>
<circle cx="50" cy="160" r="3" fill="currentColor" opacity="0.4"/>
<circle cx="20" cy="130" r="3" fill="currentColor" opacity="0.4"/>
<circle cx="80" cy="130" r="3" fill="currentColor" opacity="0.4"/>
<!-- body (small oval, profile facing right) -->
<ellipse cx="85" cy="110" rx="22" ry="28" fill="currentColor" opacity="0.25"/>
<!-- head -->
<circle cx="92" cy="68" r="16" fill="currentColor" opacity="0.3"/>
<!-- captain hat -->
<rect x="76" y="52" width="32" height="8" rx="4" fill="currentColor" opacity="0.4"/>
<rect x="80" y="46" width="24" height="8" rx="3" fill="currentColor" opacity="0.3"/>
<!-- hat brim detail -->
<ellipse cx="92" cy="60" rx="18" ry="3" fill="currentColor" opacity="0.2"/>
<!-- eyes (looking right through telescope) -->
<circle cx="98" cy="65" r="2.5" fill="currentColor" opacity="0.6"/>
<circle cx="90" cy="66" r="2" fill="currentColor" opacity="0.5"/>
<!-- beak (pointy, profile right) -->
<polygon points="108,68 120,65 108,72" fill="currentColor" opacity="0.35"/>
<!-- left arm on wheel -->
<line x1="65" y1="100" x2="55" y2="115" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- right arm holding telescope -->
<line x1="105" y1="95" x2="130" y2="72" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- telescope -->
<rect x="128" y="66" width="30" height="7" rx="3.5" fill="currentColor" opacity="0.4"/>
<rect x="155" y="64" width="10" height="11" rx="3" fill="currentColor" opacity="0.3"/>
<!-- left leg (long, forward) -->
<line x1="75" y1="135" x2="60" y2="190" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- left foot -->
<ellipse cx="55" cy="193" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
<!-- right leg (long, back) -->
<line x1="92" y1="136" x2="100" y2="192" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- right foot -->
<ellipse cx="103" cy="195" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
</g>
</svg>
<!-- 2. Shadok Avocate (top-right robe, scroll, arm up) -->
<svg class="shadok shadok-avocate" viewBox="0 0 160 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<!-- body (small oval) -->
<ellipse cx="80" cy="115" rx="22" ry="30" fill="currentColor" opacity="0.25"/>
<!-- robe/gown flowing -->
<path d="M58 100 Q55 145 48 175 L112 175 Q105 145 102 100 Z" fill="currentColor" opacity="0.18"/>
<!-- robe collar -->
<path d="M68 88 Q80 96 92 88" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.35"/>
<!-- head -->
<circle cx="80" cy="70" r="16" fill="currentColor" opacity="0.3"/>
<!-- eyes (looking up-left) -->
<circle cx="74" cy="67" r="2.5" fill="currentColor" opacity="0.6"/>
<circle cx="83" cy="68" r="2" fill="currentColor" opacity="0.5"/>
<!-- beak (3/4 view, slight left) -->
<polygon points="68,72 56,69 66,76" fill="currentColor" opacity="0.35"/>
<!-- left arm up dramatically -->
<line x1="60" y1="100" x2="30" y2="55" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- open hand at top -->
<line x1="30" y1="55" x2="24" y2="46" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
<line x1="30" y1="55" x2="28" y2="44" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
<line x1="30" y1="55" x2="34" y2="46" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
<!-- right arm holding scroll -->
<line x1="100" y1="102" x2="125" y2="90" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- scroll document (big) -->
<rect x="120" y="72" width="28" height="40" rx="3" fill="currentColor" opacity="0.25"/>
<!-- scroll curl top -->
<ellipse cx="134" cy="72" rx="14" ry="5" fill="currentColor" opacity="0.2"/>
<!-- scroll curl bottom -->
<ellipse cx="134" cy="112" rx="12" ry="4" fill="currentColor" opacity="0.2"/>
<!-- scroll text lines -->
<line x1="126" y1="82" x2="142" y2="82" stroke="currentColor" stroke-width="1.5" opacity="0.2"/>
<line x1="126" y1="88" x2="140" y2="88" stroke="currentColor" stroke-width="1.5" opacity="0.2"/>
<line x1="126" y1="94" x2="143" y2="94" stroke="currentColor" stroke-width="1.5" opacity="0.2"/>
<line x1="126" y1="100" x2="138" y2="100" stroke="currentColor" stroke-width="1.5" opacity="0.2"/>
<!-- left leg (long) -->
<line x1="72" y1="142" x2="62" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="58" cy="198" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
<!-- right leg -->
<line x1="88" y1="142" x2="95" y2="196" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="99" cy="199" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
</g>
</svg>
<!-- 3. Shadok Vigie (left crow's nest, looking far) -->
<svg class="shadok shadok-vigie" viewBox="0 0 140 220" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<!-- mast pole -->
<line x1="70" y1="90" x2="70" y2="218" stroke="currentColor" stroke-width="4" opacity="0.3"/>
<!-- crow's nest platform -->
<path d="M38 88 L42 100 L98 100 L102 88 Z" fill="currentColor" opacity="0.25"/>
<line x1="36" y1="88" x2="104" y2="88" stroke="currentColor" stroke-width="3" opacity="0.35"/>
<!-- nest railing -->
<line x1="40" y1="78" x2="40" y2="100" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<line x1="100" y1="78" x2="100" y2="100" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<line x1="40" y1="78" x2="100" y2="78" stroke="currentColor" stroke-width="2" opacity="0.25"/>
<!-- body (small, inside nest) -->
<ellipse cx="70" cy="72" rx="20" ry="22" fill="currentColor" opacity="0.25"/>
<!-- head -->
<circle cx="70" cy="38" r="15" fill="currentColor" opacity="0.3"/>
<!-- eyes (squinting, looking right far) -->
<circle cx="78" cy="35" r="2.5" fill="currentColor" opacity="0.6"/>
<circle cx="70" cy="36" r="2" fill="currentColor" opacity="0.4"/>
<!-- beak (profile right) -->
<polygon points="83,38 96,35 84,42" fill="currentColor" opacity="0.35"/>
<!-- right hand shielding eyes -->
<line x1="88" y1="55" x2="98" y2="35" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="96" y1="33" x2="108" y2="30" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.35"/>
<!-- left arm resting on railing -->
<line x1="52" y1="60" x2="42" y2="78" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- legs dangling below nest (long!) -->
<line x1="60" y1="98" x2="52" y2="160" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="48" cy="163" rx="8" ry="4" fill="currentColor" opacity="0.3"/>
<line x1="80" y1="98" x2="86" y2="158" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="90" cy="161" rx="8" ry="4" fill="currentColor" opacity="0.3"/>
</g>
</svg>
<!-- 4. Shadok Comedien (right theater masks, cape, leg lifted) -->
<svg class="shadok shadok-comedien" viewBox="0 0 170 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<!-- dramatic cape -->
<path d="M65 85 Q40 120 35 180 L115 180 Q110 120 95 85 Z" fill="currentColor" opacity="0.15"/>
<path d="M95 85 Q120 100 135 160 Q130 165 115 180" fill="currentColor" opacity="0.1"/>
<!-- body (small oval) -->
<ellipse cx="80" cy="105" rx="20" ry="26" fill="currentColor" opacity="0.25"/>
<!-- head -->
<circle cx="80" cy="62" r="16" fill="currentColor" opacity="0.3"/>
<!-- eyes (different directions dramatic!) -->
<circle cx="74" cy="59" r="2.5" fill="currentColor" opacity="0.6"/>
<circle cx="86" cy="61" r="2.5" fill="currentColor" opacity="0.55"/>
<!-- beak -->
<polygon points="72,66 60,63 70,70" fill="currentColor" opacity="0.35"/>
<!-- left arm up holding comedy mask -->
<line x1="62" y1="92" x2="32" y2="55" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- comedy mask (big, smiling) -->
<circle cx="25" cy="42" r="16" fill="currentColor" opacity="0.2"/>
<path d="M17 46 Q25 56 33 46" stroke="currentColor" stroke-width="2" fill="none" opacity="0.4"/>
<circle cx="20" cy="38" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="30" cy="38" r="2" fill="currentColor" opacity="0.5"/>
<!-- right arm out holding tragedy mask -->
<line x1="98" y1="92" x2="135" y2="65" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- tragedy mask (big, frowning) -->
<circle cx="145" cy="55" r="16" fill="currentColor" opacity="0.2"/>
<path d="M137 62 Q145 54 153 62" stroke="currentColor" stroke-width="2" fill="none" opacity="0.4"/>
<circle cx="140" cy="50" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="150" cy="50" r="2" fill="currentColor" opacity="0.5"/>
<!-- left leg (long, planted) -->
<line x1="72" y1="128" x2="62" y2="190" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="58" cy="193" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
<!-- right leg (lifted dramatically!) -->
<line x1="88" y1="126" x2="115" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="120" cy="167" rx="8" ry="4" transform="rotate(-25 120 167)" fill="currentColor" opacity="0.3"/>
</g>
</svg>
<!-- 5. Shadok Cartographe (bottom-left sitting, map, compass) -->
<svg class="shadok shadok-cartographe" viewBox="0 0 180 190" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<!-- table -->
<rect x="20" y="125" width="140" height="6" rx="2" fill="currentColor" opacity="0.3"/>
<line x1="35" y1="131" x2="30" y2="185" stroke="currentColor" stroke-width="3" opacity="0.25"/>
<line x1="145" y1="131" x2="150" y2="185" stroke="currentColor" stroke-width="3" opacity="0.25"/>
<!-- big map spread on table -->
<rect x="30" y="108" width="110" height="18" rx="2" fill="currentColor" opacity="0.2"/>
<!-- map details -->
<path d="M45 112 Q65 108 85 115 Q105 110 125 114" stroke="currentColor" stroke-width="1" fill="none" opacity="0.25"/>
<circle cx="75" cy="115" r="3" stroke="currentColor" stroke-width="1" fill="none" opacity="0.2"/>
<line x1="95" y1="110" x2="95" y2="122" stroke="currentColor" stroke-width="0.8" opacity="0.2"/>
<!-- body (small, hunched over map) -->
<ellipse cx="90" cy="88" rx="22" ry="25" fill="currentColor" opacity="0.25" transform="rotate(10 90 88)"/>
<!-- head (bent down looking at map) -->
<circle cx="78" cy="60" r="15" fill="currentColor" opacity="0.3"/>
<!-- eyes (looking down at map) -->
<circle cx="74" cy="64" r="2" fill="currentColor" opacity="0.6"/>
<circle cx="83" cy="65" r="2" fill="currentColor" opacity="0.5"/>
<!-- beak (pointing down) -->
<polygon points="72,70 65,78 78,72" fill="currentColor" opacity="0.35"/>
<!-- right arm holding compass tool -->
<line x1="110" y1="92" x2="130" y2="105" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- compass/divider tool (big V shape) -->
<line x1="130" y1="105" x2="125" y2="125" stroke="currentColor" stroke-width="2.5" opacity="0.4"/>
<line x1="130" y1="105" x2="140" y2="125" stroke="currentColor" stroke-width="2.5" opacity="0.4"/>
<circle cx="130" cy="103" r="3" fill="currentColor" opacity="0.3"/>
<!-- left arm holding magnifying glass -->
<line x1="70" y1="90" x2="48" y2="100" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- magnifying glass (big) -->
<circle cx="38" cy="96" r="12" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.35"/>
<line x1="48" y1="103" x2="55" y2="112" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- legs (sitting, bent under table) -->
<line x1="78" y1="110" x2="65" y2="145" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="65" y1="145" x2="50" y2="148" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<ellipse cx="45" cy="150" rx="8" ry="3.5" fill="currentColor" opacity="0.3"/>
<line x1="100" y1="110" x2="110" y2="145" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="110" y1="145" x2="122" y2="148" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<ellipse cx="127" cy="150" rx="8" ry="3.5" fill="currentColor" opacity="0.3"/>
</g>
</svg>
<!-- 6. Shadok Juge (bottom-right on bench, gavel, wig) -->
<svg class="shadok shadok-juge" viewBox="0 0 160 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<!-- elevated bench/podium -->
<rect x="30" y="145" width="100" height="55" rx="4" fill="currentColor" opacity="0.2"/>
<rect x="25" y="138" width="110" height="10" rx="3" fill="currentColor" opacity="0.28"/>
<!-- bench front panel detail -->
<rect x="55" y="155" width="50" height="35" rx="3" fill="currentColor" opacity="0.1"/>
<!-- body (small, seated) -->
<ellipse cx="80" cy="112" rx="22" ry="28" fill="currentColor" opacity="0.25"/>
<!-- head -->
<circle cx="80" cy="68" r="16" fill="currentColor" opacity="0.3"/>
<!-- judge wig (curly puffs) -->
<circle cx="66" cy="60" r="6" fill="currentColor" opacity="0.2"/>
<circle cx="94" cy="60" r="6" fill="currentColor" opacity="0.2"/>
<circle cx="72" cy="54" r="5" fill="currentColor" opacity="0.18"/>
<circle cx="88" cy="54" r="5" fill="currentColor" opacity="0.18"/>
<circle cx="80" cy="52" r="5.5" fill="currentColor" opacity="0.2"/>
<!-- wig curls hanging -->
<circle cx="63" cy="70" r="4.5" fill="currentColor" opacity="0.15"/>
<circle cx="97" cy="70" r="4.5" fill="currentColor" opacity="0.15"/>
<!-- eyes (stern, both forward) -->
<circle cx="74" cy="66" r="2.5" fill="currentColor" opacity="0.65"/>
<circle cx="86" cy="66" r="2.5" fill="currentColor" opacity="0.65"/>
<!-- stern eyebrows -->
<line x1="71" y1="62" x2="77" y2="63" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<line x1="89" y1="63" x2="83" y2="62" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<!-- beak (small, stern) -->
<polygon points="80,74 72,78 80,80" fill="currentColor" opacity="0.35"/>
<!-- right arm raising gavel -->
<line x1="100" y1="98" x2="130" y2="60" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- gavel (big!) -->
<rect x="120" y="44" width="26" height="12" rx="4" fill="currentColor" opacity="0.4"/>
<line x1="133" y1="56" x2="133" y2="38" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- left arm on bench -->
<line x1="60" y1="100" x2="40" y2="130" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- legs (dangling from bench, long) -->
<line x1="72" y1="138" x2="62" y2="200" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="58" cy="203" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
<line x1="88" y1="138" x2="96" y2="198" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="100" cy="201" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
</g>
</svg>
<!-- 7. Shadok Matelot (center-bottom pulling rope, leaning back, anchor) -->
<svg class="shadok shadok-matelot" viewBox="0 0 160 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<!-- rope going up and off -->
<path d="M30 20 Q35 50 55 70 Q65 78 68 85" stroke="currentColor" stroke-width="3" fill="none" opacity="0.3"/>
<!-- body (leaning back, tilted) -->
<ellipse cx="85" cy="108" rx="20" ry="27" fill="currentColor" opacity="0.25" transform="rotate(-15 85 108)"/>
<!-- head -->
<circle cx="95" cy="68" r="15" fill="currentColor" opacity="0.3"/>
<!-- bandana -->
<path d="M80 62 Q95 55 110 62" stroke="currentColor" stroke-width="3" fill="currentColor" opacity="0.2"/>
<line x1="108" y1="60" x2="118" y2="55" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<line x1="108" y1="62" x2="116" y2="60" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.2"/>
<!-- eyes (straining, squinting) -->
<circle cx="90" cy="66" r="2" fill="currentColor" opacity="0.6"/>
<circle cx="100" cy="65" r="2.5" fill="currentColor" opacity="0.55"/>
<!-- beak -->
<polygon points="104,70 115,67 105,74" fill="currentColor" opacity="0.35"/>
<!-- both arms pulling rope -->
<line x1="68" y1="95" x2="55" y2="78" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="75" y1="98" x2="62" y2="82" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- left leg (long, bracing forward) -->
<line x1="72" y1="130" x2="48" y2="185" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="43" cy="188" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
<!-- right leg (long, back) -->
<line x1="92" y1="132" x2="110" y2="188" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="114" cy="191" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
<!-- anchor nearby -->
<line x1="135" y1="140" x2="135" y2="185" stroke="currentColor" stroke-width="3" opacity="0.3"/>
<circle cx="135" cy="138" r="5" stroke="currentColor" stroke-width="2" fill="none" opacity="0.25"/>
<path d="M120 180 Q135 195 150 180" stroke="currentColor" stroke-width="3" fill="none" opacity="0.3"/>
<line x1="120" y1="180" x2="117" y2="174" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<line x1="150" y1="180" x2="153" y2="174" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
</g>
</svg>
<!-- 8. Shadok Mime (right pushing invisible wall, striped shirt, beret) -->
<svg class="shadok shadok-mime" viewBox="0 0 150 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<!-- body (small oval) -->
<ellipse cx="70" cy="112" rx="22" ry="28" fill="currentColor" opacity="0.2"/>
<!-- striped shirt lines -->
<line x1="52" y1="96" x2="88" y2="96" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<line x1="50" y1="102" x2="90" y2="102" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<line x1="50" y1="108" x2="90" y2="108" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<line x1="50" y1="114" x2="90" y2="114" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<line x1="52" y1="120" x2="88" y2="120" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<line x1="54" y1="126" x2="86" y2="126" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<!-- head (white face = lighter) -->
<circle cx="75" cy="65" r="16" fill="currentColor" opacity="0.2"/>
<!-- white face highlight -->
<circle cx="75" cy="65" r="13" fill="currentColor" opacity="0.08"/>
<!-- beret -->
<ellipse cx="75" cy="50" rx="18" ry="6" fill="currentColor" opacity="0.3"/>
<circle cx="75" cy="46" r="3" fill="currentColor" opacity="0.25"/>
<!-- eyes (expressive, wide) -->
<circle cx="69" cy="62" r="3" fill="currentColor" opacity="0.55"/>
<circle cx="81" cy="62" r="3" fill="currentColor" opacity="0.55"/>
<!-- raised eyebrows (surprised) -->
<path d="M66 57 Q69 54 72 57" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.4"/>
<path d="M78 57 Q81 54 84 57" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.4"/>
<!-- beak (small) -->
<polygon points="75,72 68,76 75,78" fill="currentColor" opacity="0.3"/>
<!-- both arms pushing against invisible wall (to the right) -->
<line x1="90" y1="100" x2="125" y2="85" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="90" y1="115" x2="125" y2="110" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- flat hands pressed against wall -->
<rect x="124" y="78" width="12" height="15" rx="3" fill="currentColor" opacity="0.2"/>
<rect x="124" y="103" width="12" height="15" rx="3" fill="currentColor" opacity="0.2"/>
<!-- invisible wall hint (faint line) -->
<line x1="138" y1="50" x2="138" y2="180" stroke="currentColor" stroke-width="1" stroke-dasharray="4 6" opacity="0.12"/>
<!-- left leg (long, leaning forward) -->
<line x1="60" y1="138" x2="45" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="40" cy="198" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
<!-- right leg (long, back) -->
<line x1="80" y1="138" x2="95" y2="194" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="99" cy="197" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
</g>
</svg>
<div class="container-content">
<header class="mb-12 text-center">
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-bold tracking-tight" style="color: hsl(var(--color-text))">
{{ content?.title }}
</h1>
<p class="mt-4 mx-auto max-w-2xl leading-relaxed" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
</header>
<div class="mx-auto max-w-3xl flex flex-col gap-6">
<!-- Decision collective -->
<div class="item-card">
<NuxtLink to="/citoyenne/decision" class="item-body group">
<div class="item-header">
<div class="item-icon">
<div class="i-lucide-gavel h-5 w-5" />
</div>
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
Décision collective
</h2>
</div>
<p class="leading-relaxed mt-3" style="color: hsl(var(--color-text-muted))">
Se donner les moyens de la décision collective.
</p>
<div class="mt-3 inline-flex items-center gap-1 text-sm text-primary group-hover:text-primary/80 transition-colors">
En savoir plus
<div class="i-lucide-arrow-right h-3.5 w-3.5" />
</div>
</NuxtLink>
<div class="item-actions">
<a :href="decisionUrl" target="_blank" rel="noopener" class="action-btn action-btn--primary">
<div class="i-lucide-external-link h-3.5 w-3.5" />
Ouvrir Glibredecision
</a>
</div>
</div>
<!-- Tarifs de l'eau -->
<div class="item-card">
<NuxtLink to="/citoyenne/tarifs-eau" class="item-body group">
<div class="item-header">
<div class="item-icon">
<div class="i-lucide-droplets h-5 w-5" />
</div>
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
Tarifs de l'eau
</h2>
<span class="gestation-badge">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
</div>
<p class="leading-relaxed mt-3" style="color: hsl(var(--color-text-muted))">
Application pour obtenir justice sociale et incitation dynamique à la réduction.
Permet de confier la décision à la population des communes.
</p>
<div class="mt-3 inline-flex items-center gap-1 text-sm text-primary group-hover:text-primary/80 transition-colors">
En savoir plus
<div class="i-lucide-arrow-right h-3.5 w-3.5" />
</div>
</NuxtLink>
<div class="item-actions">
<a :href="sejeteral0Url" target="_blank" rel="noopener" class="action-btn action-btn--primary">
<div class="i-lucide-external-link h-3.5 w-3.5" />
Lancer SejeteralO
</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'default',
})
const { data: content } = await usePageContent('citoyenne')
const appConfig = useAppConfig()
const decisionUrl = (appConfig.libredecision as { url: string })?.url ?? '#'
const sejeteral0Url = (appConfig.sejeteral0 as { url: string })?.url ?? '#'
useHead({
title: content.value?.meta?.title ?? 'Autonomie citoyenne',
})
</script>
<style scoped>
.page-title {
font-size: clamp(2rem, 5vw, 2.75rem);
}
.item-card {
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
background: hsl(var(--color-surface));
transition: border-color 0.2s;
overflow: hidden;
}
.item-card:hover {
border-color: hsl(var(--color-primary) / 0.2);
}
.item-body {
display: block;
padding: 1.5rem;
text-decoration: none;
color: inherit;
}
.item-header {
display: flex;
align-items: center;
gap: 0.75rem;
}
.item-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.5rem;
background: hsl(var(--color-primary) / 0.15);
color: hsl(var(--color-primary));
box-shadow: 0 0 12px hsl(var(--color-primary) / 0.12);
flex-shrink: 0;
}
.gestation-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
margin-left: auto;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
background: hsl(var(--color-accent) / 0.12);
color: hsl(var(--color-accent));
font-size: 0.7rem;
font-weight: 500;
font-family: var(--font-mono);
}
.item-actions {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
padding: 0.75rem 1.5rem;
border-top: 1px solid hsl(var(--color-text) / 0.06);
background: hsl(var(--color-bg) / 0.4);
}
.action-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border-radius: 0.5rem;
font-size: 0.8rem;
font-weight: 500;
text-decoration: none;
transition: all 0.2s;
cursor: pointer;
}
.action-btn--primary {
color: hsl(var(--color-primary));
background: hsl(var(--color-primary) / 0.12);
border: 1px solid hsl(var(--color-primary) / 0.25);
}
.action-btn--primary:hover {
background: hsl(var(--color-primary) / 0.2);
border-color: hsl(var(--color-primary) / 0.4);
}
/* Shadok illustrations */
.shadok {
position: absolute;
pointer-events: none;
width: clamp(70px, 10vw, 140px);
z-index: 0;
}
.shadok-capitaine {
top: 2%;
left: 2%;
color: hsl(var(--color-primary));
opacity: 0.22;
animation: shadok-float-1 9s ease-in-out infinite;
}
.shadok-avocate {
top: 1%;
right: 2%;
color: hsl(var(--color-accent));
opacity: 0.2;
animation: shadok-float-2 11s ease-in-out infinite;
}
.shadok-vigie {
left: 2%;
top: 38%;
color: hsl(var(--color-primary));
opacity: 0.24;
animation: shadok-float-3 10s ease-in-out infinite;
}
.shadok-comedien {
right: 3%;
top: 35%;
color: hsl(var(--color-accent));
opacity: 0.2;
animation: shadok-float-4 8s ease-in-out infinite;
}
.shadok-cartographe {
bottom: 10%;
left: 1%;
color: hsl(var(--color-accent));
opacity: 0.22;
animation: shadok-float-5 12s ease-in-out infinite;
}
.shadok-juge {
bottom: 6%;
right: 1%;
color: hsl(var(--color-primary));
opacity: 0.24;
animation: shadok-float-6 9.5s ease-in-out infinite;
}
.shadok-matelot {
bottom: 2%;
left: 50%;
transform: translateX(-50%);
color: hsl(var(--color-primary));
opacity: 0.18;
animation: shadok-float-7 7s ease-in-out infinite;
}
.shadok-mime {
right: 2%;
top: 55%;
color: hsl(var(--color-accent));
opacity: 0.28;
animation: shadok-float-8 10.5s ease-in-out infinite;
}
@keyframes shadok-float-1 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes shadok-float-2 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes shadok-float-3 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-7px); }
}
@keyframes shadok-float-4 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-9px); }
}
@keyframes shadok-float-5 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
@keyframes shadok-float-6 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes shadok-float-7 {
0%, 100% { transform: translateX(-50%) translateY(0); }
50% { transform: translateX(-50%) translateY(-7px); }
}
@keyframes shadok-float-8 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@media (max-width: 768px) {
.shadok {
display: none;
}
}
</style>

View File

@@ -0,0 +1,453 @@
<template>
<div class="relative overflow-hidden section-padding">
<!-- Shadok 1 : Libraire behind counter, 3/4 view, glasses, recommending a book -->
<svg class="shadok-libraire" viewBox="0 0 160 210" fill="none" aria-hidden="true">
<!-- Body (small oval) -->
<ellipse cx="75" cy="95" rx="22" ry="28" fill="currentColor" opacity="0.25"/>
<!-- Head -->
<circle cx="75" cy="58" r="16" fill="currentColor" opacity="0.3"/>
<!-- Eyes looking right (recommending) -->
<circle cx="80" cy="55" r="2" fill="currentColor" opacity="0.6"/>
<circle cx="86" cy="54" r="2" fill="currentColor" opacity="0.6"/>
<!-- Glasses on beak -->
<circle cx="80" cy="55" r="5" stroke="currentColor" stroke-width="1.2" fill="none" opacity="0.4"/>
<circle cx="86" cy="54" r="5" stroke="currentColor" stroke-width="1.2" fill="none" opacity="0.4"/>
<line x1="85" y1="55" x2="81" y2="54" stroke="currentColor" stroke-width="1" opacity="0.3"/>
<!-- Beak (pointy, triangular) -->
<polygon points="90,58 102,55 90,52" fill="currentColor" opacity="0.35"/>
<!-- Arm holding book out to customer -->
<line x1="97" y1="88" x2="125" y2="78" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Book in hand (big, detailed) -->
<rect x="120" y="70" width="18" height="24" rx="2" fill="currentColor" opacity="0.35"/>
<rect x="122" y="73" width="14" height="3" rx="1" fill="currentColor" opacity="0.2"/>
<line x1="129" y1="70" x2="129" y2="94" stroke="currentColor" stroke-width="1" opacity="0.2"/>
<!-- Other arm resting on counter -->
<line x1="53" y1="90" x2="35" y2="108" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Counter (big) -->
<rect x="10" y="120" width="140" height="10" rx="2" fill="currentColor" opacity="0.3"/>
<rect x="15" y="130" width="130" height="6" rx="1" fill="currentColor" opacity="0.15"/>
<!-- Stack of books on counter -->
<rect x="25" y="105" width="22" height="15" rx="2" fill="currentColor" opacity="0.3"/>
<rect x="28" y="98" width="18" height="7" rx="1" fill="currentColor" opacity="0.25"/>
<rect x="30" y="93" width="14" height="5" rx="1" fill="currentColor" opacity="0.2"/>
<!-- Legs (long!) -->
<line x1="65" y1="123" x2="55" y2="185" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="85" y1="123" x2="95" y2="185" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Big flat feet -->
<ellipse cx="50" cy="188" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
<ellipse cx="100" cy="188" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok 2 : Factrice running profile, letter carrier bag, letters flying, cap -->
<svg class="shadok-factrice" viewBox="0 0 180 200" fill="none" aria-hidden="true">
<!-- Body (small oval, leaning forward) -->
<ellipse cx="80" cy="80" rx="20" ry="26" fill="currentColor" opacity="0.25" transform="rotate(-15 80 80)"/>
<!-- Head -->
<circle cx="95" cy="48" r="15" fill="currentColor" opacity="0.3"/>
<!-- Cap -->
<rect x="82" y="34" width="28" height="7" rx="3" fill="currentColor" opacity="0.4"/>
<rect x="106" y="36" width="10" height="5" rx="2" fill="currentColor" opacity="0.3"/>
<!-- Eyes (determined, looking forward) -->
<circle cx="101" cy="46" r="1.8" fill="currentColor" opacity="0.6"/>
<circle cx="106" cy="45" r="1.8" fill="currentColor" opacity="0.6"/>
<!-- Beak (pointy, profile) -->
<polygon points="110,48 125,44 110,42" fill="currentColor" opacity="0.35"/>
<!-- Carrier bag (big, on shoulder) -->
<rect x="55" y="62" width="28" height="35" rx="4" fill="currentColor" opacity="0.3"/>
<line x1="60" y1="62" x2="75" y2="55" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<!-- Letters flying out of bag -->
<rect x="42" y="55" width="12" height="9" rx="1" fill="currentColor" opacity="0.2" transform="rotate(-20 48 59)"/>
<rect x="35" y="45" width="10" height="7" rx="1" fill="currentColor" opacity="0.15" transform="rotate(-35 40 48)"/>
<rect x="48" y="42" width="11" height="8" rx="1" fill="currentColor" opacity="0.18" transform="rotate(10 53 46)"/>
<!-- Arm swinging back -->
<line x1="65" y1="72" x2="45" y2="90" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Running legs (long strides!) -->
<line x1="72" y1="106" x2="40" y2="170" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="88" y1="106" x2="130" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Big flat feet -->
<ellipse cx="35" cy="174" rx="11" ry="4" fill="currentColor" opacity="0.3"/>
<ellipse cx="135" cy="168" rx="11" ry="4" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok 3 : Cartonnier assembling cardboard box, tape gun, flat boxes nearby -->
<svg class="shadok-cartonnier" viewBox="0 0 170 220" fill="none" aria-hidden="true">
<!-- Body (small, leaning over work) -->
<ellipse cx="70" cy="100" rx="22" ry="27" fill="currentColor" opacity="0.25" transform="rotate(8 70 100)"/>
<!-- Head (looking down at box) -->
<circle cx="78" cy="65" r="15" fill="currentColor" opacity="0.3"/>
<!-- Eyes (focused, looking down) -->
<circle cx="83" cy="67" r="1.8" fill="currentColor" opacity="0.6"/>
<circle cx="88" cy="68" r="1.8" fill="currentColor" opacity="0.6"/>
<!-- Beak pointing down -->
<polygon points="88,72 98,78 86,76" fill="currentColor" opacity="0.35"/>
<!-- Arm holding tape gun -->
<line x1="90" y1="95" x2="130" y2="105" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Tape gun (big, detailed) -->
<circle cx="138" cy="102" r="8" fill="currentColor" opacity="0.25"/>
<rect x="132" y="99" width="18" height="6" rx="1" fill="currentColor" opacity="0.35"/>
<polygon points="150,100 158,102 150,104" fill="currentColor" opacity="0.3"/>
<!-- Other arm holding box flap -->
<line x1="50" y1="92" x2="35" y2="118" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Box being assembled (big, 3D perspective) -->
<rect x="25" y="118" width="45" height="35" rx="2" fill="currentColor" opacity="0.2"/>
<polygon points="25,118 15,108 60,108 70,118" fill="currentColor" opacity="0.15"/>
<polygon points="70,118 60,108 60,143 70,153" fill="currentColor" opacity="0.12"/>
<!-- Flap open -->
<polygon points="25,118 15,108 15,98 25,108" fill="currentColor" opacity="0.18"/>
<!-- Flat boxes nearby -->
<rect x="90" y="145" width="35" height="4" rx="1" fill="currentColor" opacity="0.2"/>
<rect x="88" y="150" width="38" height="4" rx="1" fill="currentColor" opacity="0.15"/>
<rect x="92" y="155" width="32" height="4" rx="1" fill="currentColor" opacity="0.12"/>
<!-- Legs (long!) -->
<line x1="60" y1="127" x2="48" y2="192" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="80" y1="127" x2="92" y2="192" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Big flat feet -->
<ellipse cx="43" cy="196" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
<ellipse cx="97" cy="196" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok 4 : Cycliste livreur on bicycle, package on rack, profile leaning forward -->
<svg class="shadok-cycliste" viewBox="0 0 180 200" fill="none" aria-hidden="true">
<!-- Back wheel -->
<circle cx="35" cy="160" r="22" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.25"/>
<circle cx="35" cy="160" r="3" fill="currentColor" opacity="0.3"/>
<!-- Front wheel -->
<circle cx="145" cy="160" r="22" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.25"/>
<circle cx="145" cy="160" r="3" fill="currentColor" opacity="0.3"/>
<!-- Frame -->
<line x1="35" y1="160" x2="80" y2="120" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.3"/>
<line x1="80" y1="120" x2="120" y2="120" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.3"/>
<line x1="120" y1="120" x2="145" y2="160" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.3"/>
<line x1="35" y1="160" x2="80" y2="140" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.2"/>
<line x1="80" y1="140" x2="120" y2="120" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.2"/>
<!-- Handlebars -->
<line x1="120" y1="120" x2="130" y2="105" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.3"/>
<line x1="125" y1="105" x2="135" y2="105" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.3"/>
<!-- Saddle -->
<rect x="72" y="116" width="16" height="5" rx="2" fill="currentColor" opacity="0.3"/>
<!-- Package on rear rack (big!) -->
<rect x="15" y="125" width="30" height="22" rx="3" fill="currentColor" opacity="0.3"/>
<line x1="30" y1="125" x2="30" y2="147" stroke="currentColor" stroke-width="1" opacity="0.2"/>
<line x1="15" y1="136" x2="45" y2="136" stroke="currentColor" stroke-width="1" opacity="0.2"/>
<!-- Rear rack -->
<line x1="35" y1="147" x2="35" y2="155" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<line x1="15" y1="147" x2="55" y2="147" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<!-- Body (small, leaning forward on bike) -->
<ellipse cx="90" cy="100" rx="18" ry="24" fill="currentColor" opacity="0.25" transform="rotate(-20 90 100)"/>
<!-- Head -->
<circle cx="108" cy="68" r="14" fill="currentColor" opacity="0.3"/>
<!-- Eyes (focused ahead) -->
<circle cx="115" cy="65" r="1.8" fill="currentColor" opacity="0.6"/>
<circle cx="120" cy="64" r="1.8" fill="currentColor" opacity="0.6"/>
<!-- Beak -->
<polygon points="122,68 134,64 122,62" fill="currentColor" opacity="0.35"/>
<!-- Arms to handlebars -->
<line x1="100" y1="90" x2="130" y2="105" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Legs pedaling -->
<line x1="82" y1="118" x2="72" y2="145" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="88" y1="120" x2="95" y2="148" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Feet on pedals -->
<ellipse cx="70" cy="148" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="97" cy="150" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok 5 : Lectrice satisfaite sitting in armchair, open book, wrapping paper on floor -->
<svg class="shadok-lectrice" viewBox="0 0 160 210" fill="none" aria-hidden="true">
<!-- Armchair (big!) -->
<path d="M20 100 Q20 80 35 80 L115 80 Q130 80 130 100 L130 150 L20 150 Z" fill="currentColor" opacity="0.15"/>
<!-- Armrests -->
<rect x="10" y="90" width="16" height="55" rx="6" fill="currentColor" opacity="0.2"/>
<rect x="124" y="90" width="16" height="55" rx="6" fill="currentColor" opacity="0.2"/>
<!-- Chair legs -->
<line x1="25" y1="150" x2="22" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.2"/>
<line x1="125" y1="150" x2="128" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.2"/>
<!-- Body (sitting, small) -->
<ellipse cx="75" cy="120" rx="20" ry="25" fill="currentColor" opacity="0.25"/>
<!-- Head (tilted, reading happily) -->
<circle cx="75" cy="82" r="16" fill="currentColor" opacity="0.3"/>
<!-- Happy eyes (curved) -->
<path d="M66 79 Q69 76 72 79" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.5"/>
<path d="M80 78 Q83 75 86 78" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.5"/>
<!-- Small smile -->
<path d="M70 88 Q75 93 80 88" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
<!-- Beak (small, happy) -->
<polygon points="88,83 98,80 88,78" fill="currentColor" opacity="0.3"/>
<!-- Arms holding open book -->
<line x1="55" y1="112" x2="40" y2="125" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="95" y1="112" x2="110" y2="125" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Open book (big, in lap) -->
<rect x="38" y="125" width="28" height="20" rx="1" fill="currentColor" opacity="0.3"/>
<rect x="66" y="125" width="28" height="20" rx="1" fill="currentColor" opacity="0.25"/>
<line x1="66" y1="125" x2="66" y2="145" stroke="currentColor" stroke-width="1.5" opacity="0.35"/>
<!-- Text lines on book pages -->
<line x1="42" y1="131" x2="62" y2="131" stroke="currentColor" stroke-width="0.8" opacity="0.15"/>
<line x1="42" y1="135" x2="60" y2="135" stroke="currentColor" stroke-width="0.8" opacity="0.15"/>
<line x1="70" y1="131" x2="90" y2="131" stroke="currentColor" stroke-width="0.8" opacity="0.15"/>
<line x1="70" y1="135" x2="88" y2="135" stroke="currentColor" stroke-width="0.8" opacity="0.15"/>
<!-- Legs (long, dangling from chair) -->
<line x1="62" y1="145" x2="50" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="88" y1="145" x2="100" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Big flat feet -->
<ellipse cx="45" cy="198" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
<ellipse cx="105" cy="198" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
<!-- Wrapping paper on floor -->
<path d="M30 175 Q45 170 55 178 Q60 182 50 185" stroke="currentColor" stroke-width="1.5" fill="currentColor" opacity="0.12"/>
<path d="M100 180 Q115 172 125 180 Q120 188 108 185" stroke="currentColor" stroke-width="1.5" fill="currentColor" opacity="0.1"/>
</svg>
<!-- Shadok 6 : Empileur balancing tower of books on head, arms out, worried eyes -->
<svg class="shadok-empileur" viewBox="0 0 140 220" fill="none" aria-hidden="true">
<!-- Precarious tower of books on head (big, detailed!) -->
<rect x="48" y="8" width="24" height="7" rx="1" fill="currentColor" opacity="0.2" transform="rotate(3 60 11)"/>
<rect x="46" y="16" width="28" height="7" rx="1" fill="currentColor" opacity="0.25" transform="rotate(-2 60 19)"/>
<rect x="44" y="24" width="32" height="7" rx="1" fill="currentColor" opacity="0.2" transform="rotate(4 60 27)"/>
<rect x="47" y="32" width="26" height="7" rx="1" fill="currentColor" opacity="0.28" transform="rotate(-3 60 35)"/>
<rect x="50" y="40" width="22" height="6" rx="1" fill="currentColor" opacity="0.22" transform="rotate(2 61 43)"/>
<!-- Head -->
<circle cx="65" cy="58" r="14" fill="currentColor" opacity="0.3"/>
<!-- Worried eyes (looking up at books) -->
<circle cx="60" cy="54" r="2.2" fill="currentColor" opacity="0.6"/>
<circle cx="70" cy="54" r="2.2" fill="currentColor" opacity="0.6"/>
<!-- Tiny worried pupils (looking up) -->
<circle cx="60" cy="53" r="1" fill="currentColor" opacity="0.35"/>
<circle cx="70" cy="53" r="1" fill="currentColor" opacity="0.35"/>
<!-- Worried eyebrows -->
<line x1="56" y1="50" x2="63" y2="51" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.4"/>
<line x1="74" y1="51" x2="67" y2="50" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.4"/>
<!-- Beak (slightly open, worried) -->
<polygon points="74,60 86,57 74,55" fill="currentColor" opacity="0.3"/>
<line x1="76" y1="58" x2="83" y2="58" stroke="currentColor" stroke-width="0.8" opacity="0.2"/>
<!-- Body (small oval, upright, tense) -->
<ellipse cx="65" cy="95" rx="20" ry="26" fill="currentColor" opacity="0.25"/>
<!-- Arms out wide for balance -->
<line x1="45" y1="88" x2="10" y2="82" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="85" y1="88" x2="120" y2="82" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Hands (small circles at arm ends) -->
<circle cx="8" cy="82" r="3" fill="currentColor" opacity="0.25"/>
<circle cx="122" cy="82" r="3" fill="currentColor" opacity="0.25"/>
<!-- Legs (long!) -->
<line x1="55" y1="121" x2="45" y2="192" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="75" y1="121" x2="85" y2="192" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Big flat feet -->
<ellipse cx="40" cy="196" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
<ellipse cx="90" cy="196" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
</svg>
<div class="container-content">
<div class="mx-auto max-w-2xl">
<div class="section-icon mx-auto mb-6">
<div class="i-lucide-shopping-bag h-12 w-12" />
</div>
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase text-center">
{{ content?.kicker }}
</p>
<h1 class="font-display text-3xl font-bold mb-4 text-center" style="color: hsl(var(--color-text))">
{{ content?.title }}
</h1>
<p class="text-lg leading-relaxed mb-10 text-center" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
<!-- Bookelis CTA -->
<div class="cta-block mb-8">
<div class="cta-icon">
<div class="i-lucide-globe h-5 w-5" />
</div>
<div class="flex-1">
<h2 class="font-display text-lg font-semibold mb-1" style="color: hsl(var(--color-text))">
Commander en ligne
</h2>
<p class="text-sm mb-3" style="color: hsl(var(--color-text-muted))">
Impression à la demande, livraison chez vous.
</p>
<a
:href="content?.bookelis?.url"
target="_blank"
rel="noopener"
class="order-btn"
>
<div class="i-lucide-external-link h-4 w-4" />
{{ content?.bookelis?.label }}
</a>
</div>
</div>
<!-- Librairie -->
<div class="cta-block mb-10">
<div class="cta-icon cta-icon--accent">
<div class="i-lucide-store h-5 w-5" />
</div>
<div class="flex-1">
<h2 class="font-display text-lg font-semibold mb-2" style="color: hsl(var(--color-text))">
{{ content?.librairie?.title }}
</h2>
<p class="leading-relaxed whitespace-pre-line" style="color: hsl(var(--color-text-muted))">
{{ content?.librairie?.text }}
</p>
</div>
</div>
<div class="text-center">
<UiBaseButton variant="ghost" to="/economique">
<div class="i-lucide-arrow-left mr-2 h-4 w-4" />
Autonomie économique
</UiBaseButton>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const { data: content } = await usePageContent('economique/commande')
useHead({
title: content.value?.meta?.title ?? 'Commander le livre',
})
</script>
<style scoped>
.section-icon {
display: flex;
align-items: center;
justify-content: center;
width: 5rem;
height: 5rem;
border-radius: 1rem;
background: hsl(var(--color-primary) / 0.1);
border: 1px solid hsl(var(--color-primary) / 0.2);
color: hsl(var(--color-primary));
}
.cta-block {
display: flex;
gap: 1rem;
padding: 1.5rem;
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
background: hsl(var(--color-surface));
}
.cta-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
background: hsl(var(--color-primary) / 0.12);
color: hsl(var(--color-primary));
flex-shrink: 0;
margin-top: 0.125rem;
}
.cta-icon--accent {
background: hsl(var(--color-accent) / 0.12);
color: hsl(var(--color-accent));
}
.order-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
text-decoration: none;
background: hsl(var(--color-primary));
color: white;
transition: all 0.2s;
}
.order-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px hsl(var(--color-primary) / 0.3);
}
.shadok-libraire {
position: absolute;
left: 2%;
top: 5%;
width: clamp(70px, 10vw, 140px);
opacity: 0.22;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-1 9s ease-in-out infinite;
}
.shadok-factrice {
position: absolute;
right: 2%;
top: 4%;
width: clamp(70px, 10vw, 140px);
opacity: 0.24;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-2 11s ease-in-out infinite;
}
.shadok-cartonnier {
position: absolute;
left: 3%;
top: 45%;
width: clamp(70px, 10vw, 130px);
opacity: 0.2;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-3 10s ease-in-out infinite;
}
.shadok-cycliste {
position: absolute;
left: 3%;
bottom: 8%;
width: clamp(75px, 10vw, 140px);
opacity: 0.18;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-4 8s ease-in-out infinite;
}
.shadok-lectrice {
position: absolute;
right: 2%;
bottom: 10%;
width: clamp(70px, 10vw, 135px);
opacity: 0.28;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-5 12s ease-in-out infinite;
}
.shadok-empileur {
position: absolute;
left: 50%;
bottom: 2%;
transform: translateX(-50%);
width: clamp(70px, 10vw, 130px);
opacity: 0.2;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-6 7s ease-in-out infinite;
}
@keyframes shadok-float-1 { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
@keyframes shadok-float-2 { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-10px) rotate(2deg); } }
@keyframes shadok-float-3 { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-12px) rotate(-1deg); } }
@keyframes shadok-float-4 { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-7px) rotate(1deg); } }
@keyframes shadok-float-5 { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
@keyframes shadok-float-6 { 0%, 100% { transform: translateX(-50%) translateY(0); } 50% { transform: translateX(-50%) translateY(-9px); } }
@media (max-width: 768px) {
.shadok-libraire,
.shadok-factrice,
.shadok-cartonnier,
.shadok-cycliste,
.shadok-lectrice,
.shadok-empileur { display: none; }
}
</style>

View File

@@ -0,0 +1,647 @@
<template>
<div class="relative overflow-hidden section-padding">
<!-- Shadok boulangère (top-left, walking profile, carrying bread tray) -->
<svg class="shadok shadok-boulangere" viewBox="0 0 160 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- body tilted forward walking -->
<ellipse cx="85" cy="105" rx="22" ry="28" fill="currentColor" opacity="0.25" transform="rotate(-8 85 105)" />
<!-- head -->
<circle cx="80" cy="65" r="16" fill="currentColor" opacity="0.3" />
<!-- eyes looking forward -->
<circle cx="88" cy="62" r="2" fill="currentColor" />
<circle cx="88" cy="68" r="1.5" fill="currentColor" />
<!-- beak pointing right -->
<path d="M94 64 L108 62 L94 68" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.2" />
<!-- arms up holding tray -->
<line x1="75" y1="88" x2="60" y2="42" stroke-width="3" />
<line x1="95" y1="88" x2="110" y2="42" stroke-width="3" />
<!-- big bread tray -->
<rect x="48" y="32" width="74" height="12" rx="3" fill="currentColor" opacity="0.25" />
<!-- bread loaves on tray -->
<ellipse cx="62" cy="30" rx="10" ry="6" fill="currentColor" opacity="0.3" />
<ellipse cx="85" cy="28" rx="11" ry="7" fill="currentColor" opacity="0.3" />
<ellipse cx="108" cy="30" rx="9" ry="6" fill="currentColor" opacity="0.3" />
<!-- score marks on loaves -->
<line x1="58" y1="28" x2="60" y2="32" opacity="0.4" />
<line x1="64" y1="27" x2="66" y2="31" opacity="0.4" />
<line x1="82" y1="26" x2="84" y2="30" opacity="0.4" />
<line x1="88" y1="25" x2="90" y2="29" opacity="0.4" />
<!-- flour apron -->
<path d="M70 95 Q85 92 100 95 L97 125 Q85 128 73 125 Z" fill="currentColor" opacity="0.1" />
<!-- flour dots -->
<circle cx="78" cy="100" r="1" fill="currentColor" opacity="0.3" />
<circle cx="92" cy="108" r="1.2" fill="currentColor" opacity="0.25" />
<circle cx="83" cy="115" r="0.8" fill="currentColor" opacity="0.3" />
<!-- long legs walking stride -->
<line x1="78" y1="132" x2="58" y2="185" stroke-width="3" />
<line x1="92" y1="132" x2="112" y2="180" stroke-width="3" />
<!-- big flat feet -->
<ellipse cx="52" cy="188" rx="12" ry="4" fill="currentColor" opacity="0.3" />
<ellipse cx="118" cy="183" rx="12" ry="4" fill="currentColor" opacity="0.3" />
</g>
</svg>
<!-- Shadok potier (top-right, sitting at wheel) -->
<svg class="shadok shadok-potier" viewBox="0 0 170 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- body sitting, slightly hunched -->
<ellipse cx="70" cy="100" rx="23" ry="26" fill="currentColor" opacity="0.25" transform="rotate(5 70 100)" />
<!-- head looking down at wheel -->
<circle cx="65" cy="65" r="15" fill="currentColor" opacity="0.3" />
<!-- eyes looking down -->
<circle cx="60" cy="68" r="2" fill="currentColor" />
<circle cx="72" cy="70" r="1.8" fill="currentColor" />
<!-- beak pointing down-right -->
<path d="M68 76 L80 82 L66 82" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.2" />
<!-- arms reaching to vase -->
<line x1="50" y1="92" x2="110" y2="115" stroke-width="3" />
<line x1="90" y1="90" x2="120" y2="110" stroke-width="3" />
<!-- hands on vase -->
<circle cx="112" cy="113" r="4" fill="currentColor" opacity="0.2" />
<circle cx="122" cy="108" r="4" fill="currentColor" opacity="0.2" />
<!-- vase being shaped -->
<path d="M108 95 Q102 108 106 125 Q115 135 124 125 Q128 108 122 95" fill="currentColor" opacity="0.2" stroke="currentColor" stroke-width="2" />
<!-- vase opening -->
<ellipse cx="115" cy="95" rx="8" ry="3" fill="currentColor" opacity="0.15" />
<!-- pottery wheel -->
<ellipse cx="115" cy="140" rx="28" ry="8" fill="currentColor" opacity="0.2" />
<line x1="115" y1="148" x2="115" y2="170" stroke-width="3" />
<line x1="100" y1="170" x2="130" y2="170" stroke-width="3" />
<!-- spinning motion lines -->
<path d="M88 138 Q85 135 88 132" fill="none" opacity="0.35" />
<path d="M142 138 Q145 135 142 132" fill="none" opacity="0.35" />
<path d="M90 145 Q86 143 90 140" fill="none" opacity="0.25" />
<!-- legs folded sitting -->
<line x1="60" y1="124" x2="40" y2="165" stroke-width="3" />
<line x1="80" y1="124" x2="75" y2="170" stroke-width="3" />
<!-- flat feet -->
<ellipse cx="34" cy="168" rx="11" ry="4" fill="currentColor" opacity="0.3" />
<ellipse cx="70" cy="173" rx="11" ry="4" fill="currentColor" opacity="0.3" />
</g>
</svg>
<!-- Shadok apicultrice (left, 40% down, with smoker and bees) -->
<svg class="shadok shadok-apicultrice" viewBox="0 0 160 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- body upright -->
<ellipse cx="75" cy="110" rx="20" ry="27" fill="currentColor" opacity="0.25" />
<!-- head -->
<circle cx="75" cy="70" r="15" fill="currentColor" opacity="0.3" />
<!-- beekeeping hat brim -->
<ellipse cx="75" cy="56" rx="22" ry="5" fill="currentColor" opacity="0.2" />
<!-- hat top -->
<path d="M58 56 Q58 42 75 40 Q92 42 92 56" fill="currentColor" opacity="0.15" />
<!-- veil hanging down -->
<path d="M53 56 L53 82 Q75 88 97 82 L97 56" fill="none" stroke-dasharray="3 2" opacity="0.3" />
<!-- eyes behind veil -->
<circle cx="70" cy="68" r="2" fill="currentColor" />
<circle cx="82" cy="66" r="1.8" fill="currentColor" />
<!-- beak -->
<path d="M80 74 L94 72 L82 78" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.2" />
<!-- arm left holding smoker -->
<line x1="56" y1="100" x2="28" y2="85" stroke-width="3" />
<!-- smoker device (big) -->
<rect x="10" y="72" width="22" height="20" rx="4" fill="currentColor" opacity="0.25" />
<path d="M14 72 L14 62 Q21 58 28 62 L28 72" fill="currentColor" opacity="0.15" />
<!-- smoke puffs -->
<circle cx="18" cy="55" r="5" fill="currentColor" opacity="0.12" />
<circle cx="24" cy="48" r="4" fill="currentColor" opacity="0.08" />
<circle cx="16" cy="42" r="3" fill="currentColor" opacity="0.06" />
<!-- arm right -->
<line x1="94" y1="100" x2="110" y2="90" stroke-width="3" />
<!-- buzzing bees (small detailed dots) -->
<circle cx="120" cy="60" r="2.5" fill="currentColor" opacity="0.35" />
<line x1="118" y1="58" x2="115" y2="56" opacity="0.3" />
<line x1="122" y1="58" x2="125" y2="55" opacity="0.3" />
<circle cx="135" cy="75" r="2" fill="currentColor" opacity="0.3" />
<line x1="133" y1="73" x2="131" y2="71" opacity="0.25" />
<line x1="137" y1="73" x2="139" y2="71" opacity="0.25" />
<circle cx="110" cy="48" r="2.2" fill="currentColor" opacity="0.25" />
<line x1="108" y1="46" x2="106" y2="44" opacity="0.2" />
<line x1="112" y1="46" x2="114" y2="43" opacity="0.2" />
<circle cx="140" cy="55" r="1.8" fill="currentColor" opacity="0.2" />
<circle cx="128" cy="42" r="2" fill="currentColor" opacity="0.22" />
<!-- long legs -->
<line x1="66" y1="136" x2="55" y2="192" stroke-width="3" />
<line x1="84" y1="136" x2="95" y2="192" stroke-width="3" />
<!-- flat feet -->
<ellipse cx="49" cy="195" rx="11" ry="4" fill="currentColor" opacity="0.3" />
<ellipse cx="101" cy="195" rx="11" ry="4" fill="currentColor" opacity="0.3" />
</g>
</svg>
<!-- Shadok forgeron (right, 35%, swinging hammer on anvil) -->
<svg class="shadok shadok-forgeron" viewBox="0 0 180 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- body leaning forward, muscular feel -->
<ellipse cx="90" cy="105" rx="24" ry="30" fill="currentColor" opacity="0.3" transform="rotate(-12 90 105)" />
<!-- head in profile -->
<circle cx="100" cy="65" r="16" fill="currentColor" opacity="0.3" />
<!-- eyes focused, intense -->
<circle cx="108" cy="62" r="2.5" fill="currentColor" />
<circle cx="108" cy="70" r="2" fill="currentColor" />
<!-- beak pointing right -->
<path d="M114 65 L130 62 L114 70" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.2" />
<!-- big arm swinging hammer UP -->
<line x1="72" y1="90" x2="40" y2="40" stroke-width="3.5" />
<!-- hammer head (big!) -->
<rect x="22" y="22" width="38" height="18" rx="3" fill="currentColor" opacity="0.3" />
<!-- hammer handle end -->
<line x1="40" y1="40" x2="42" y2="30" stroke-width="4" />
<!-- other arm resting on anvil -->
<line x1="110" y1="92" x2="140" y2="130" stroke-width="3" />
<!-- anvil (big detailed) -->
<path d="M120 135 L160 135 L168 145 L112 145 Z" fill="currentColor" opacity="0.25" />
<rect x="128" y="145" width="24" height="20" fill="currentColor" opacity="0.2" />
<rect x="122" y="165" width="36" height="6" rx="2" fill="currentColor" opacity="0.25" />
<!-- hot metal on anvil -->
<rect x="132" y="130" width="20" height="5" rx="1" fill="currentColor" opacity="0.35" />
<!-- sparks flying -->
<line x1="138" y1="128" x2="132" y2="118" opacity="0.4" />
<line x1="145" y1="126" x2="150" y2="116" opacity="0.35" />
<line x1="152" y1="128" x2="160" y2="120" opacity="0.3" />
<circle cx="130" cy="115" r="1.5" fill="currentColor" opacity="0.35" />
<circle cx="155" cy="112" r="1.2" fill="currentColor" opacity="0.3" />
<circle cx="162" cy="118" r="1" fill="currentColor" opacity="0.25" />
<!-- long legs wide stance -->
<line x1="78" y1="133" x2="55" y2="190" stroke-width="3" />
<line x1="100" y1="133" x2="120" y2="192" stroke-width="3" />
<!-- flat feet -->
<ellipse cx="48" cy="193" rx="13" ry="4" fill="currentColor" opacity="0.3" />
<ellipse cx="127" cy="195" rx="13" ry="4" fill="currentColor" opacity="0.3" />
</g>
</svg>
<!-- Shadok maraîchère (bottom-left, pushing wheelbarrow) -->
<svg class="shadok shadok-maraichere" viewBox="0 0 180 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- body leaning forward pushing -->
<ellipse cx="120" cy="95" rx="21" ry="26" fill="currentColor" opacity="0.25" transform="rotate(-20 120 95)" />
<!-- head tilted forward -->
<circle cx="130" cy="58" r="14" fill="currentColor" opacity="0.3" />
<!-- eyes looking down at path -->
<circle cx="136" cy="56" r="2" fill="currentColor" />
<circle cx="136" cy="63" r="1.5" fill="currentColor" />
<!-- beak pointing right-down -->
<path d="M140 60 L154 64 L140 66" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.2" />
<!-- arms pushing wheelbarrow handles -->
<line x1="104" y1="86" x2="72" y2="110" stroke-width="3" />
<line x1="108" y1="80" x2="72" y2="100" stroke-width="3" />
<!-- wheelbarrow body (big!) -->
<path d="M10 80 L70 80 L75 120 L5 120 Z" fill="currentColor" opacity="0.2" />
<path d="M10 80 L70 80 L75 120 L5 120 Z" />
<!-- wheelbarrow handles -->
<line x1="70" y1="90" x2="72" y2="110" stroke-width="3" />
<line x1="70" y1="100" x2="72" y2="100" stroke-width="2" />
<!-- wheel -->
<circle cx="12" cy="128" r="12" fill="currentColor" opacity="0.15" />
<circle cx="12" cy="128" r="12" />
<circle cx="12" cy="128" r="3" fill="currentColor" opacity="0.3" />
<!-- vegetables in wheelbarrow -->
<circle cx="22" cy="72" r="7" fill="currentColor" opacity="0.3" />
<circle cx="38" cy="70" r="8" fill="currentColor" opacity="0.25" />
<circle cx="55" cy="73" r="6" fill="currentColor" opacity="0.3" />
<ellipse cx="30" cy="76" rx="5" ry="8" fill="currentColor" opacity="0.2" transform="rotate(15 30 76)" />
<!-- carrot tops -->
<line x1="22" y1="66" x2="18" y2="58" opacity="0.35" />
<line x1="22" y1="66" x2="25" y2="57" opacity="0.35" />
<line x1="55" y1="67" x2="52" y2="60" opacity="0.3" />
<line x1="55" y1="67" x2="58" y2="59" opacity="0.3" />
<!-- long legs striding -->
<line x1="112" y1="118" x2="100" y2="180" stroke-width="3" />
<line x1="128" y1="118" x2="150" y2="178" stroke-width="3" />
<!-- flat feet -->
<ellipse cx="94" cy="183" rx="12" ry="4" fill="currentColor" opacity="0.3" />
<ellipse cx="156" cy="181" rx="12" ry="4" fill="currentColor" opacity="0.3" />
</g>
</svg>
<!-- Shadok tisserand (bottom-right, sitting at loom) -->
<svg class="shadok shadok-tisserand" viewBox="0 0 170 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- body sitting upright -->
<ellipse cx="55" cy="100" rx="20" ry="25" fill="currentColor" opacity="0.25" />
<!-- head looking down at loom -->
<circle cx="55" cy="65" r="15" fill="currentColor" opacity="0.3" />
<!-- eyes focused downward -->
<circle cx="62" cy="66" r="2" fill="currentColor" />
<circle cx="60" cy="72" r="1.5" fill="currentColor" />
<!-- beak pointing right-down -->
<path d="M66 70 L78 74 L64 76" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.2" />
<!-- arms reaching to loom -->
<line x1="72" y1="92" x2="100" y2="80" stroke-width="3" />
<line x1="74" y1="100" x2="105" y2="100" stroke-width="3" />
<!-- loom frame (big detailed) -->
<rect x="95" y="55" width="60" height="80" rx="3" fill="none" />
<!-- vertical loom posts -->
<line x1="95" y1="55" x2="95" y2="135" stroke-width="3" />
<line x1="155" y1="55" x2="155" y2="135" stroke-width="3" />
<!-- top beam -->
<line x1="95" y1="55" x2="155" y2="55" stroke-width="3" />
<!-- warp threads (vertical) -->
<line x1="105" y1="55" x2="105" y2="135" opacity="0.3" />
<line x1="115" y1="55" x2="115" y2="135" opacity="0.3" />
<line x1="125" y1="55" x2="125" y2="135" opacity="0.3" />
<line x1="135" y1="55" x2="135" y2="135" opacity="0.3" />
<line x1="145" y1="55" x2="145" y2="135" opacity="0.3" />
<!-- weft threads (horizontal, partial = work in progress) -->
<line x1="95" y1="70" x2="155" y2="70" opacity="0.25" />
<line x1="95" y1="80" x2="155" y2="80" opacity="0.25" />
<line x1="95" y1="90" x2="145" y2="90" opacity="0.25" />
<line x1="95" y1="100" x2="135" y2="100" opacity="0.2" />
<!-- shuttle in hand -->
<ellipse cx="105" cy="100" rx="8" ry="3" fill="currentColor" opacity="0.3" transform="rotate(-5 105 100)" />
<!-- woven fabric area -->
<rect x="97" y="62" width="56" height="28" fill="currentColor" opacity="0.08" />
<!-- legs folded sitting on stool -->
<line x1="45" y1="123" x2="30" y2="178" stroke-width="3" />
<line x1="65" y1="123" x2="70" y2="180" stroke-width="3" />
<!-- flat feet -->
<ellipse cx="24" cy="181" rx="11" ry="4" fill="currentColor" opacity="0.3" />
<ellipse cx="76" cy="183" rx="11" ry="4" fill="currentColor" opacity="0.3" />
<!-- stool -->
<rect x="38" y="124" width="30" height="6" rx="2" fill="currentColor" opacity="0.15" />
<line x1="42" y1="130" x2="40" y2="148" stroke-width="2" opacity="0.3" />
<line x1="64" y1="130" x2="66" y2="148" stroke-width="2" opacity="0.3" />
</g>
</svg>
<!-- Shadok berger (center-bottom, walking with sheep) -->
<svg class="shadok shadok-berger" viewBox="0 0 180 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- body walking, slight lean -->
<ellipse cx="85" cy="90" rx="20" ry="26" fill="currentColor" opacity="0.25" transform="rotate(-5 85 90)" />
<!-- head 3/4 view -->
<circle cx="88" cy="52" r="16" fill="currentColor" opacity="0.3" />
<!-- eyes looking ahead, slightly different directions -->
<circle cx="94" cy="48" r="2" fill="currentColor" />
<circle cx="96" cy="56" r="1.8" fill="currentColor" />
<!-- beak -->
<path d="M100 52 L114 50 L100 56" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.2" />
<!-- arm holding staff -->
<line x1="68" y1="82" x2="50" y2="60" stroke-width="3" />
<!-- shepherd's crook (big, detailed) -->
<line x1="50" y1="60" x2="44" y2="10" stroke-width="3" />
<path d="M44 10 Q44 0 54 0 Q62 0 62 10 Q62 18 54 18" fill="none" stroke-width="3" />
<!-- other arm relaxed -->
<line x1="104" y1="84" x2="118" y2="95" stroke-width="3" />
<!-- long legs walking -->
<line x1="76" y1="114" x2="60" y2="175" stroke-width="3" />
<line x1="94" y1="114" x2="110" y2="172" stroke-width="3" />
<!-- flat feet -->
<ellipse cx="54" cy="178" rx="12" ry="4" fill="currentColor" opacity="0.3" />
<ellipse cx="116" cy="175" rx="12" ry="4" fill="currentColor" opacity="0.3" />
<!-- sheep 1 (following) -->
<ellipse cx="140" cy="160" rx="14" ry="10" fill="currentColor" opacity="0.18" />
<circle cx="150" cy="152" r="6" fill="currentColor" opacity="0.15" />
<circle cx="153" cy="150" r="1" fill="currentColor" />
<line x1="132" y1="170" x2="132" y2="180" stroke-width="2" opacity="0.3" />
<line x1="148" y1="170" x2="148" y2="180" stroke-width="2" opacity="0.3" />
<!-- sheep 2 -->
<ellipse cx="158" cy="172" rx="12" ry="8" fill="currentColor" opacity="0.14" />
<circle cx="166" cy="166" r="5" fill="currentColor" opacity="0.12" />
<circle cx="168" cy="164" r="0.8" fill="currentColor" />
<line x1="152" y1="180" x2="152" y2="188" stroke-width="1.5" opacity="0.25" />
<line x1="164" y1="180" x2="164" y2="188" stroke-width="1.5" opacity="0.25" />
<!-- sheep 3 (smaller, behind) -->
<ellipse cx="170" cy="178" rx="9" ry="6" fill="currentColor" opacity="0.1" />
<circle cx="176" cy="174" r="4" fill="currentColor" opacity="0.08" />
<!-- dog at side -->
<ellipse cx="125" cy="170" rx="8" ry="5" fill="currentColor" opacity="0.2" />
<circle cx="130" cy="164" r="4" fill="currentColor" opacity="0.18" />
<circle cx="132" cy="163" r="1" fill="currentColor" />
<line x1="118" y1="168" x2="112" y2="165" stroke-width="1.5" opacity="0.3" />
<line x1="120" y1="175" x2="120" y2="182" stroke-width="1.5" opacity="0.25" />
<line x1="130" y1="175" x2="130" y2="182" stroke-width="1.5" opacity="0.25" />
</g>
</svg>
<!-- Shadok vigneronne (right, 55%, carrying grape basket) -->
<svg class="shadok shadok-vigneronne" viewBox="0 0 170 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- body seen from 3/4 back, leaning under weight -->
<ellipse cx="80" cy="105" rx="22" ry="28" fill="currentColor" opacity="0.25" transform="rotate(10 80 105)" />
<!-- head turned slightly -->
<circle cx="72" cy="65" r="15" fill="currentColor" opacity="0.3" />
<!-- eyes looking to the side -->
<circle cx="64" cy="62" r="2" fill="currentColor" />
<circle cx="66" cy="69" r="1.5" fill="currentColor" />
<!-- beak pointing left -->
<path d="M58 65 L44 62 L58 69" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.2" />
<!-- arm back holding basket strap -->
<line x1="98" y1="92" x2="115" y2="75" stroke-width="3" />
<!-- arm front with pruning shears -->
<line x1="62" y1="95" x2="38" y2="80" stroke-width="3" />
<!-- pruning shears (big) -->
<line x1="38" y1="80" x2="26" y2="68" stroke-width="3" />
<line x1="38" y1="80" x2="28" y2="78" stroke-width="3" />
<circle cx="36" cy="78" r="3" fill="currentColor" opacity="0.2" />
<!-- blade shapes -->
<path d="M26 68 Q22 72 28 74" fill="none" stroke-width="2" />
<path d="M28 78 Q22 76 24 72" fill="none" stroke-width="2" />
<!-- big basket on back with grapes -->
<path d="M100 60 L130 60 L135 120 L95 120 Z" fill="currentColor" opacity="0.2" />
<path d="M100 60 L130 60 L135 120 L95 120 Z" />
<!-- basket weave texture -->
<line x1="100" y1="75" x2="133" y2="75" opacity="0.2" />
<line x1="98" y1="90" x2="134" y2="90" opacity="0.2" />
<line x1="97" y1="105" x2="135" y2="105" opacity="0.2" />
<!-- grapes overflowing -->
<circle cx="108" cy="55" r="4" fill="currentColor" opacity="0.3" />
<circle cx="116" cy="53" r="4.5" fill="currentColor" opacity="0.3" />
<circle cx="124" cy="55" r="4" fill="currentColor" opacity="0.25" />
<circle cx="112" cy="48" r="3.5" fill="currentColor" opacity="0.25" />
<circle cx="120" cy="47" r="3.5" fill="currentColor" opacity="0.2" />
<circle cx="105" cy="52" r="3" fill="currentColor" opacity="0.2" />
<!-- grape stems -->
<line x1="115" y1="43" x2="115" y2="36" opacity="0.3" />
<line x1="113" y1="36" x2="117" y2="36" opacity="0.3" />
<!-- grapevine nearby -->
<line x1="148" y1="30" x2="148" y2="130" stroke-width="2" opacity="0.2" />
<path d="M148 50 Q158 45 162 55" fill="none" opacity="0.2" />
<path d="M148 75 Q160 70 165 80" fill="none" opacity="0.2" />
<circle cx="160" cy="58" r="3" fill="currentColor" opacity="0.12" />
<circle cx="163" cy="83" r="3" fill="currentColor" opacity="0.1" />
<!-- grape leaf -->
<path d="M148 90 Q155 85 158 90 Q160 95 155 98 Q150 95 148 90" fill="currentColor" opacity="0.12" />
<!-- long legs -->
<line x1="70" y1="132" x2="55" y2="192" stroke-width="3" />
<line x1="90" y1="132" x2="105" y2="190" stroke-width="3" />
<!-- flat feet -->
<ellipse cx="49" cy="195" rx="12" ry="4" fill="currentColor" opacity="0.3" />
<ellipse cx="111" cy="193" rx="12" ry="4" fill="currentColor" opacity="0.3" />
</g>
</svg>
<div class="container-content">
<!-- Header -->
<header class="mb-12 text-center">
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-bold tracking-tight" style="color: hsl(var(--color-text))">
{{ content?.title }}
</h1>
<p class="mt-4 mx-auto max-w-2xl leading-relaxed" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
</header>
<div class="mx-auto max-w-3xl flex flex-col gap-8">
<!-- Monnaie libre -->
<NuxtLink to="/economique/monnaie-libre" class="item-card group">
<div class="item-header">
<div class="item-icon">
<span class="g1-icon">Ğ1</span>
</div>
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
Monnaie libre
</h2>
</div>
<p class="leading-relaxed mt-3" style="color: hsl(var(--color-text-muted))">
La Ğ1 (June) : une monnaie co-créée par ses membres, sans dette ni intérêt. Le dividende universel comme base.
</p>
<div class="mt-3 inline-flex items-center gap-1 text-sm text-primary group-hover:text-primary/80 transition-colors">
En savoir plus
<div class="i-lucide-arrow-right h-3.5 w-3.5" />
</div>
</NuxtLink>
<!-- Modèle économique — bloc livre -->
<div class="book-block">
<HomeBookSection
@open-player="showBookPlayer = true"
@open-pdf="showPdfReader = true"
/>
</div>
<!-- Productions collectives -->
<NuxtLink to="/economique/productions-collectives" class="item-card group">
<div class="item-header">
<div class="item-icon">
<div class="i-lucide-users h-5 w-5" />
</div>
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
Productions collectives
</h2>
<span class="gestation-badge">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
</div>
<p class="leading-relaxed mt-3" style="color: hsl(var(--color-text-muted))">
Une plateforme pour faciliter la création d'équipes et la réalisation de productions à l'échelle des bassins de vie. Passer la seconde.
</p>
<div class="mt-3 inline-flex items-center gap-1 text-sm text-primary group-hover:text-primary/80 transition-colors">
En savoir plus
<div class="i-lucide-arrow-right h-3.5 w-3.5" />
</div>
</NuxtLink>
</div>
</div>
<BookPlayer v-model="showBookPlayer" />
<BookPdfReader v-model="showPdfReader" />
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'default',
})
const { data: content } = await usePageContent('economique')
useHead({
title: content.value?.meta?.title ?? 'Autonomie économique',
})
const showBookPlayer = ref(false)
const showPdfReader = ref(false)
</script>
<style scoped>
.page-title {
font-size: clamp(2rem, 5vw, 2.75rem);
}
.item-card {
display: block;
padding: 1.5rem;
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
background: hsl(var(--color-surface));
transition: border-color 0.2s, transform 0.12s ease;
text-decoration: none;
}
.item-card:hover {
border-color: hsl(var(--color-primary) / 0.2);
transform: translateY(-3px);
box-shadow: 0 8px 24px hsl(var(--color-primary) / 0.08);
}
.item-header {
display: flex;
align-items: center;
gap: 0.75rem;
}
.g1-icon {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.1rem;
line-height: 1;
}
.item-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.5rem;
background: hsl(var(--color-primary) / 0.15);
color: hsl(var(--color-primary));
box-shadow: 0 0 12px hsl(var(--color-primary) / 0.12);
flex-shrink: 0;
}
.gestation-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
margin-left: auto;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
background: hsl(var(--color-accent) / 0.12);
color: hsl(var(--color-accent));
font-size: 0.7rem;
font-weight: 500;
font-family: var(--font-mono);
}
.book-block {
padding: 1.5rem;
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
background: hsl(var(--color-surface));
}
/* Shadok illustrations */
.shadok {
position: absolute;
pointer-events: none;
width: clamp(70px, 10vw, 140px);
z-index: 0;
}
.shadok-boulangere {
top: 1%;
left: 2%;
opacity: 0.22;
color: hsl(var(--color-primary));
animation: shadok-float-1 9s ease-in-out infinite;
}
.shadok-potier {
top: 1%;
right: 2%;
opacity: 0.2;
color: hsl(var(--color-accent));
animation: shadok-float-2 11s ease-in-out infinite;
}
.shadok-apicultrice {
left: 2%;
top: 40%;
opacity: 0.2;
color: hsl(var(--color-accent));
animation: shadok-float-3 10s ease-in-out infinite;
}
.shadok-forgeron {
right: 3%;
top: 35%;
opacity: 0.24;
color: hsl(var(--color-primary));
animation: shadok-float-4 8s ease-in-out infinite;
}
.shadok-maraichere {
bottom: 14%;
left: 1%;
opacity: 0.18;
color: hsl(var(--color-primary));
animation: shadok-float-5 12s ease-in-out infinite;
}
.shadok-tisserand {
bottom: 12%;
right: 1%;
opacity: 0.22;
color: hsl(var(--color-accent));
animation: shadok-float-6 9.5s ease-in-out infinite;
}
.shadok-berger {
bottom: 2%;
left: 50%;
transform: translateX(-50%);
opacity: 0.28;
color: hsl(var(--color-primary));
animation: shadok-float-7 11s ease-in-out infinite;
}
.shadok-vigneronne {
right: 2%;
top: 55%;
opacity: 0.2;
color: hsl(var(--color-accent));
animation: shadok-float-8 7.5s ease-in-out infinite;
}
@keyframes shadok-float-1 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes shadok-float-2 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes shadok-float-3 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
@keyframes shadok-float-4 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-9px); }
}
@keyframes shadok-float-5 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-11px); }
}
@keyframes shadok-float-6 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes shadok-float-7 {
0%, 100% { transform: translateX(-50%) translateY(0); }
50% { transform: translateX(-50%) translateY(-10px); }
}
@keyframes shadok-float-8 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-7px); }
}
@media (max-width: 768px) {
.shadok {
display: none;
}
}
</style>

View File

@@ -14,7 +14,7 @@
<nav class="mt-16 flex items-center justify-between border-t border-white/8 pt-8"> <nav class="mt-16 flex items-center justify-between border-t border-white/8 pt-8">
<NuxtLink <NuxtLink
v-if="prevChapter" v-if="prevChapter"
:to="`/modele-eco/${prevChapter.stem?.split('/').pop()}`" :to="`/economique/modele-eco/${prevChapter.stem?.split('/').pop()}`"
class="btn-ghost gap-2" class="btn-ghost gap-2"
> >
<div class="i-lucide-arrow-left h-4 w-4" /> <div class="i-lucide-arrow-left h-4 w-4" />
@@ -24,7 +24,7 @@
<NuxtLink <NuxtLink
v-if="nextChapter" v-if="nextChapter"
:to="`/modele-eco/${nextChapter.stem?.split('/').pop()}`" :to="`/economique/modele-eco/${nextChapter.stem?.split('/').pop()}`"
class="btn-ghost gap-2" class="btn-ghost gap-2"
> >
<span class="text-sm">{{ nextChapter.title }}</span> <span class="text-sm">{{ nextChapter.title }}</span>

View File

@@ -0,0 +1,562 @@
<template>
<div class="relative overflow-hidden section-padding">
<!-- Shadok illustrations -->
<!-- 1. Typographe placing movable type in composing stick, profile view -->
<svg class="shadok shadok-typographe" viewBox="0 0 170 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- body (small oval, profile facing right) -->
<ellipse cx="70" cy="72" rx="22" ry="28" fill="currentColor" opacity="0.28"/>
<!-- head -->
<circle cx="78" cy="36" r="16" fill="currentColor" opacity="0.3"/>
<!-- beak (pointy, profile right) -->
<polygon points="94,34 110,38 94,42" fill="currentColor" opacity="0.35"/>
<!-- eyes (profile one visible) -->
<circle cx="84" cy="33" r="2.5" fill="currentColor" opacity="0.6"/>
<!-- apron -->
<path d="M52 60 L88 60 L85 100 L55 100 Z" fill="currentColor" opacity="0.15"/>
<line x1="70" y1="60" x2="70" y2="100" stroke="currentColor" stroke-width="1" opacity="0.2"/>
<!-- arm reaching to composing stick -->
<line x1="88" y1="65" x2="120" y2="55" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- other arm holding type block -->
<line x1="52" y1="68" x2="35" y2="58" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- composing stick (big, detailed) -->
<rect x="110" y="40" width="50" height="14" rx="2" fill="currentColor" opacity="0.2"/>
<rect x="110" y="42" width="50" height="10" rx="1" stroke="currentColor" stroke-width="1" fill="none" opacity="0.25"/>
<!-- type blocks in stick -->
<rect x="114" y="44" width="6" height="7" rx="0.5" fill="currentColor" opacity="0.3"/>
<rect x="122" y="44" width="5" height="7" rx="0.5" fill="currentColor" opacity="0.25"/>
<rect x="129" y="44" width="7" height="7" rx="0.5" fill="currentColor" opacity="0.3"/>
<rect x="138" y="44" width="5" height="7" rx="0.5" fill="currentColor" opacity="0.22"/>
<rect x="145" y="44" width="6" height="7" rx="0.5" fill="currentColor" opacity="0.28"/>
<!-- tiny letter on held block -->
<rect x="30" y="52" width="8" height="10" rx="1" fill="currentColor" opacity="0.25"/>
<text x="32" y="60" font-size="6" fill="currentColor" opacity="0.5" font-family="serif">A</text>
<!-- long legs -->
<line x1="62" y1="98" x2="50" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="78" y1="98" x2="90" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- big flat feet -->
<ellipse cx="45" cy="168" rx="10" ry="4" fill="currentColor" opacity="0.25"/>
<ellipse cx="95" cy="168" rx="10" ry="4" fill="currentColor" opacity="0.25"/>
</svg>
<!-- 2. Lectrice sitting in armchair, legs crossed, book on lap, reading glasses -->
<svg class="shadok shadok-lectrice" viewBox="0 0 180 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- armchair back -->
<path d="M30 55 Q25 50 28 30 L85 28 Q88 50 83 55" fill="currentColor" opacity="0.12"/>
<!-- armchair seat -->
<path d="M25 55 L88 55 L92 90 L20 90 Z" fill="currentColor" opacity="0.15"/>
<!-- armchair arms -->
<rect x="15" y="45" width="12" height="45" rx="5" fill="currentColor" opacity="0.15"/>
<rect x="85" y="45" width="12" height="45" rx="5" fill="currentColor" opacity="0.15"/>
<!-- body (seated, small) -->
<ellipse cx="58" cy="68" rx="20" ry="25" fill="currentColor" opacity="0.28"/>
<!-- head -->
<circle cx="58" cy="32" r="15" fill="currentColor" opacity="0.3"/>
<!-- beak (small, facing right-down toward book) -->
<polygon points="70,35 80,40 70,42" fill="currentColor" opacity="0.3"/>
<!-- reading glasses -->
<circle cx="52" cy="30" r="6" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.4"/>
<circle cx="65" cy="30" r="6" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.4"/>
<line x1="58" y1="30" x2="59" y2="30" stroke="currentColor" stroke-width="1.5" opacity="0.35"/>
<!-- eyes behind glasses (looking down) -->
<circle cx="53" cy="31" r="2" fill="currentColor" opacity="0.55"/>
<circle cx="64" cy="31" r="2" fill="currentColor" opacity="0.55"/>
<!-- arms holding open book on lap -->
<line x1="40" y1="60" x2="35" y2="82" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="76" y1="60" x2="80" y2="82" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- open book on lap -->
<path d="M32 80 L58 88 L84 80 L84 95 L58 103 L32 95 Z" fill="currentColor" opacity="0.18"/>
<line x1="58" y1="88" x2="58" y2="103" stroke="currentColor" stroke-width="1" opacity="0.25"/>
<!-- text lines on pages -->
<line x1="37" y1="86" x2="54" y2="91" stroke="currentColor" stroke-width="0.7" opacity="0.2"/>
<line x1="37" y1="89" x2="54" y2="94" stroke="currentColor" stroke-width="0.7" opacity="0.2"/>
<line x1="62" y1="91" x2="79" y2="86" stroke="currentColor" stroke-width="0.7" opacity="0.2"/>
<line x1="62" y1="94" x2="79" y2="89" stroke="currentColor" stroke-width="0.7" opacity="0.2"/>
<!-- crossed legs (long!) -->
<line x1="48" y1="90" x2="30" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="68" y1="90" x2="55" y2="150" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- the crossed leg goes over -->
<line x1="55" y1="150" x2="75" y2="140" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.22"/>
<!-- big flat feet -->
<ellipse cx="25" cy="168" rx="11" ry="4" fill="currentColor" opacity="0.25"/>
<ellipse cx="79" cy="142" rx="9" ry="3.5" fill="currentColor" opacity="0.22"/>
<!-- cup of tea nearby -->
<rect x="130" y="78" width="14" height="16" rx="3" fill="currentColor" opacity="0.2"/>
<ellipse cx="137" cy="78" rx="7" ry="2.5" fill="currentColor" opacity="0.25"/>
<!-- tea handle -->
<path d="M144 83 Q152 86 144 92" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.2"/>
<!-- steam -->
<path d="M134 73 Q132 68 135 64" stroke="currentColor" stroke-width="0.8" fill="none" opacity="0.15"/>
<path d="M139 74 Q141 69 138 65" stroke="currentColor" stroke-width="0.8" fill="none" opacity="0.15"/>
</svg>
<!-- 3. Calligraphe standing at tilted drafting table, large quill, ink flourishes -->
<svg class="shadok shadok-calligraphe" viewBox="0 0 180 220" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- drafting table (tilted, big) -->
<rect x="75" y="50" width="65" height="85" rx="3" fill="currentColor" opacity="0.12" transform="rotate(-15 108 92)"/>
<!-- table legs -->
<line x1="85" y1="130" x2="78" y2="195" stroke="currentColor" stroke-width="2.5" opacity="0.18"/>
<line x1="140" y1="115" x2="150" y2="195" stroke="currentColor" stroke-width="2.5" opacity="0.18"/>
<!-- paper on table -->
<rect x="85" y="58" width="48" height="65" rx="1" fill="currentColor" opacity="0.08" transform="rotate(-15 109 90)"/>
<!-- ink flourishes on paper -->
<path d="M95 80 Q105 70 115 82 Q120 90 110 95" stroke="currentColor" stroke-width="1.2" fill="none" opacity="0.25" transform="rotate(-15 105 85)"/>
<path d="M100 95 Q108 88 118 98" stroke="currentColor" stroke-width="0.8" fill="none" opacity="0.2" transform="rotate(-15 109 93)"/>
<!-- body (3/4 view, facing table) -->
<ellipse cx="55" cy="95" rx="21" ry="27" fill="currentColor" opacity="0.28"/>
<!-- head (turned toward table) -->
<circle cx="62" cy="58" r="16" fill="currentColor" opacity="0.3"/>
<!-- beak pointing at paper -->
<polygon points="75,55 90,52 78,60" fill="currentColor" opacity="0.32"/>
<!-- eyes (looking at paper, slightly different directions) -->
<circle cx="66" cy="55" r="2.5" fill="currentColor" opacity="0.6"/>
<circle cx="58" cy="54" r="2" fill="currentColor" opacity="0.5"/>
<!-- arm holding large quill -->
<line x1="72" y1="85" x2="105" y2="65" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- large quill -->
<line x1="105" y1="65" x2="100" y2="80" stroke="currentColor" stroke-width="1.5" opacity="0.35"/>
<path d="M105 65 L115 30 Q108 45 100 40 Q105 55 105 65" fill="currentColor" opacity="0.2"/>
<!-- other arm resting on table edge -->
<line x1="40" y1="88" x2="80" y2="85" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- ink trailing from quill tip -->
<path d="M100 80 Q95 90 98 100 Q102 108 96 115" stroke="currentColor" stroke-width="0.8" fill="none" opacity="0.2"/>
<!-- long legs -->
<line x1="45" y1="120" x2="35" y2="190" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="65" y1="120" x2="72" y2="190" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- big flat feet -->
<ellipse cx="30" cy="193" rx="11" ry="4" fill="currentColor" opacity="0.25"/>
<ellipse cx="77" cy="193" rx="11" ry="4" fill="currentColor" opacity="0.25"/>
</svg>
<!-- 4. Relieur sewing book spine with needle and thread, stack of signatures -->
<svg class="shadok shadok-relieur" viewBox="0 0 170 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- body (leaning forward over work) -->
<ellipse cx="85" cy="80" rx="22" ry="26" fill="currentColor" opacity="0.28" transform="rotate(10 85 80)"/>
<!-- head (tilted down, focused) -->
<circle cx="95" cy="46" r="15" fill="currentColor" opacity="0.3"/>
<!-- beak (pointing down at work) -->
<polygon points="105,52 115,62 103,58" fill="currentColor" opacity="0.32"/>
<!-- eyes (both looking down at different angles) -->
<circle cx="92" cy="44" r="2" fill="currentColor" opacity="0.55"/>
<circle cx="100" cy="46" r="2.5" fill="currentColor" opacity="0.6"/>
<!-- arm holding needle high -->
<line x1="100" y1="70" x2="130" y2="40" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- needle -->
<line x1="130" y1="40" x2="135" y2="32" stroke="currentColor" stroke-width="2" opacity="0.45"/>
<!-- thread from needle down to book -->
<path d="M132 38 Q140 55 125 75 Q115 90 120 100" stroke="currentColor" stroke-width="1" fill="none" stroke-dasharray="4 3" opacity="0.3"/>
<!-- other arm holding book spine -->
<line x1="68" y1="75" x2="50" y2="95" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- book being bound (open spine view, big) -->
<path d="M40 90 L60 85 L60 125 L40 130 Z" fill="currentColor" opacity="0.18"/>
<path d="M60 85 L80 90 L80 130 L60 125 Z" fill="currentColor" opacity="0.14"/>
<!-- stitching holes along spine -->
<circle cx="60" cy="92" r="1.2" fill="currentColor" opacity="0.35"/>
<circle cx="60" cy="100" r="1.2" fill="currentColor" opacity="0.35"/>
<circle cx="60" cy="108" r="1.2" fill="currentColor" opacity="0.35"/>
<circle cx="60" cy="116" r="1.2" fill="currentColor" opacity="0.35"/>
<!-- stack of folded signatures nearby -->
<rect x="10" y="125" width="30" height="5" rx="1" fill="currentColor" opacity="0.18"/>
<rect x="12" y="119" width="28" height="5" rx="1" fill="currentColor" opacity="0.15"/>
<rect x="11" y="113" width="29" height="5" rx="1" fill="currentColor" opacity="0.2"/>
<rect x="13" y="107" width="27" height="5" rx="1" fill="currentColor" opacity="0.16"/>
<!-- long legs -->
<line x1="75" y1="104" x2="65" y2="172" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="95" y1="104" x2="105" y2="172" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- big flat feet -->
<ellipse cx="60" cy="175" rx="10" ry="4" fill="currentColor" opacity="0.25"/>
<ellipse cx="110" cy="175" rx="10" ry="4" fill="currentColor" opacity="0.25"/>
</svg>
<!-- 5. Conteuse on small stage, arms dramatically wide, 3 tiny shadoks below -->
<svg class="shadok shadok-conteuse" viewBox="0 0 180 220" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- small stage/box -->
<rect x="55" y="110" width="60" height="20" rx="3" fill="currentColor" opacity="0.18"/>
<rect x="58" y="108" width="54" height="4" rx="1.5" fill="currentColor" opacity="0.22"/>
<!-- body (on stage, upright, dramatic) -->
<ellipse cx="85" cy="85" rx="20" ry="26" fill="currentColor" opacity="0.28"/>
<!-- head (thrown back slightly) -->
<circle cx="85" cy="50" r="16" fill="currentColor" opacity="0.3"/>
<!-- beak (open, telling story) -->
<polygon points="98,46 112,42 100,52" fill="currentColor" opacity="0.3"/>
<polygon points="100,52 112,55 98,54" fill="currentColor" opacity="0.22"/>
<!-- wide expressive eyes -->
<circle cx="82" cy="46" r="3" fill="currentColor" opacity="0.6"/>
<circle cx="92" cy="45" r="2.5" fill="currentColor" opacity="0.55"/>
<!-- arms dramatically wide -->
<line x1="65" y1="75" x2="15" y2="50" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<line x1="105" y1="75" x2="160" y2="50" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- gesture sparkles -->
<circle cx="10" cy="45" r="2" fill="currentColor" opacity="0.15"/>
<circle cx="165" cy="45" r="2" fill="currentColor" opacity="0.15"/>
<circle cx="18" cy="38" r="1.5" fill="currentColor" opacity="0.12"/>
<circle cx="157" cy="38" r="1.5" fill="currentColor" opacity="0.12"/>
<!-- long legs (standing on stage) -->
<line x1="77" y1="108" x2="70" y2="110" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="93" y1="108" x2="100" y2="110" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- big flat feet on stage -->
<ellipse cx="67" cy="112" rx="8" ry="3" fill="currentColor" opacity="0.2"/>
<ellipse cx="103" cy="112" rx="8" ry="3" fill="currentColor" opacity="0.2"/>
<!-- audience: 3 tiny shadoks below stage -->
<!-- tiny shadok 1 (left) -->
<ellipse cx="40" cy="165" rx="8" ry="10" fill="currentColor" opacity="0.18"/>
<circle cx="40" cy="152" r="6" fill="currentColor" opacity="0.22"/>
<circle cx="39" cy="151" r="1" fill="currentColor" opacity="0.45"/>
<circle cx="42" cy="151" r="1" fill="currentColor" opacity="0.45"/>
<polygon points="45,151 50,153 45,155" fill="currentColor" opacity="0.2"/>
<line x1="37" y1="175" x2="35" y2="195" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
<line x1="43" y1="175" x2="45" y2="195" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
<!-- tiny shadok 2 (center) -->
<ellipse cx="85" cy="168" rx="8" ry="10" fill="currentColor" opacity="0.18"/>
<circle cx="85" cy="155" r="6" fill="currentColor" opacity="0.22"/>
<circle cx="83" cy="154" r="1" fill="currentColor" opacity="0.45"/>
<circle cx="87" cy="154" r="1" fill="currentColor" opacity="0.45"/>
<polygon points="90,154 95,156 90,158" fill="currentColor" opacity="0.2"/>
<line x1="82" y1="178" x2="80" y2="198" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
<line x1="88" y1="178" x2="90" y2="198" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
<!-- tiny shadok 3 (right) -->
<ellipse cx="130" cy="163" rx="8" ry="10" fill="currentColor" opacity="0.18"/>
<circle cx="130" cy="150" r="6" fill="currentColor" opacity="0.22"/>
<circle cx="128" cy="149" r="1" fill="currentColor" opacity="0.45"/>
<circle cx="132" cy="149" r="1" fill="currentColor" opacity="0.45"/>
<polygon points="135,149 140,151 135,153" fill="currentColor" opacity="0.2"/>
<line x1="127" y1="173" x2="125" y2="193" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
<line x1="133" y1="173" x2="135" y2="193" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
</svg>
<!-- 6. Correcteur leaning forward, magnifying glass over manuscript, red pen -->
<svg class="shadok shadok-correcteur" viewBox="0 0 170 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- body (leaning forward heavily) -->
<ellipse cx="75" cy="82" rx="21" ry="28" fill="currentColor" opacity="0.28" transform="rotate(20 75 82)"/>
<!-- head (craned forward) -->
<circle cx="95" cy="48" r="15" fill="currentColor" opacity="0.3"/>
<!-- beak (pursed, critical) -->
<polygon points="108,45 118,48 108,52" fill="currentColor" opacity="0.3"/>
<!-- eyes (squinting, one bigger peering through glass) -->
<circle cx="92" cy="45" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="101" cy="46" r="3" fill="currentColor" opacity="0.6"/>
<!-- arm holding magnifying glass -->
<line x1="90" y1="72" x2="120" y2="90" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- magnifying glass (big) -->
<circle cx="128" cy="98" r="18" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.3"/>
<circle cx="128" cy="98" r="16" fill="currentColor" opacity="0.06"/>
<!-- handle -->
<line x1="140" y1="112" x2="155" y2="135" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- manuscript under magnifying glass -->
<rect x="95" y="115" width="55" height="70" rx="2" fill="currentColor" opacity="0.1"/>
<!-- text lines on manuscript -->
<line x1="100" y1="125" x2="140" y2="125" stroke="currentColor" stroke-width="0.8" opacity="0.18"/>
<line x1="100" y1="132" x2="145" y2="132" stroke="currentColor" stroke-width="0.8" opacity="0.18"/>
<line x1="100" y1="139" x2="138" y2="139" stroke="currentColor" stroke-width="0.8" opacity="0.18"/>
<line x1="100" y1="146" x2="142" y2="146" stroke="currentColor" stroke-width="0.8" opacity="0.18"/>
<line x1="100" y1="153" x2="135" y2="153" stroke="currentColor" stroke-width="0.8" opacity="0.18"/>
<!-- crossed-out text (red corrections) -->
<line x1="100" y1="132" x2="130" y2="132" stroke="currentColor" stroke-width="1.5" opacity="0.35"/>
<line x1="105" y1="146" x2="125" y2="146" stroke="currentColor" stroke-width="1.5" opacity="0.35"/>
<!-- other arm holding red pen -->
<line x1="58" y1="78" x2="35" y2="100" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- red pen -->
<line x1="35" y1="100" x2="25" y2="115" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.35"/>
<circle cx="24" cy="117" r="1.5" fill="currentColor" opacity="0.4"/>
<!-- long legs -->
<line x1="65" y1="108" x2="50" y2="178" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="82" y1="105" x2="95" y2="178" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- big flat feet -->
<ellipse cx="45" cy="181" rx="10" ry="4" fill="currentColor" opacity="0.25"/>
<ellipse cx="100" cy="181" rx="10" ry="4" fill="currentColor" opacity="0.25"/>
</svg>
<!-- 7. Colporteur walking profile, books in wooden crate on back, walking stick, hat -->
<svg class="shadok shadok-colporteur" viewBox="0 0 160 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- body (profile, walking right, leaning forward under weight) -->
<ellipse cx="70" cy="72" rx="20" ry="25" fill="currentColor" opacity="0.28" transform="rotate(15 70 72)"/>
<!-- head -->
<circle cx="82" cy="38" r="14" fill="currentColor" opacity="0.3"/>
<!-- hat (brimmed) -->
<ellipse cx="82" cy="26" rx="18" ry="5" fill="currentColor" opacity="0.25"/>
<rect x="72" y="16" width="20" height="12" rx="3" fill="currentColor" opacity="0.2"/>
<!-- beak (profile right) -->
<polygon points="94,36 106,40 94,43" fill="currentColor" opacity="0.32"/>
<!-- eye (profile one visible, determined) -->
<circle cx="88" cy="35" r="2.5" fill="currentColor" opacity="0.6"/>
<!-- wooden crate on back (big, loaded with books) -->
<rect x="25" y="35" width="40" height="50" rx="3" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.12"/>
<!-- strap over shoulder -->
<line x1="45" y1="35" x2="78" y2="55" stroke="currentColor" stroke-width="2" opacity="0.25"/>
<line x1="65" y1="35" x2="85" y2="60" stroke="currentColor" stroke-width="2" opacity="0.25"/>
<!-- books in crate (visible spines) -->
<rect x="28" y="38" width="6" height="44" rx="1" fill="currentColor" opacity="0.22"/>
<rect x="36" y="40" width="5" height="42" rx="1" fill="currentColor" opacity="0.18"/>
<rect x="43" y="37" width="7" height="45" rx="1" fill="currentColor" opacity="0.2"/>
<rect x="52" y="39" width="5" height="43" rx="1" fill="currentColor" opacity="0.16"/>
<rect x="58" y="38" width="4" height="44" rx="1" fill="currentColor" opacity="0.2"/>
<!-- arm forward with walking stick -->
<line x1="88" y1="65" x2="115" y2="80" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- walking stick (long) -->
<line x1="115" y1="80" x2="125" y2="195" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.3"/>
<!-- other arm back holding strap -->
<line x1="55" y1="68" x2="45" y2="55" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- long legs (walking stride) front leg forward, back leg behind -->
<line x1="78" y1="95" x2="100" y2="170" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="62" y1="95" x2="40" y2="170" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- big flat feet (walking) -->
<ellipse cx="105" cy="173" rx="11" ry="4" fill="currentColor" opacity="0.25"/>
<ellipse cx="35" cy="173" rx="10" ry="4" fill="currentColor" opacity="0.22"/>
</svg>
<!-- 8. Illustratrice at easel, brush in one hand, palette in other, canvas showing a shadok -->
<svg class="shadok shadok-illustratrice" viewBox="0 0 180 220" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- easel legs -->
<line x1="105" y1="30" x2="85" y2="210" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<line x1="145" y1="30" x2="165" y2="210" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<line x1="125" y1="60" x2="125" y2="210" stroke="currentColor" stroke-width="2" opacity="0.18"/>
<!-- canvas on easel -->
<rect x="95" y="25" width="60" height="75" rx="2" fill="currentColor" opacity="0.08"/>
<rect x="95" y="25" width="60" height="75" rx="2" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.2"/>
<!-- tiny shadok drawing on canvas! -->
<ellipse cx="125" cy="65" rx="8" ry="10" fill="currentColor" opacity="0.18"/>
<circle cx="125" cy="52" r="6" fill="currentColor" opacity="0.2"/>
<polygon points="130,51 136,53 130,55" fill="currentColor" opacity="0.18"/>
<circle cx="123" cy="51" r="1" fill="currentColor" opacity="0.3"/>
<circle cx="127" cy="51" r="1" fill="currentColor" opacity="0.3"/>
<line x1="121" y1="75" x2="118" y2="88" stroke="currentColor" stroke-width="1" opacity="0.15"/>
<line x1="129" y1="75" x2="132" y2="88" stroke="currentColor" stroke-width="1" opacity="0.15"/>
<!-- body (3/4 view, standing back from easel) -->
<ellipse cx="55" cy="90" rx="22" ry="28" fill="currentColor" opacity="0.28"/>
<!-- head (looking at canvas) -->
<circle cx="62" cy="52" r="16" fill="currentColor" opacity="0.3"/>
<!-- beak (profile right, toward canvas) -->
<polygon points="75,50 88,54 76,56" fill="currentColor" opacity="0.3"/>
<!-- eyes (artistic scrutiny, different sizes) -->
<circle cx="60" cy="49" r="2" fill="currentColor" opacity="0.55"/>
<circle cx="69" cy="50" r="2.8" fill="currentColor" opacity="0.6"/>
<!-- arm holding brush toward canvas -->
<line x1="73" y1="82" x2="100" y2="60" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- paintbrush -->
<line x1="100" y1="60" x2="108" y2="52" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
<circle cx="110" cy="50" r="2" fill="currentColor" opacity="0.3"/>
<!-- other arm holding palette -->
<line x1="38" y1="85" x2="18" y2="95" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- palette (big, with color dots) -->
<ellipse cx="12" cy="102" rx="16" ry="12" fill="currentColor" opacity="0.15"/>
<!-- thumb hole -->
<circle cx="12" cy="108" r="3" fill="currentColor" opacity="0.05"/>
<!-- paint dabs on palette -->
<circle cx="6" cy="97" r="2.5" fill="currentColor" opacity="0.3"/>
<circle cx="14" cy="94" r="2" fill="currentColor" opacity="0.25"/>
<circle cx="21" cy="98" r="2.5" fill="currentColor" opacity="0.28"/>
<circle cx="8" cy="104" r="2" fill="currentColor" opacity="0.22"/>
<circle cx="18" cy="103" r="2" fill="currentColor" opacity="0.3"/>
<!-- long legs -->
<line x1="45" y1="116" x2="35" y2="188" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="65" y1="116" x2="75" y2="188" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- big flat feet -->
<ellipse cx="30" cy="191" rx="10" ry="4" fill="currentColor" opacity="0.25"/>
<ellipse cx="80" cy="191" rx="10" ry="4" fill="currentColor" opacity="0.25"/>
</svg>
<div class="container-content">
<!-- Page de couverture du livre -->
<HomeBookSection
class="mb-16"
:show-chapters="false"
@open-player="showBookPlayer = true"
@open-pdf="showPdfReader = true"
/>
<header class="mb-12 text-center">
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-bold tracking-tight" style="color: hsl(var(--color-text))">
{{ content?.title }}
</h1>
<p class="mt-4 mx-auto max-w-2xl" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
</header>
<div class="mx-auto max-w-3xl">
<ul class="flex flex-col gap-3">
<li
v-for="chapter in chapters"
:key="chapter.path"
>
<NuxtLink
:to="`/economique/modele-eco/${chapter.stem?.split('/').pop()}`"
class="card-surface flex items-start gap-4 group"
>
<span class="font-mono text-2xl font-bold text-primary/30 leading-none mt-1 w-10 text-right flex-shrink-0">
{{ String(chapter.order).padStart(2, '0') }}
</span>
<div class="min-w-0 flex-1">
<h2 class="font-display text-lg font-semibold text-white group-hover:text-primary transition-colors">
{{ chapter.title }}
</h2>
<p v-if="chapter.description" class="mt-1 text-sm text-white/50">
{{ chapter.description }}
</p>
<div class="mt-2 flex items-center gap-3">
<span v-if="chapter.readingTime" class="text-xs text-white/30">
<span class="i-lucide-clock inline-block h-3 w-3 mr-1 align-middle" />
{{ chapter.readingTime }}
</span>
<SongBadges :chapter-slug="chapter.stem?.split('/').pop() ?? ''" />
</div>
</div>
<div class="i-lucide-chevron-right h-5 w-5 text-white/20 group-hover:text-primary/60 transition-colors flex-shrink-0 mt-2" />
</NuxtLink>
</li>
</ul>
</div>
</div>
<BookPlayer v-model="showBookPlayer" />
<BookPdfReader v-model="showPdfReader" />
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'default',
})
const { data: content } = await usePageContent('economique/modele-eco')
useHead({
title: content.value?.meta?.title ?? 'Table des matières',
})
const { data: chapters } = await useAsyncData('book-toc', () =>
queryCollection('book').order('order', 'ASC').all(),
)
const showBookPlayer = ref(false)
const showPdfReader = ref(false)
</script>
<style scoped>
.page-title {
font-size: clamp(2rem, 5vw, 2.75rem);
}
/* Shadok illustrations — shared */
.shadok {
position: absolute;
pointer-events: none;
width: clamp(70px, 10vw, 140px);
z-index: 0;
}
/* 1. Typographe — top left */
.shadok-typographe {
top: 1%;
left: 1%;
opacity: 0.24;
color: hsl(var(--color-primary));
animation: shadok-float-1 9s ease-in-out infinite;
}
/* 2. Lectrice — top right, sitting */
.shadok-lectrice {
top: 2%;
right: 1%;
opacity: 0.22;
color: hsl(var(--color-accent));
animation: shadok-float-2 11s ease-in-out infinite;
}
/* 3. Calligraphe — left, 40% */
.shadok-calligraphe {
top: 40%;
left: 1%;
opacity: 0.2;
color: hsl(var(--color-primary));
animation: shadok-float-3 10s ease-in-out infinite;
}
/* 4. Relieur — right, 35% */
.shadok-relieur {
top: 35%;
right: 2%;
opacity: 0.24;
color: hsl(var(--color-accent));
animation: shadok-float-4 8s ease-in-out infinite;
}
/* 5. Conteuse — bottom left */
.shadok-conteuse {
bottom: 6%;
left: 1%;
opacity: 0.22;
color: hsl(var(--color-primary));
animation: shadok-float-5 12s ease-in-out infinite;
}
/* 6. Correcteur — bottom right, leaning */
.shadok-correcteur {
bottom: 5%;
right: 1%;
opacity: 0.2;
color: hsl(var(--color-accent));
animation: shadok-float-6 9.5s ease-in-out infinite;
}
/* 7. Colporteur — center bottom, walking */
.shadok-colporteur {
bottom: 2%;
left: 50%;
transform: translateX(-50%);
opacity: 0.18;
color: hsl(var(--color-primary));
animation: shadok-float-7 7.5s ease-in-out infinite;
}
/* 8. Illustratrice — right, 58% */
.shadok-illustratrice {
top: 58%;
right: 1%;
opacity: 0.26;
color: hsl(var(--color-accent));
animation: shadok-float-8 10.5s ease-in-out infinite;
}
/* Float animations — each unique duration and offset */
@keyframes shadok-float-1 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes shadok-float-2 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes shadok-float-3 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
@keyframes shadok-float-4 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-9px); }
}
@keyframes shadok-float-5 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-11px); }
}
@keyframes shadok-float-6 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-7px); }
}
@keyframes shadok-float-7 {
0%, 100% { transform: translateX(-50%) translateY(0); }
50% { transform: translateX(-50%) translateY(-10px); }
}
@keyframes shadok-float-8 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
/* Hidden on mobile */
@media (max-width: 768px) {
.shadok {
display: none;
}
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<div class="section-padding">
<div class="container-content">
<div class="mx-auto max-w-2xl">
<div class="section-icon mx-auto mb-6">
<span v-if="content?.icon === 'g1'" class="g1-icon">Ğ1</span>
<div v-else :class="`i-lucide-${content?.icon ?? 'coins'}`" class="h-12 w-12" />
</div>
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase text-center">
{{ content?.kicker }}
</p>
<h1 class="font-display text-3xl font-bold mb-4 text-center" style="color: hsl(var(--color-text))">
{{ content?.title }}
</h1>
<p class="text-lg leading-relaxed mb-8 text-center" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
<!-- Content -->
<div v-if="content?.content" class="prose-block mb-8">
<p class="leading-relaxed whitespace-pre-line" style="color: hsl(var(--color-text-muted))">
{{ content.content }}
</p>
</div>
<!-- External links -->
<div v-if="content?.links" class="flex flex-col gap-3 mb-10">
<a
v-for="link in content.links"
:key="link.href"
:href="link.href"
target="_blank"
rel="noopener"
class="link-card group"
>
<div class="link-icon">
<div :class="`i-lucide-${link.icon ?? 'external-link'} h-4 w-4`" />
</div>
<span class="text-sm font-medium" style="color: hsl(var(--color-text))">{{ link.label }}</span>
<div class="i-lucide-arrow-up-right h-3.5 w-3.5 ml-auto text-primary/40 group-hover:text-primary transition-colors" />
</a>
</div>
<div class="text-center">
<UiBaseButton variant="ghost" to="/economique">
<div class="i-lucide-arrow-left mr-2 h-4 w-4" />
Autonomie économique
</UiBaseButton>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const { data: content } = await usePageContent('economique/monnaie-libre')
useHead({
title: content.value?.meta?.title ?? 'Monnaie libre',
})
</script>
<style scoped>
.g1-icon {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.75rem;
line-height: 1;
}
.section-icon {
display: flex;
align-items: center;
justify-content: center;
width: 5rem;
height: 5rem;
border-radius: 1rem;
background: hsl(var(--color-primary) / 0.1);
border: 1px solid hsl(var(--color-primary) / 0.2);
color: hsl(var(--color-primary));
}
.prose-block {
padding: 1.5rem;
border-radius: 0.75rem;
background: hsl(var(--color-surface));
}
.link-card {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--color-primary) / 0.1);
background: hsl(var(--color-surface));
text-decoration: none;
transition: border-color 0.2s;
}
.link-card:hover {
border-color: hsl(var(--color-primary) / 0.25);
}
.link-icon {
display: flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
border-radius: 0.375rem;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
flex-shrink: 0;
}
</style>

View File

@@ -0,0 +1,85 @@
<template>
<div class="section-padding">
<div class="container-content">
<div class="mx-auto max-w-2xl">
<div class="section-icon mx-auto mb-6">
<div :class="`i-lucide-${content?.icon ?? 'users'}`" class="h-12 w-12" />
</div>
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase text-center">
{{ content?.kicker }}
</p>
<h1 class="font-display text-3xl font-bold mb-4 text-center" style="color: hsl(var(--color-text))">
{{ content?.title }}
</h1>
<p class="text-lg leading-relaxed mb-8 text-center" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
<span v-if="content?.gestation" class="gestation-badge mx-auto mb-8">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
<!-- Content -->
<div v-if="content?.content" class="prose-block mb-10">
<p class="leading-relaxed whitespace-pre-line" style="color: hsl(var(--color-text-muted))">
{{ content.content }}
</p>
</div>
<div class="text-center">
<UiBaseButton variant="ghost" to="/economique">
<div class="i-lucide-arrow-left mr-2 h-4 w-4" />
Autonomie économique
</UiBaseButton>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const { data: content } = await usePageContent('economique/productions-collectives')
useHead({
title: content.value?.meta?.title ?? 'Productions collectives',
})
</script>
<style scoped>
.section-icon {
display: flex;
align-items: center;
justify-content: center;
width: 5rem;
height: 5rem;
border-radius: 1rem;
background: hsl(var(--color-primary) / 0.1);
border: 1px solid hsl(var(--color-primary) / 0.2);
color: hsl(var(--color-primary));
}
.gestation-badge {
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
background: hsl(var(--color-accent) / 0.12);
color: hsl(var(--color-accent));
font-size: 0.75rem;
font-weight: 500;
font-family: var(--font-mono);
width: fit-content;
}
.prose-block {
padding: 1.5rem;
border-radius: 0.75rem;
background: hsl(var(--color-surface));
}
</style>

View File

@@ -1,152 +1,340 @@
<template> <template>
<div class="relative overflow-hidden section-padding min-h-[70vh] flex items-center justify-center"> <div class="relative overflow-hidden section-padding min-h-[70vh] flex items-center justify-center">
<!-- Shadok jongleur: juggling coins (top-left) --> <!-- 1. Shadok funambule: walking on tightrope (top-left) -->
<svg class="shadok-juggler" viewBox="0 0 240 300" fill="none" aria-hidden="true"> <svg class="shadok-funambule" viewBox="0 0 170 200" fill="none" aria-hidden="true">
<!-- Tightrope -->
<line x1="5" y1="170" x2="165" y2="170" stroke="currentColor" stroke-width="2" opacity="0.3"/>
<!-- Body (small oval, leaning forward) -->
<ellipse cx="85" cy="110" rx="20" ry="28" fill="currentColor" opacity="0.25" transform="rotate(-5 85 110)"/>
<!-- Head -->
<circle cx="88" cy="72" r="16" fill="currentColor" opacity="0.3"/>
<!-- Eyes (focused, looking down-right) -->
<circle cx="93" cy="70" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="99" cy="71" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Beak (pointy, right side) -->
<polygon points="103,74 116,72 103,78" fill="currentColor" opacity="0.3"/>
<!-- Balancing pole (big, horizontal) -->
<line x1="10" y1="92" x2="160" y2="88" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Arms holding pole -->
<line x1="67" y1="100" x2="45" y2="92" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="103" y1="98" x2="125" y2="90" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Left leg (forward, on rope) -->
<line x1="78" y1="136" x2="70" y2="170" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="70" cy="172" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<!-- Right leg (back, lifted) -->
<line x1="92" y1="136" x2="108" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="108" cy="167" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- 2. Shadok accordeoniste: playing accordion (top-right, profile) -->
<svg class="shadok-accordeoniste" viewBox="0 0 180 210" fill="none" aria-hidden="true">
<!-- Body (profile, small) -->
<ellipse cx="70" cy="115" rx="22" ry="28" fill="currentColor" opacity="0.25"/>
<!-- Head (profile) -->
<circle cx="72" cy="75" r="16" fill="currentColor" opacity="0.3"/>
<!-- Beret -->
<ellipse cx="72" cy="60" rx="18" ry="6" fill="currentColor" opacity="0.3"/>
<circle cx="72" cy="56" r="4" fill="currentColor" opacity="0.25"/>
<!-- Eye (profile, one visible) -->
<circle cx="80" cy="73" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Beak (profile right) -->
<polygon points="86,76 98,74 86,80" fill="currentColor" opacity="0.3"/>
<!-- Accordion bellows (big, extended right) -->
<rect x="92" y="95" width="12" height="45" rx="2" fill="currentColor" opacity="0.3"/>
<line x1="98" y1="98" x2="98" y2="137" stroke="currentColor" stroke-width="0.8" opacity="0.2"/>
<rect x="107" y="93" width="8" height="49" rx="2" fill="currentColor" opacity="0.25"/>
<rect x="118" y="91" width="8" height="53" rx="2" fill="currentColor" opacity="0.2"/>
<rect x="129" y="89" width="8" height="57" rx="2" fill="currentColor" opacity="0.25"/>
<rect x="140" y="87" width="12" height="61" rx="2" fill="currentColor" opacity="0.3"/>
<!-- Keyboard dots on right panel -->
<circle cx="146" cy="100" r="1.5" fill="currentColor" opacity="0.2"/>
<circle cx="146" cy="110" r="1.5" fill="currentColor" opacity="0.2"/>
<circle cx="146" cy="120" r="1.5" fill="currentColor" opacity="0.2"/>
<circle cx="146" cy="130" r="1.5" fill="currentColor" opacity="0.2"/>
<!-- Left arm on bellows -->
<line x1="52" y1="105" x2="92" y2="105" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Right arm stretched to far end -->
<line x1="90" y1="108" x2="140" y2="100" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Foot tapping (right lifted) -->
<line x1="60" y1="141" x2="52" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="52" cy="197" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<line x1="80" y1="141" x2="90" y2="188" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="90" cy="190" rx="8" ry="3" fill="currentColor" opacity="0.3" transform="rotate(-15 90 190)"/>
<!-- Music notes -->
<text x="155" y="80" fill="currentColor" opacity="0.25" font-size="14">&#9834;</text>
<text x="145" y="68" fill="currentColor" opacity="0.2" font-size="11">&#9835;</text>
</svg>
<!-- 3. Shadok jongleur: 4 balls in the air (top-center) -->
<svg class="shadok-jongleur" viewBox="0 0 160 210" fill="none" aria-hidden="true">
<!-- Body --> <!-- Body -->
<ellipse cx="120" cy="160" rx="38" ry="46" fill="currentColor" opacity="0.85"/> <ellipse cx="80" cy="120" rx="21" ry="27" fill="currentColor" opacity="0.25"/>
<!-- Head --> <!-- Head -->
<circle cx="120" cy="98" r="24" fill="currentColor" opacity="0.8"/> <circle cx="80" cy="82" r="15" fill="currentColor" opacity="0.3"/>
<!-- Eyes (looking up at coins) --> <!-- Eyes (looking up, wide) -->
<circle cx="112" cy="92" r="3.5" fill="currentColor" opacity="0.2"/> <circle cx="74" cy="78" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="130" cy="92" r="3.5" fill="currentColor" opacity="0.2"/> <circle cx="86" cy="78" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="113" cy="91" r="1.5" fill="currentColor" opacity="0.5"/> <!-- Mouth open (concentration) -->
<circle cx="131" cy="91" r="1.5" fill="currentColor" opacity="0.5"/> <ellipse cx="80" cy="92" rx="4" ry="3" fill="currentColor" opacity="0.2"/>
<!-- Smile --> <!-- Beak (small, front view) -->
<path d="M112 108 Q120 114 128 108" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.35"/> <polygon points="80,86 87,89 80,92" fill="currentColor" opacity="0.3"/>
<!-- Arms up (juggling) --> <!-- Arms up wide -->
<line x1="85" y1="145" x2="55" y2="105" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/> <line x1="60" y1="110" x2="30" y2="75" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="155" y1="145" x2="185" y2="105" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/> <line x1="100" y1="110" x2="130" y2="75" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Hands --> <!-- 4 juggling balls in arc -->
<circle cx="55" cy="103" r="4" fill="currentColor" opacity="0.4"/> <circle cx="40" cy="35" r="8" fill="currentColor" opacity="0.3"/>
<circle cx="185" cy="103" r="4" fill="currentColor" opacity="0.4"/> <circle cx="70" cy="18" r="8" fill="currentColor" opacity="0.25"/>
<!-- Juggling coins --> <circle cx="100" cy="15" r="8" fill="currentColor" opacity="0.3"/>
<circle cx="90" cy="55" r="8" fill="currentColor" opacity="0.35"/> <circle cx="128" cy="30" r="8" fill="currentColor" opacity="0.25"/>
<text x="86" y="59" fill="currentColor" opacity="0.5" font-size="10" font-weight="bold">$</text> <!-- Long legs -->
<circle cx="120" cy="40" r="8" fill="currentColor" opacity="0.3"/> <line x1="72" y1="145" x2="60" y2="200" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<text x="116" y="44" fill="currentColor" opacity="0.45" font-size="10" font-weight="bold">$</text> <ellipse cx="60" cy="202" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<circle cx="150" cy="50" r="8" fill="currentColor" opacity="0.32"/> <line x1="88" y1="145" x2="100" y2="200" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<text x="146" y="54" fill="currentColor" opacity="0.48" font-size="10" font-weight="bold">$</text> <ellipse cx="100" cy="202" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<!-- Legs -->
<line x1="105" y1="203" x2="95" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="135" y1="203" x2="145" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
</svg> </svg>
<!-- Shadok échelle: on a wobbly ladder (top-right) --> <!-- 4. Shadok cracheur de feu: head tilted back, flame from mouth -->
<svg class="shadok-ladder" viewBox="0 0 220 320" fill="none" aria-hidden="true"> <svg class="shadok-cracheur" viewBox="0 0 170 220" fill="none" aria-hidden="true">
<!-- Ladder (tilting) -->
<line x1="80" y1="50" x2="70" y2="300" stroke="currentColor" stroke-width="3" opacity="0.35"/>
<line x1="150" y1="50" x2="140" y2="300" stroke="currentColor" stroke-width="3" opacity="0.35"/>
<!-- Rungs -->
<line x1="82" y1="80" x2="148" y2="80" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<line x1="83" y1="120" x2="147" y2="120" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<line x1="84" y1="160" x2="146" y2="160" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<line x1="85" y1="200" x2="145" y2="200" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<line x1="86" y1="240" x2="144" y2="240" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<!-- Shadok on top (arms out for balance) -->
<ellipse cx="115" cy="68" rx="18" ry="14" fill="currentColor" opacity="0.85"/>
<circle cx="115" cy="46" r="14" fill="currentColor" opacity="0.8"/>
<!-- Eyes (worried) -->
<circle cx="110" cy="43" r="3" fill="currentColor" opacity="0.25"/>
<circle cx="122" cy="43" r="3" fill="currentColor" opacity="0.25"/>
<circle cx="110" cy="44" r="1.2" fill="currentColor" opacity="0.5"/>
<circle cx="122" cy="44" r="1.2" fill="currentColor" opacity="0.5"/>
<!-- Worried mouth -->
<path d="M108 52 Q115 49 122 52" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
<!-- Arms out (balancing) -->
<line x1="97" y1="62" x2="60" y2="55" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.6"/>
<line x1="133" y1="62" x2="170" y2="55" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.6"/>
</svg>
<!-- Shadok acrobate: doing a cartwheel (center) -->
<svg class="shadok-acrobat" viewBox="0 0 260 240" fill="none" aria-hidden="true">
<!-- Body (sideways, mid-cartwheel) -->
<ellipse cx="130" cy="120" rx="30" ry="38" fill="currentColor" opacity="0.85" transform="rotate(45 130 120)"/>
<!-- Head -->
<circle cx="155" cy="82" r="20" fill="currentColor" opacity="0.8"/>
<!-- Eyes (dizzy/happy) -->
<path d="M148 78 Q152 74 156 78" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.4"/>
<path d="M160 78 Q164 74 168 78" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.4"/>
<!-- Smile -->
<path d="M150 90 Q158 95 165 90" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
<!-- Arms (one touching ground, one up) -->
<line x1="110" y1="100" x2="80" y2="130" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.6"/>
<line x1="150" y1="105" x2="185" y2="70" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.6"/>
<!-- Hand on ground -->
<circle cx="78" cy="132" r="4" fill="currentColor" opacity="0.4"/>
<!-- Legs (splayed in cartwheel) -->
<line x1="125" y1="155" x2="100" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.6"/>
<line x1="140" y1="150" x2="175" y2="175" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.6"/>
<!-- Motion lines -->
<path d="M70 110 Q60 105 55 115" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/>
<path d="M190 60 Q200 55 205 65" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/>
</svg>
<!-- Shadok dormeur: sleeping on a cloud (bottom-left) -->
<svg class="shadok-sleeper" viewBox="0 0 260 220" fill="none" aria-hidden="true">
<!-- Cloud -->
<ellipse cx="130" cy="150" rx="80" ry="25" fill="currentColor" opacity="0.2"/>
<circle cx="80" cy="140" r="25" fill="currentColor" opacity="0.18"/>
<circle cx="120" cy="130" r="30" fill="currentColor" opacity="0.2"/>
<circle cx="165" cy="135" r="22" fill="currentColor" opacity="0.18"/>
<circle cx="190" cy="142" r="18" fill="currentColor" opacity="0.15"/>
<!-- Shadok body (lying down) -->
<ellipse cx="130" cy="125" rx="35" ry="18" fill="currentColor" opacity="0.85"/>
<!-- Head (on cloud, sideways) -->
<ellipse cx="85" cy="118" rx="18" ry="16" fill="currentColor" opacity="0.8"/>
<!-- Closed eyes (sleeping) -->
<path d="M76 115 Q80 112 84 115" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.4"/>
<path d="M88 115 Q92 112 96 115" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.4"/>
<!-- Snooze bubbles -->
<text x="70" y="100" fill="currentColor" opacity="0.3" font-size="12" font-weight="bold">z</text>
<text x="60" y="85" fill="currentColor" opacity="0.25" font-size="16" font-weight="bold">z</text>
<text x="48" y="68" fill="currentColor" opacity="0.2" font-size="20" font-weight="bold">z</text>
<!-- Legs (curled) -->
<path d="M165 125 Q180 130 175 140" stroke="currentColor" stroke-width="3" stroke-linecap="round" fill="none" opacity="0.5"/>
<path d="M160 130 Q172 138 168 148" stroke="currentColor" stroke-width="3" stroke-linecap="round" fill="none" opacity="0.5"/>
</svg>
<!-- Shadok cuisinier: cooking in a cauldron (bottom-right) -->
<svg class="shadok-cook" viewBox="0 0 240 300" fill="none" aria-hidden="true">
<!-- Body --> <!-- Body -->
<ellipse cx="120" cy="145" rx="38" ry="45" fill="currentColor" opacity="0.85"/> <ellipse cx="75" cy="125" rx="22" ry="30" fill="currentColor" opacity="0.25"/>
<!-- Head (tilted back) -->
<circle cx="78" cy="82" r="16" fill="currentColor" opacity="0.3" />
<!-- Eyes (looking up) -->
<circle cx="73" cy="76" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="83" cy="76" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Beak (pointing up, open) -->
<polygon points="78,66 85,52 72,66" fill="currentColor" opacity="0.3"/>
<!-- Flame stream from beak (big, upward-right) -->
<path d="M82 54 Q100 25 95 10 Q110 30 120 8 Q115 35 135 15 Q120 42 140 30 Q118 50 130 45" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="currentColor" opacity="0.2"/>
<!-- Torch in left hand -->
<line x1="55" y1="115" x2="30" y2="90" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<rect x="24" y="78" width="8" height="16" rx="2" fill="currentColor" opacity="0.35"/>
<path d="M28 78 Q28 68 32 72 Q28 65 28 78" fill="currentColor" opacity="0.25"/>
<!-- Right arm out for balance -->
<line x1="95" y1="112" x2="125" y2="100" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Long legs -->
<line x1="66" y1="153" x2="55" y2="210" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="55" cy="212" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<line x1="84" y1="153" x2="95" y2="210" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="95" cy="212" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- 5. Shadok trapeziste: hanging from trapeze, body in arc -->
<svg class="shadok-trapeziste" viewBox="0 0 150 220" fill="none" aria-hidden="true">
<!-- Trapeze ropes -->
<line x1="40" y1="0" x2="50" y2="40" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<line x1="110" y1="0" x2="100" y2="40" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<!-- Trapeze bar -->
<line x1="48" y1="40" x2="102" y2="40" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.35"/>
<!-- Arms (hanging from bar) -->
<line x1="60" y1="42" x2="65" y2="70" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="90" y1="42" x2="85" y2="70" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Body (arched, graceful) -->
<ellipse cx="75" cy="95" rx="20" ry="25" fill="currentColor" opacity="0.25" transform="rotate(10 75 95)"/>
<!-- Head (below body, looking down) -->
<circle cx="78" cy="72" r="14" fill="currentColor" opacity="0.3"/>
<!-- Eyes (excited, looking down) -->
<circle cx="74" cy="74" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="84" cy="75" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Beak (pointing down-right) -->
<polygon points="86,78 96,84 86,82" fill="currentColor" opacity="0.3"/>
<!-- Legs (pointed, graceful, extending down-right) -->
<line x1="80" y1="118" x2="95" y2="175" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="70" y1="118" x2="82" y2="180" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Pointed feet -->
<polygon points="95,175 105,180 95,182" fill="currentColor" opacity="0.3"/>
<polygon points="82,180 92,185 82,187" fill="currentColor" opacity="0.3"/>
</svg>
<!-- 6. Shadok batteur: behind drum kit, sticks raised -->
<svg class="shadok-batteur" viewBox="0 0 180 210" fill="none" aria-hidden="true">
<!-- Snare drum (center, big) -->
<ellipse cx="90" cy="165" rx="35" ry="12" fill="currentColor" opacity="0.2"/>
<rect x="55" y="155" width="70" height="20" rx="3" fill="currentColor" opacity="0.2"/>
<ellipse cx="90" cy="155" rx="35" ry="12" fill="currentColor" opacity="0.25"/>
<!-- Hi-hat (left) -->
<ellipse cx="30" cy="140" rx="18" ry="5" fill="currentColor" opacity="0.2"/>
<line x1="30" y1="140" x2="30" y2="185" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<!-- Cymbal (right) -->
<ellipse cx="155" cy="120" rx="20" ry="5" fill="currentColor" opacity="0.2"/>
<line x1="155" y1="120" x2="155" y2="185" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<!-- Body (behind kit) -->
<ellipse cx="90" cy="110" rx="22" ry="28" fill="currentColor" opacity="0.25"/>
<!-- Head --> <!-- Head -->
<circle cx="120" cy="85" r="24" fill="currentColor" opacity="0.8"/> <circle cx="90" cy="70" r="16" fill="currentColor" opacity="0.3"/>
<!-- Chef hat --> <!-- Eyes (intense, looking at drums) -->
<ellipse cx="120" cy="62" rx="22" ry="18" fill="currentColor" opacity="0.35"/> <circle cx="84" cy="68" r="2" fill="currentColor" opacity="0.5"/>
<rect x="105" y="68" width="30" height="6" rx="1" fill="currentColor" opacity="0.4"/> <circle cx="96" cy="69" r="2" fill="currentColor" opacity="0.5"/>
<!-- Eyes (focused on cooking) --> <!-- Beak (front, small) -->
<circle cx="112" cy="82" r="3.5" fill="currentColor" opacity="0.2"/> <polygon points="90,76 97,80 90,82" fill="currentColor" opacity="0.3"/>
<circle cx="130" cy="82" r="3.5" fill="currentColor" opacity="0.2"/> <!-- Arms raised with drumsticks -->
<circle cx="113" cy="83" r="1.5" fill="currentColor" opacity="0.5"/> <line x1="70" y1="100" x2="40" y2="65" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<circle cx="131" cy="83" r="1.5" fill="currentColor" opacity="0.5"/> <line x1="40" y1="65" x2="25" y2="50" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
<!-- Tongue out (concentrating) --> <line x1="110" y1="100" x2="140" y2="60" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<path d="M115 96 Q120 100 125 96" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/> <line x1="140" y1="60" x2="158" y2="48" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
<!-- Arm with ladle --> <!-- Motion lines on sticks -->
<line x1="155" y1="135" x2="185" y2="175" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/> <path d="M22 52 Q18 48 20 44" stroke="currentColor" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.2"/>
<!-- Ladle --> <path d="M160 50 Q164 46 162 42" stroke="currentColor" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.2"/>
<line x1="185" y1="175" x2="175" y2="200" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.5"/> <!-- Legs (tucked behind kit) -->
<ellipse cx="175" cy="205" rx="8" ry="5" fill="currentColor" opacity="0.35"/> <line x1="80" y1="136" x2="70" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Other arm --> <ellipse cx="70" cy="197" rx="7" ry="3" fill="currentColor" opacity="0.25"/>
<line x1="85" y1="140" x2="60" y2="175" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/> <line x1="100" y1="136" x2="110" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Legs --> <ellipse cx="110" cy="197" rx="7" ry="3" fill="currentColor" opacity="0.25"/>
<line x1="105" y1="188" x2="95" y2="250" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/> </svg>
<line x1="135" y1="188" x2="145" y2="250" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Cauldron --> <!-- 7. Shadok marionnettiste: holding puppet strings -->
<path d="M55 220 Q55 260 120 260 Q185 260 185 220" fill="currentColor" opacity="0.3"/> <svg class="shadok-marionnettiste" viewBox="0 0 160 220" fill="none" aria-hidden="true">
<ellipse cx="120" cy="220" rx="65" ry="12" fill="currentColor" opacity="0.25"/> <!-- Body -->
<ellipse cx="120" cy="220" rx="65" ry="12" stroke="currentColor" stroke-width="2" fill="none" opacity="0.35"/> <ellipse cx="80" cy="75" rx="20" ry="26" fill="currentColor" opacity="0.25"/>
<!-- Steam --> <!-- Head -->
<path d="M95 210 Q90 195 95 185" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/> <circle cx="80" cy="40" r="15" fill="currentColor" opacity="0.3"/>
<path d="M120 208 Q118 190 122 180" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/> <!-- Eyes (looking down at puppet) -->
<path d="M145 210 Q148 195 143 185" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/> <circle cx="75" cy="42" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="86" cy="43" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Beak -->
<polygon points="89,44 100,42 89,48" fill="currentColor" opacity="0.3"/>
<!-- Arms down holding control bar -->
<line x1="62" y1="68" x2="50" y2="105" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="98" y1="68" x2="110" y2="105" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Control bar (cross) -->
<line x1="40" y1="108" x2="120" y2="108" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.35"/>
<line x1="80" y1="98" x2="80" y2="118" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.35"/>
<!-- Puppet strings -->
<line x1="45" y1="108" x2="55" y2="155" stroke="currentColor" stroke-width="1" opacity="0.25"/>
<line x1="80" y1="118" x2="70" y2="158" stroke="currentColor" stroke-width="1" opacity="0.25"/>
<line x1="115" y1="108" x2="85" y2="155" stroke="currentColor" stroke-width="1" opacity="0.25"/>
<line x1="80" y1="98" x2="70" y2="148" stroke="currentColor" stroke-width="1" opacity="0.25"/>
<!-- Mini puppet shadok -->
<circle cx="70" cy="155" r="8" fill="currentColor" opacity="0.2"/>
<ellipse cx="70" cy="172" rx="10" ry="13" fill="currentColor" opacity="0.15"/>
<!-- Puppet eyes -->
<circle cx="67" cy="154" r="1" fill="currentColor" opacity="0.4"/>
<circle cx="74" cy="154" r="1" fill="currentColor" opacity="0.4"/>
<!-- Puppet beak -->
<polygon points="76,156 82,155 76,158" fill="currentColor" opacity="0.2"/>
<!-- Puppet legs -->
<line x1="64" y1="184" x2="58" y2="208" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<line x1="76" y1="184" x2="82" y2="208" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<!-- Master shadok legs -->
<line x1="72" y1="99" x2="62" y2="155" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<line x1="88" y1="99" x2="98" y2="155" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<ellipse cx="62" cy="157" rx="7" ry="3" fill="currentColor" opacity="0.25"/>
<ellipse cx="98" cy="157" rx="7" ry="3" fill="currentColor" opacity="0.25"/>
</svg>
<!-- 8. Shadok clown: oversized shoes, red nose, squirting flower -->
<svg class="shadok-clown" viewBox="0 0 160 210" fill="none" aria-hidden="true">
<!-- Body -->
<ellipse cx="80" cy="110" rx="22" ry="28" fill="currentColor" opacity="0.25"/>
<!-- Head -->
<circle cx="80" cy="70" r="16" fill="currentColor" opacity="0.3"/>
<!-- Party hat (tall cone) -->
<polygon points="80,38 68,62 92,62" fill="currentColor" opacity="0.25"/>
<circle cx="80" cy="38" r="3" fill="currentColor" opacity="0.35"/>
<!-- Big red nose (circle on beak) -->
<circle cx="92" cy="74" r="5" fill="currentColor" opacity="0.4"/>
<!-- Beak behind nose -->
<polygon points="88,72 100,74 88,78" fill="currentColor" opacity="0.2"/>
<!-- Eyes (different directions goofy) -->
<circle cx="74" cy="66" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="85" cy="64" r="2" fill="currentColor" opacity="0.5"/>
<!-- Big goofy grin -->
<path d="M72 82 Q80 90 88 82" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
<!-- Squirting flower on chest -->
<circle cx="80" cy="95" r="6" fill="currentColor" opacity="0.25"/>
<circle cx="80" cy="95" r="2.5" fill="currentColor" opacity="0.35"/>
<!-- Water squirt from flower -->
<path d="M86 93 Q100 85 105 90" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/>
<circle cx="107" cy="90" r="2" fill="currentColor" opacity="0.2"/>
<circle cx="112" cy="88" r="1.5" fill="currentColor" opacity="0.15"/>
<!-- Arms -->
<line x1="60" y1="100" x2="35" y2="90" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="100" y1="100" x2="125" y2="85" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Long legs -->
<line x1="72" y1="136" x2="50" y2="185" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="88" y1="136" x2="110" y2="185" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- OVERSIZED shoes -->
<ellipse cx="42" cy="190" rx="18" ry="6" fill="currentColor" opacity="0.3"/>
<ellipse cx="118" cy="190" rx="18" ry="6" fill="currentColor" opacity="0.3"/>
</svg>
<!-- 9. Shadok acrobate: mid-cartwheel, body rotated 90deg -->
<svg class="shadok-acrobate" viewBox="0 0 160 200" fill="none" aria-hidden="true">
<!-- Body (rotated 90 degrees) -->
<ellipse cx="80" cy="100" rx="20" ry="26" fill="currentColor" opacity="0.25" transform="rotate(90 80 100)"/>
<!-- Head (at bottom-right, inverted) -->
<circle cx="112" cy="100" r="14" fill="currentColor" opacity="0.3"/>
<!-- Eyes (dizzy, spiral-ish) -->
<path d="M108 97 Q110 94 113 97" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.4"/>
<path d="M117 97 Q119 94 122 97" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.4"/>
<!-- Beak (pointing right) -->
<polygon points="124,102 136,100 124,106" fill="currentColor" opacity="0.3"/>
<!-- Limbs in pinwheel pattern -->
<!-- Arm up-right -->
<line x1="85" y1="80" x2="110" y2="35" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Arm down-left -->
<line x1="75" y1="120" x2="50" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Leg up-left (hand on ground) -->
<line x1="65" y1="88" x2="25" y2="55" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<circle cx="23" cy="53" r="4" fill="currentColor" opacity="0.25"/>
<!-- Leg down-right -->
<line x1="95" y1="112" x2="135" y2="150" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Feet -->
<ellipse cx="110" cy="33" rx="6" ry="3" fill="currentColor" opacity="0.25" transform="rotate(-60 110 33)"/>
<ellipse cx="135" cy="152" rx="6" ry="3" fill="currentColor" opacity="0.25" transform="rotate(40 135 152)"/>
<!-- Motion arc -->
<path d="M30 40 Q20 50 25 60" stroke="currentColor" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.2"/>
<path d="M140 140 Q148 148 145 158" stroke="currentColor" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.2"/>
</svg>
<!-- 10. Shadok regisseur: headset, clipboard, running -->
<svg class="shadok-regisseur" viewBox="0 0 160 210" fill="none" aria-hidden="true">
<!-- Body (leaning forward, running) -->
<ellipse cx="75" cy="105" rx="20" ry="27" fill="currentColor" opacity="0.25" transform="rotate(-12 75 105)"/>
<!-- Head -->
<circle cx="80" cy="68" r="15" fill="currentColor" opacity="0.3"/>
<!-- Headset arc -->
<path d="M66 60 Q80 48 94 60" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" fill="none" opacity="0.35"/>
<!-- Headset earpiece -->
<ellipse cx="66" cy="64" rx="4" ry="6" fill="currentColor" opacity="0.3"/>
<!-- Headset mic -->
<path d="M66 70 Q62 78 70 80" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
<!-- Eyes (stressed, one looking forward one sideways) -->
<circle cx="76" cy="65" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="87" cy="64" r="2" fill="currentColor" opacity="0.5"/>
<!-- Beak -->
<polygon points="91,68 104,66 91,72" fill="currentColor" opacity="0.3"/>
<!-- Lanyard -->
<line x1="80" y1="82" x2="80" y2="100" stroke="currentColor" stroke-width="1.5" opacity="0.25"/>
<!-- Badge -->
<rect x="74" y="100" width="12" height="15" rx="2" fill="currentColor" opacity="0.25"/>
<rect x="76" y="103" width="8" height="3" rx="1" fill="currentColor" opacity="0.15"/>
<!-- Right arm pointing forward (with authority) -->
<line x1="92" y1="95" x2="135" y2="75" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Pointing finger -->
<line x1="135" y1="75" x2="145" y2="70" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
<!-- Left arm holding clipboard -->
<line x1="58" y1="98" x2="38" y2="110" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Clipboard (big) -->
<rect x="22" y="105" width="22" height="30" rx="2" fill="currentColor" opacity="0.25"/>
<rect x="28" y="102" width="10" height="5" rx="1" fill="currentColor" opacity="0.3"/>
<!-- Clipboard lines -->
<line x1="26" y1="114" x2="40" y2="114" stroke="currentColor" stroke-width="1" opacity="0.15"/>
<line x1="26" y1="120" x2="38" y2="120" stroke="currentColor" stroke-width="1" opacity="0.15"/>
<line x1="26" y1="126" x2="40" y2="126" stroke="currentColor" stroke-width="1" opacity="0.15"/>
<!-- Legs (running stride, long) -->
<line x1="68" y1="130" x2="40" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="38" cy="197" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
<line x1="82" y1="130" x2="115" y2="190" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="117" cy="192" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
</svg> </svg>
<div class="container-content relative z-10 text-center"> <div class="container-content relative z-10 text-center">
<p class="mb-3 font-mono text-sm tracking-widest text-accent uppercase">{{ content?.kicker }}</p> <p class="mb-3 font-mono text-sm tracking-widest text-accent uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-extrabold tracking-tight text-white"> <h1 class="page-title font-display font-extrabold tracking-tight" style="color: hsl(var(--color-text))">
{{ content?.title }} {{ content?.title }}
</h1> </h1>
<p class="mt-4 text-lg text-white/50"> <p class="mt-4 text-lg" style="color: hsl(var(--color-text-muted))">
{{ content?.description }} {{ content?.description }}
</p> </p>
</div> </div>
@@ -170,98 +358,201 @@ useHead({
font-size: clamp(2.5rem, 6vw, 3.5rem); font-size: clamp(2.5rem, 6vw, 3.5rem);
} }
.shadok-juggler { /* 1. Funambule — top-left */
.shadok-funambule {
position: absolute; position: absolute;
left: 4%; left: 4%;
top: 5%; top: 5%;
width: clamp(100px, 14vw, 190px); width: clamp(70px, 10vw, 140px);
opacity: 0.3; opacity: 0.24;
pointer-events: none; pointer-events: none;
color: hsl(var(--color-primary)); color: hsl(var(--color-primary));
animation: shadok-bounce-juggler 4s ease-in-out infinite; animation: shadok-sway-funambule 9s ease-in-out infinite;
} }
.shadok-ladder { /* 2. Accordeoniste — top-right */
.shadok-accordeoniste {
position: absolute; position: absolute;
right: 4%; right: 3%;
top: 3%; top: 4%;
width: clamp(90px, 12vw, 170px); width: clamp(70px, 10vw, 140px);
opacity: 0.28; opacity: 0.22;
pointer-events: none; pointer-events: none;
color: hsl(var(--color-accent)); color: hsl(var(--color-accent));
animation: shadok-wobble-ladder 5s ease-in-out infinite; animation: shadok-bounce-accordeon 8s ease-in-out infinite;
} }
.shadok-acrobat { /* 3. Jongleur — top-center */
.shadok-jongleur {
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 55%; top: 2%;
transform: translateX(-50%); transform: translateX(-50%);
width: clamp(100px, 13vw, 180px); width: clamp(70px, 10vw, 130px);
opacity: 0.2; opacity: 0.2;
pointer-events: none; pointer-events: none;
color: hsl(var(--color-primary)); color: hsl(var(--color-primary));
animation: shadok-spin-acrobat 6s ease-in-out infinite; animation: shadok-float-jongleur 7s ease-in-out infinite;
} }
.shadok-sleeper { /* 4. Cracheur de feu — left 5%, 40% */
.shadok-cracheur {
position: absolute; position: absolute;
left: 3%; left: 5%;
bottom: 5%; top: 40%;
width: clamp(110px, 15vw, 210px); width: clamp(70px, 10vw, 140px);
opacity: 0.25; opacity: 0.22;
pointer-events: none; pointer-events: none;
color: hsl(var(--color-accent)); color: hsl(var(--color-accent));
animation: shadok-float-sleeper 8s ease-in-out infinite; animation: shadok-flicker-cracheur 8s ease-in-out infinite;
} }
.shadok-cook { /* 5. Trapeziste — right 3%, 30% */
.shadok-trapeziste {
position: absolute;
right: 3%;
top: 30%;
width: clamp(70px, 10vw, 130px);
opacity: 0.2;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-swing-trapeze 10s ease-in-out infinite;
}
/* 6. Batteur — left 4%, bottom 15% */
.shadok-batteur {
position: absolute;
left: 4%;
bottom: 15%;
width: clamp(70px, 10vw, 140px);
opacity: 0.2;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-pulse-batteur 7s ease-in-out infinite;
}
/* 7. Marionnettiste — right 4%, bottom 20% */
.shadok-marionnettiste {
position: absolute;
right: 4%;
bottom: 20%;
width: clamp(70px, 10vw, 130px);
opacity: 0.22;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-bob-marionnette 9s ease-in-out infinite;
}
/* 8. Clown — bottom-left */
.shadok-clown {
position: absolute;
left: 6%;
bottom: 3%;
width: clamp(70px, 10vw, 135px);
opacity: 0.24;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-wobble-clown 8s ease-in-out infinite;
}
/* 9. Acrobate — center-bottom */
.shadok-acrobate {
position: absolute;
left: 50%;
bottom: 2%;
transform: translateX(-50%);
width: clamp(70px, 10vw, 130px);
opacity: 0.18;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-spin-acrobate 12s ease-in-out infinite;
}
/* 10. Regisseur — bottom-right */
.shadok-regisseur {
position: absolute; position: absolute;
right: 3%; right: 3%;
bottom: 4%; bottom: 4%;
width: clamp(100px, 14vw, 200px); width: clamp(70px, 10vw, 140px);
opacity: 0.3; opacity: 0.22;
pointer-events: none; pointer-events: none;
color: hsl(var(--color-primary)); color: hsl(var(--color-accent));
animation: shadok-bounce-cook 5s ease-in-out infinite; animation: shadok-rush-regisseur 7s ease-in-out infinite;
} }
@keyframes shadok-bounce-juggler { @keyframes shadok-sway-funambule {
0%, 100% { transform: translateY(0) rotate(0deg); }
25% { transform: translateY(-5px) rotate(2deg); }
75% { transform: translateY(-3px) rotate(-2deg); }
}
@keyframes shadok-bounce-accordeon {
0%, 100% { transform: translateY(0); } 0%, 100% { transform: translateY(0); }
30% { transform: translateY(-12px); } 40% { transform: translateY(-8px); }
60% { transform: translateY(-6px); } 70% { transform: translateY(-3px); }
} }
@keyframes shadok-wobble-ladder { @keyframes shadok-float-jongleur {
0%, 100% { transform: translateX(-50%) translateY(0); }
50% { transform: translateX(-50%) translateY(-10px); }
}
@keyframes shadok-flicker-cracheur {
0%, 100% { transform: translateY(0) scale(1); }
30% { transform: translateY(-4px) scale(1.02); }
60% { transform: translateY(-2px) scale(0.99); }
}
@keyframes shadok-swing-trapeze {
0%, 100% { transform: rotate(0deg); } 0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(3deg); } 25% { transform: rotate(4deg); }
75% { transform: rotate(-3deg); } 75% { transform: rotate(-4deg); }
} }
@keyframes shadok-spin-acrobat { @keyframes shadok-pulse-batteur {
0%, 100% { transform: translateY(0); }
15% { transform: translateY(-4px); }
30% { transform: translateY(0); }
45% { transform: translateY(-6px); }
60% { transform: translateY(0); }
}
@keyframes shadok-bob-marionnette {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-7px); }
}
@keyframes shadok-wobble-clown {
0%, 100% { transform: translateY(0) rotate(0deg); }
30% { transform: translateY(-5px) rotate(-3deg); }
70% { transform: translateY(-2px) rotate(3deg); }
}
@keyframes shadok-spin-acrobate {
0% { transform: translateX(-50%) rotate(0deg); } 0% { transform: translateX(-50%) rotate(0deg); }
25% { transform: translateX(-50%) rotate(15deg); } 50% { transform: translateX(-50%) rotate(10deg); }
50% { transform: translateX(-50%) rotate(0deg); }
75% { transform: translateX(-50%) rotate(-15deg); }
100% { transform: translateX(-50%) rotate(0deg); } 100% { transform: translateX(-50%) rotate(0deg); }
} }
@keyframes shadok-float-sleeper { @keyframes shadok-rush-regisseur {
0%, 100% { transform: translateY(0); } 0%, 100% { transform: translateX(0) translateY(0); }
50% { transform: translateY(-6px); } 25% { transform: translateX(3px) translateY(-5px); }
} 50% { transform: translateX(-2px) translateY(-2px); }
75% { transform: translateX(2px) translateY(-6px); }
@keyframes shadok-bounce-cook {
0%, 100% { transform: translateY(0); }
40% { transform: translateY(-10px); }
70% { transform: translateY(-4px); }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.shadok-juggler { display: none; } .shadok-funambule,
.shadok-ladder { display: none; } .shadok-accordeoniste,
.shadok-acrobat { display: none; } .shadok-jongleur,
.shadok-sleeper { display: none; } .shadok-cracheur,
.shadok-cook { display: none; } .shadok-trapeziste,
.shadok-batteur,
.shadok-marionnettiste,
.shadok-clown,
.shadok-acrobate,
.shadok-regisseur {
display: none;
}
} }
</style> </style>

View File

@@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<HomeHeroSection /> <HomeHeroSection />
<HomeBookSection @open-player="showBookPlayer = true" @open-pdf="showPdfReader = true" /> <HomeAxisGrid @open-player="showBookPlayer = true" @open-pdf="showPdfReader = true" />
<HomeGrateWizardTeaser /> <HomeMessages />
<BookPlayer v-model="showBookPlayer" /> <BookPlayer v-model="showBookPlayer" />
<BookPdfReader v-model="showPdfReader" /> <BookPdfReader v-model="showPdfReader" />
</div> </div>

View File

@@ -1,199 +0,0 @@
<template>
<div class="relative overflow-hidden section-padding">
<!-- Shadok reader: character with big glasses reading a book -->
<svg class="shadok-reader" viewBox="0 0 240 300" fill="none" aria-hidden="true">
<!-- Body -->
<ellipse cx="120" cy="170" rx="45" ry="55" fill="currentColor" opacity="0.85"/>
<!-- Head -->
<circle cx="120" cy="100" r="28" fill="currentColor" opacity="0.8"/>
<!-- Big round glasses -->
<circle cx="107" cy="94" r="11" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.5"/>
<circle cx="133" cy="94" r="11" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.5"/>
<line x1="118" y1="94" x2="122" y2="94" stroke="currentColor" stroke-width="2" opacity="0.5"/>
<!-- Eyes behind glasses -->
<circle cx="108" cy="93" r="2.5" fill="currentColor" opacity="0.5"/>
<circle cx="134" cy="93" r="2.5" fill="currentColor" opacity="0.5"/>
<!-- Arms holding book -->
<line x1="78" y1="155" x2="60" y2="180" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="162" y1="155" x2="180" y2="180" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Book (open) -->
<rect x="55" y="175" width="55" height="40" rx="2" fill="currentColor" opacity="0.35"/>
<rect x="110" y="175" width="55" height="40" rx="2" fill="currentColor" opacity="0.3"/>
<line x1="110" y1="175" x2="110" y2="215" stroke="currentColor" stroke-width="2" opacity="0.5"/>
<!-- Book lines (text) -->
<line x1="65" y1="188" x2="100" y2="188" stroke="currentColor" stroke-width="1" opacity="0.2"/>
<line x1="65" y1="195" x2="95" y2="195" stroke="currentColor" stroke-width="1" opacity="0.2"/>
<line x1="65" y1="202" x2="98" y2="202" stroke="currentColor" stroke-width="1" opacity="0.2"/>
<!-- Legs -->
<line x1="105" y1="222" x2="95" y2="270" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="135" y1="222" x2="145" y2="270" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
</svg>
<!-- Shadok stack: pile of books tilting -->
<svg class="shadok-stack" viewBox="0 0 160 220" fill="none" aria-hidden="true">
<!-- Bottom book -->
<rect x="20" y="170" width="120" height="22" rx="3" fill="currentColor" opacity="0.5" transform="rotate(-2 80 181)"/>
<!-- Second book -->
<rect x="30" y="145" width="100" height="20" rx="3" fill="currentColor" opacity="0.45" transform="rotate(3 80 155)"/>
<!-- Third book -->
<rect x="25" y="120" width="110" height="18" rx="3" fill="currentColor" opacity="0.4" transform="rotate(-4 80 129)"/>
<!-- Fourth book -->
<rect x="35" y="97" width="90" height="18" rx="3" fill="currentColor" opacity="0.35" transform="rotate(5 80 106)"/>
<!-- Fifth book (tilting more) -->
<rect x="40" y="74" width="80" height="17" rx="3" fill="currentColor" opacity="0.3" transform="rotate(-7 80 82)"/>
<!-- Top book (really tilting) -->
<rect x="45" y="52" width="70" height="16" rx="3" fill="currentColor" opacity="0.25" transform="rotate(10 80 60)"/>
<!-- Tiny Shadok sitting on top -->
<ellipse cx="85" cy="42" rx="10" ry="8" fill="currentColor" opacity="0.5"/>
<circle cx="85" cy="30" r="6" fill="currentColor" opacity="0.45"/>
<circle cx="87" cy="29" r="1.5" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok scribe: character with quill and inkwell -->
<svg class="shadok-scribe" viewBox="0 0 240 280" fill="none" aria-hidden="true">
<ellipse cx="120" cy="155" rx="40" ry="48" fill="currentColor" opacity="0.85"/>
<circle cx="120" cy="92" r="25" fill="currentColor" opacity="0.8"/>
<ellipse cx="120" cy="72" rx="20" ry="8" fill="currentColor" opacity="0.35"/>
<circle cx="112" cy="90" r="4" fill="currentColor" opacity="0.2"/>
<circle cx="130" cy="90" r="4" fill="currentColor" opacity="0.2"/>
<circle cx="113" cy="91" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="131" cy="91" r="1.8" fill="currentColor" opacity="0.5"/>
<path d="M118 104 Q120 108 122 104" fill="currentColor" opacity="0.3"/>
<line x1="160" y1="140" x2="190" y2="170" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="190" y1="170" x2="210" y2="145" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
<path d="M210 145 Q215 140 212 135 Q208 138 210 145" fill="currentColor" opacity="0.3"/>
<line x1="80" y1="148" x2="55" y2="180" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<path d="M100 200 Q90 230 95 250" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" fill="none" opacity="0.6"/>
<path d="M140 200 Q150 230 145 250" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" fill="none" opacity="0.6"/>
<rect x="45" y="185" width="20" height="18" rx="3" fill="currentColor" opacity="0.35"/>
<ellipse cx="55" cy="185" rx="12" ry="4" fill="currentColor" opacity="0.3"/>
<rect x="170" y="175" width="40" height="50" rx="2" fill="currentColor" opacity="0.2"/>
<line x1="178" y1="188" x2="202" y2="188" stroke="currentColor" stroke-width="1" opacity="0.15"/>
<line x1="178" y1="195" x2="200" y2="195" stroke="currentColor" stroke-width="1" opacity="0.15"/>
<line x1="178" y1="202" x2="198" y2="202" stroke="currentColor" stroke-width="1" opacity="0.15"/>
</svg>
<div class="container-content">
<header class="mb-12 text-center">
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-bold tracking-tight text-white">
{{ content?.title }}
</h1>
<p class="mt-4 mx-auto max-w-2xl text-white/60">
{{ content?.description }}
</p>
</header>
<div class="mx-auto max-w-3xl">
<ul class="flex flex-col gap-3">
<li
v-for="chapter in chapters"
:key="chapter.path"
>
<NuxtLink
:to="`/modele-eco/${chapter.stem?.split('/').pop()}`"
class="card-surface flex items-start gap-4 group"
>
<span class="font-mono text-2xl font-bold text-primary/30 leading-none mt-1 w-10 text-right flex-shrink-0">
{{ String(chapter.order).padStart(2, '0') }}
</span>
<div class="min-w-0 flex-1">
<h2 class="font-display text-lg font-semibold text-white group-hover:text-primary transition-colors">
{{ chapter.title }}
</h2>
<p v-if="chapter.description" class="mt-1 text-sm text-white/50">
{{ chapter.description }}
</p>
<div class="mt-2 flex items-center gap-3">
<span v-if="chapter.readingTime" class="text-xs text-white/30">
<span class="i-lucide-clock inline-block h-3 w-3 mr-1 align-middle" />
{{ chapter.readingTime }}
</span>
<SongBadges :chapter-slug="chapter.stem?.split('/').pop() ?? ''" />
</div>
</div>
<div class="i-lucide-chevron-right h-5 w-5 text-white/20 group-hover:text-primary/60 transition-colors flex-shrink-0 mt-2" />
</NuxtLink>
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'default',
})
const { data: content } = await usePageContent('modele-eco')
useHead({
title: content.value?.meta?.title ?? 'Table des matières',
})
const { data: chapters } = await useAsyncData('book-toc', () =>
queryCollection('book').order('order', 'ASC').all(),
)
</script>
<style scoped>
.page-title {
font-size: clamp(2rem, 5vw, 2.75rem);
}
.shadok-reader {
position: absolute;
right: 2%;
top: 3%;
width: clamp(110px, 15vw, 220px);
opacity: 0.3;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-reader 9s ease-in-out infinite;
}
.shadok-stack {
position: absolute;
left: 2%;
bottom: 5%;
width: clamp(100px, 13vw, 180px);
opacity: 0.25;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-stack 11s ease-in-out infinite;
}
@keyframes shadok-float-reader {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes shadok-float-stack {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-8px) rotate(-2deg); }
}
.shadok-scribe {
position: absolute;
left: 50%;
bottom: 3%;
transform: translateX(-50%);
width: clamp(100px, 13vw, 180px);
opacity: 0.25;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-scribe 10s ease-in-out infinite;
}
@keyframes shadok-float-scribe {
0%, 100% { transform: translateX(-50%) translateY(0); }
50% { transform: translateX(-50%) translateY(-8px); }
}
@media (max-width: 768px) {
.shadok-reader { display: none; }
.shadok-stack { display: none; }
.shadok-scribe { display: none; }
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,586 @@
<template>
<div class="relative overflow-hidden section-padding">
<!-- Shadok codeuse sitting at desk, typing on laptop, glasses -->
<svg class="shadok-codeuse" viewBox="0 0 170 200" fill="none" aria-hidden="true">
<!-- Desk -->
<rect x="15" y="120" width="80" height="5" rx="2" fill="currentColor" opacity="0.3"/>
<line x1="25" y1="125" x2="25" y2="165" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.25"/>
<line x1="85" y1="125" x2="85" y2="165" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.25"/>
<!-- Laptop on desk -->
<rect x="30" y="108" width="50" height="12" rx="2" fill="currentColor" opacity="0.35"/>
<path d="M35 108 L40 80 L70 80 L75 108" fill="currentColor" opacity="0.28"/>
<rect x="42" y="84" width="26" height="18" rx="1" fill="currentColor" opacity="0.15"/>
<!-- Code on screen < /> -->
<text x="48" y="96" font-size="8" fill="currentColor" opacity="0.4" font-family="monospace">&lt;/&gt;</text>
<!-- Body seated, tilted forward -->
<ellipse cx="110" cy="105" rx="20" ry="26" fill="currentColor" opacity="0.25" transform="rotate(-10 110 105)"/>
<!-- Head looking at screen -->
<circle cx="105" cy="68" r="15" fill="currentColor" opacity="0.3"/>
<!-- Glasses -->
<circle cx="100" cy="66" r="5" stroke="currentColor" stroke-width="1.2" fill="none" opacity="0.4"/>
<circle cx="111" cy="66" r="5" stroke="currentColor" stroke-width="1.2" fill="none" opacity="0.4"/>
<line x1="105" y1="66" x2="106" y2="66" stroke="currentColor" stroke-width="1" opacity="0.35"/>
<!-- Eyes behind glasses -->
<circle cx="101" cy="66" r="1.5" fill="currentColor" opacity="0.5"/>
<circle cx="110" cy="65" r="1.5" fill="currentColor" opacity="0.5"/>
<!-- Beak pointy, looking left -->
<polygon points="88,68 82,65 88,72" fill="currentColor" opacity="0.35"/>
<!-- Arms reaching to laptop -->
<line x1="95" y1="95" x2="78" y2="110" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="125" y1="98" x2="80" y2="115" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Legs bent, seated -->
<line x1="100" y1="128" x2="90" y2="180" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="118" y1="128" x2="125" y2="180" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Feet -->
<ellipse cx="88" cy="183" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="127" cy="183" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok electricien profile view, climbing utility pole, tool belt -->
<svg class="shadok-electricien" viewBox="0 0 150 210" fill="none" aria-hidden="true">
<!-- Utility pole -->
<rect x="68" y="5" width="8" height="200" rx="2" fill="currentColor" opacity="0.2"/>
<line x1="50" y1="30" x2="94" y2="30" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.25"/>
<line x1="45" y1="55" x2="99" y2="55" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.22"/>
<!-- Cables hanging -->
<path d="M50 30 Q35 45 30 35" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.2"/>
<path d="M94 30 Q110 45 115 35" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.2"/>
<path d="M45 55 Q30 68 25 60" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.18"/>
<!-- Body climbing, profile, tilted -->
<ellipse cx="95" cy="100" rx="20" ry="25" fill="currentColor" opacity="0.25" transform="rotate(15 95 100)"/>
<!-- Head looking up at cable -->
<circle cx="100" cy="65" r="14" fill="currentColor" opacity="0.3"/>
<!-- Eyes looking up -->
<circle cx="96" cy="61" r="1.5" fill="currentColor" opacity="0.5"/>
<circle cx="105" cy="60" r="1.5" fill="currentColor" opacity="0.45"/>
<!-- Beak profile right -->
<polygon points="114,63 122,60 114,67" fill="currentColor" opacity="0.35"/>
<!-- Hard hat -->
<ellipse cx="100" cy="53" rx="16" ry="5" fill="currentColor" opacity="0.3"/>
<path d="M88 53 Q100 42 112 53" fill="currentColor" opacity="0.25"/>
<!-- Arms one gripping pole, one holding cable -->
<line x1="82" y1="90" x2="74" y2="75" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="108" y1="88" x2="118" y2="60" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Tool belt -->
<ellipse cx="95" cy="118" rx="22" ry="4" fill="currentColor" opacity="0.25"/>
<rect x="82" y="118" width="6" height="8" rx="1" fill="currentColor" opacity="0.2"/>
<rect x="105" y="118" width="5" height="10" rx="1" fill="currentColor" opacity="0.2"/>
<!-- Legs climbing, spread on pole -->
<line x1="85" y1="122" x2="72" y2="185" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="105" y1="122" x2="76" y2="175" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Feet gripping pole -->
<ellipse cx="70" cy="188" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="75" cy="178" rx="8" ry="3" fill="currentColor" opacity="0.28"/>
</svg>
<!-- Shadok soudeuse welding mask flipped up, torch with sparks, apron -->
<svg class="shadok-soudeuse" viewBox="0 0 160 210" fill="none" aria-hidden="true">
<!-- Body upright, slight lean forward -->
<ellipse cx="80" cy="110" rx="22" ry="28" fill="currentColor" opacity="0.25"/>
<!-- Apron -->
<path d="M62 95 L60 140 L100 140 L98 95" fill="currentColor" opacity="0.18"/>
<line x1="80" y1="95" x2="80" y2="140" stroke="currentColor" stroke-width="1" opacity="0.15"/>
<!-- Head -->
<circle cx="80" cy="65" r="16" fill="currentColor" opacity="0.3"/>
<!-- Welding mask flipped up on head -->
<rect x="65" y="42" width="30" height="18" rx="4" fill="currentColor" opacity="0.2"/>
<rect x="70" y="46" width="20" height="10" rx="2" fill="currentColor" opacity="0.12"/>
<!-- Eyes squinting from bright light -->
<line x1="73" y1="64" x2="77" y2="64" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" opacity="0.5"/>
<line x1="84" y1="63" x2="88" y2="63" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" opacity="0.45"/>
<!-- Beak small, front-facing -->
<polygon points="78,72 80,78 82,72" fill="currentColor" opacity="0.35"/>
<!-- Right arm holding torch -->
<line x1="100" y1="100" x2="135" y2="85" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Welding torch -->
<rect x="132" y="78" width="22" height="6" rx="2" fill="currentColor" opacity="0.35"/>
<line x1="154" y1="81" x2="160" y2="78" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<!-- Sparks from torch -->
<circle cx="158" cy="75" r="1.5" fill="currentColor" opacity="0.45"/>
<circle cx="155" cy="70" r="1" fill="currentColor" opacity="0.35"/>
<circle cx="160" cy="72" r="1.2" fill="currentColor" opacity="0.4"/>
<line x1="157" y1="76" x2="160" y2="68" stroke="currentColor" stroke-width="0.8" opacity="0.3"/>
<line x1="155" y1="78" x2="152" y2="71" stroke="currentColor" stroke-width="0.8" opacity="0.25"/>
<!-- Left arm down -->
<line x1="60" y1="100" x2="45" y2="125" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Legs standing, spread -->
<line x1="70" y1="136" x2="58" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="90" y1="136" x2="102" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Big flat feet -->
<ellipse cx="55" cy="198" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="105" cy="198" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok admin réseau tangled in cables, router at feet, frustrated -->
<svg class="shadok-admin-reseau" viewBox="0 0 160 200" fill="none" aria-hidden="true">
<!-- Body upright but struggling -->
<ellipse cx="80" cy="95" rx="21" ry="27" fill="currentColor" opacity="0.25"/>
<!-- Head tilted, frustrated -->
<circle cx="82" cy="52" r="15" fill="currentColor" opacity="0.3" />
<!-- Eyes looking in different directions (frustrated) -->
<circle cx="77" cy="50" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="88" cy="48" r="1.8" fill="currentColor" opacity="0.45"/>
<!-- Angry eyebrows -->
<line x1="74" y1="46" x2="79" y2="47" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.4"/>
<line x1="91" y1="44" x2="86" y2="46" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.4"/>
<!-- Beak open, yelling -->
<polygon points="73,56 65,55 73,60" fill="currentColor" opacity="0.35"/>
<line x1="67" y1="56" x2="66" y2="58" stroke="currentColor" stroke-width="1" opacity="0.3"/>
<!-- Cables tangling around body -->
<path d="M50 40 Q30 60 55 80 Q80 95 60 115 Q40 130 70 140" stroke="currentColor" stroke-width="2" fill="none" opacity="0.3"/>
<path d="M110 45 Q130 65 105 85 Q85 100 110 120 Q125 130 95 145" stroke="currentColor" stroke-width="2" fill="none" opacity="0.28"/>
<path d="M75 38 Q95 50 75 70" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.22"/>
<!-- Arms trying to untangle -->
<line x1="60" y1="85" x2="40" y2="70" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="100" y1="85" x2="120" y2="68" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Legs -->
<line x1="70" y1="120" x2="60" y2="175" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="90" y1="120" x2="100" y2="175" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Feet -->
<ellipse cx="57" cy="178" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="103" cy="178" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<!-- Router box at feet -->
<rect x="60" y="168" width="40" height="14" rx="3" fill="currentColor" opacity="0.25"/>
<circle cx="70" cy="175" r="1.5" fill="currentColor" opacity="0.35"/>
<circle cx="78" cy="175" r="1.5" fill="currentColor" opacity="0.3"/>
<circle cx="86" cy="175" r="1.5" fill="currentColor" opacity="0.35"/>
<!-- Antenna on router -->
<line x1="92" y1="168" x2="96" y2="155" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.25"/>
</svg>
<!-- Shadok imprimeuse 3D watching printer build object layer by layer -->
<svg class="shadok-imprimeuse3d" viewBox="0 0 170 210" fill="none" aria-hidden="true">
<!-- 3D printer frame -->
<rect x="10" y="90" width="65" height="60" rx="3" fill="currentColor" opacity="0.2"/>
<!-- Printer rails -->
<line x1="15" y1="95" x2="15" y2="145" stroke="currentColor" stroke-width="1.5" opacity="0.25"/>
<line x1="70" y1="95" x2="70" y2="145" stroke="currentColor" stroke-width="1.5" opacity="0.25"/>
<!-- Print bed -->
<rect x="18" y="135" width="49" height="4" rx="1" fill="currentColor" opacity="0.25"/>
<!-- Object being printed (layered) -->
<rect x="32" y="128" width="20" height="7" rx="1" fill="currentColor" opacity="0.3"/>
<rect x="34" y="122" width="16" height="6" rx="1" fill="currentColor" opacity="0.25"/>
<rect x="36" y="117" width="12" height="5" rx="1" fill="currentColor" opacity="0.2"/>
<!-- Extruder head -->
<rect x="34" y="110" width="16" height="6" rx="2" fill="currentColor" opacity="0.35"/>
<line x1="42" y1="116" x2="42" y2="118" stroke="currentColor" stroke-width="1.5" opacity="0.4"/>
<!-- Body leaning forward, watching intently -->
<ellipse cx="120" cy="105" rx="20" ry="25" fill="currentColor" opacity="0.25" transform="rotate(-15 120 105)"/>
<!-- Head looking down at printer -->
<circle cx="108" cy="70" r="15" fill="currentColor" opacity="0.3"/>
<!-- Eyes both staring down at printer, fascinated -->
<circle cx="103" cy="72" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="112" cy="73" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Beak pointing down -->
<polygon points="100,78 96,82 104,82" fill="currentColor" opacity="0.35"/>
<!-- Arms one pointing at printer -->
<line x1="105" y1="95" x2="75" y2="105" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="135" y1="98" x2="150" y2="115" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Legs standing, leaning -->
<line x1="112" y1="128" x2="105" y2="192" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="128" y1="128" x2="138" y2="192" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Feet -->
<ellipse cx="103" cy="195" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="140" cy="195" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok cryptographe 3/4 view, cloak, giant key + padlock -->
<svg class="shadok-cryptographe" viewBox="0 0 170 210" fill="none" aria-hidden="true">
<!-- Mysterious cloak -->
<path d="M55 70 Q50 110 45 160 L125 160 Q120 110 115 70" fill="currentColor" opacity="0.15"/>
<path d="M45 160 Q40 170 38 180 L132 180 Q130 170 125 160" fill="currentColor" opacity="0.12"/>
<!-- Body under cloak -->
<ellipse cx="85" cy="105" rx="22" ry="28" fill="currentColor" opacity="0.25"/>
<!-- Head 3/4 view, slightly turned -->
<circle cx="85" cy="55" r="16" fill="currentColor" opacity="0.3"/>
<!-- Hood suggestion -->
<path d="M68 50 Q85 35 102 50" fill="currentColor" opacity="0.18"/>
<!-- Eyes mysterious, one slightly larger -->
<circle cx="80" cy="53" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="91" cy="52" r="1.5" fill="currentColor" opacity="0.4"/>
<!-- Beak 3/4 profile -->
<polygon points="75,58 67,55 74,62" fill="currentColor" opacity="0.35"/>
<!-- Left arm holding giant key -->
<line x1="63" y1="95" x2="30" y2="110" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Giant key -->
<circle cx="18" cy="108" r="12" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.4"/>
<line x1="30" y1="108" x2="55" y2="108" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="48" y1="108" x2="48" y2="118" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<line x1="53" y1="108" x2="53" y2="115" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<!-- Right arm holding padlock -->
<line x1="107" y1="95" x2="140" y2="85" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Padlock big -->
<rect x="135" y="82" width="28" height="24" rx="4" fill="currentColor" opacity="0.3"/>
<path d="M141 82 L141 70 Q149 58 157 70 L157 82" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.35"/>
<circle cx="149" cy="95" r="3" fill="currentColor" opacity="0.2"/>
<line x1="149" y1="98" x2="149" y2="103" stroke="currentColor" stroke-width="1.5" opacity="0.2"/>
<!-- Legs long, cloak partly covering -->
<line x1="75" y1="130" x2="65" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="95" y1="130" x2="108" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Feet -->
<ellipse cx="63" cy="198" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="110" cy="198" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok droneuse looking up at flying drone, remote in hands -->
<svg class="shadok-droneuse" viewBox="0 0 160 210" fill="none" aria-hidden="true">
<!-- Drone above -->
<rect x="60" y="12" width="30" height="8" rx="3" fill="currentColor" opacity="0.3"/>
<!-- Drone arms -->
<line x1="55" y1="16" x2="35" y2="16" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<line x1="95" y1="16" x2="115" y2="16" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<!-- Propellers spinning -->
<ellipse cx="35" cy="14" rx="12" ry="3" fill="currentColor" opacity="0.2"/>
<ellipse cx="115" cy="14" rx="12" ry="3" fill="currentColor" opacity="0.2"/>
<!-- Propeller blur lines -->
<line x1="25" y1="12" x2="45" y2="12" stroke="currentColor" stroke-width="0.8" opacity="0.15"/>
<line x1="105" y1="12" x2="125" y2="12" stroke="currentColor" stroke-width="0.8" opacity="0.15"/>
<!-- Drone camera -->
<circle cx="75" cy="22" r="3" fill="currentColor" opacity="0.25"/>
<!-- Drone legs -->
<line x1="62" y1="20" x2="58" y2="28" stroke="currentColor" stroke-width="1.5" opacity="0.2"/>
<line x1="88" y1="20" x2="92" y2="28" stroke="currentColor" stroke-width="1.5" opacity="0.2"/>
<!-- Body upright, looking up -->
<ellipse cx="78" cy="115" rx="20" ry="26" fill="currentColor" opacity="0.25"/>
<!-- Head tilted back looking up -->
<circle cx="78" cy="72" r="15" fill="currentColor" opacity="0.3"/>
<!-- Eyes looking up at drone -->
<circle cx="74" cy="68" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="83" cy="67" r="1.8" fill="currentColor" opacity="0.45"/>
<!-- Beak pointing up -->
<polygon points="76,56 80,50 82,57" fill="currentColor" opacity="0.35"/>
<!-- Arms holding remote control -->
<line x1="60" y1="108" x2="50" y2="128" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="96" y1="108" x2="106" y2="128" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Remote control in hands -->
<rect x="42" y="126" width="70" height="14" rx="4" fill="currentColor" opacity="0.3"/>
<circle cx="55" cy="133" r="3" fill="currentColor" opacity="0.2"/>
<circle cx="100" cy="133" r="3" fill="currentColor" opacity="0.2"/>
<!-- Joysticks -->
<line x1="55" y1="130" x2="55" y2="127" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.3"/>
<line x1="100" y1="131" x2="102" y2="128" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.3"/>
<!-- Legs standing -->
<line x1="68" y1="138" x2="58" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="88" y1="138" x2="100" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Feet -->
<ellipse cx="55" cy="198" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="103" cy="198" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok mécanicienne lying under machine, legs sticking out, wrench, oil drops -->
<svg class="shadok-mecanicienne" viewBox="0 0 180 200" fill="none" aria-hidden="true">
<!-- Machine/server box above -->
<rect x="5" y="55" width="100" height="50" rx="5" fill="currentColor" opacity="0.2"/>
<rect x="12" y="62" width="35" height="12" rx="2" fill="currentColor" opacity="0.15"/>
<rect x="12" y="80" width="35" height="12" rx="2" fill="currentColor" opacity="0.15"/>
<!-- Machine details -->
<circle cx="20" cy="68" r="2" fill="currentColor" opacity="0.3"/>
<circle cx="20" cy="86" r="2" fill="currentColor" opacity="0.25"/>
<line x1="60" y1="65" x2="90" y2="65" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
<line x1="60" y1="72" x2="85" y2="72" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
<!-- Body lying flat under machine, only partial visible -->
<ellipse cx="80" cy="118" rx="25" ry="18" fill="currentColor" opacity="0.22" transform="rotate(90 80 118)"/>
<!-- Head under machine (barely visible) -->
<circle cx="55" cy="115" r="12" fill="currentColor" opacity="0.2"/>
<!-- Arm sticking out with wrench -->
<line x1="42" y1="115" x2="15" y2="105" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Wrench -->
<path d="M8 100 L15 105 L12 108 L5 103 Z" fill="currentColor" opacity="0.35"/>
<circle cx="5" cy="100" r="5" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.3"/>
<!-- Oil drops falling -->
<ellipse cx="70" cy="108" rx="2" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="85" cy="112" rx="1.5" ry="2.5" fill="currentColor" opacity="0.25"/>
<ellipse cx="62" cy="105" rx="1" ry="2" fill="currentColor" opacity="0.2"/>
<!-- Legs sticking out from under machine the signature view -->
<line x1="90" y1="125" x2="130" y2="175" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="95" y1="120" x2="145" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Big feet sticking up -->
<ellipse cx="133" cy="178" rx="10" ry="4" fill="currentColor" opacity="0.35" transform="rotate(-25 133 178)"/>
<ellipse cx="148" cy="168" rx="10" ry="4" fill="currentColor" opacity="0.35" transform="rotate(-25 148 168)"/>
<!-- Beak visible from side -->
<polygon points="42,118 35,116 42,122" fill="currentColor" opacity="0.3"/>
<!-- Eye visible -->
<circle cx="50" cy="113" r="1.5" fill="currentColor" opacity="0.4"/>
</svg>
<div class="container-content">
<header class="mb-12 text-center">
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-bold tracking-tight" style="color: hsl(var(--color-text))">
{{ content?.title }}
</h1>
<p class="mt-4 mx-auto max-w-2xl leading-relaxed" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
</header>
<div class="mx-auto max-w-3xl flex flex-col gap-6">
<div
v-for="pillar in content?.pillars"
:key="pillar.id"
class="pillar-card"
>
<div class="pillar-header">
<div class="pillar-icon">
<div :class="`i-lucide-${pillar.icon}`" class="h-5 w-5" />
</div>
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
{{ pillar.label }}
</h2>
<span v-if="pillar.gestation" class="gestation-badge">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
</div>
<p class="leading-relaxed whitespace-pre-line mt-3" style="color: hsl(var(--color-text-muted))">{{ pillar.text }}</p>
<!-- Project card -->
<div v-if="pillar.project" class="project-card mt-4">
<div class="project-icon">
<div class="i-lucide-rocket h-4 w-4" />
</div>
<div>
<span class="font-display font-semibold text-sm" style="color: hsl(var(--color-text))">{{ pillar.project.name }}</span>
<span class="text-sm ml-2" style="color: hsl(var(--color-text-muted))">{{ pillar.project.text }}</span>
</div>
</div>
<div v-if="pillar.to" class="mt-4">
<NuxtLink
:to="pillar.to"
class="inline-flex items-center gap-1 text-sm text-primary hover:text-primary/80 transition-colors"
>
En savoir plus
<div class="i-lucide-arrow-right h-3.5 w-3.5" />
</NuxtLink>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'default',
})
const { data: content } = await usePageContent('numerique')
useHead({
title: content.value?.meta?.title ?? 'Autonomie numérique',
})
</script>
<style scoped>
.page-title {
font-size: clamp(2rem, 5vw, 2.75rem);
}
.pillar-card {
padding: 1.5rem;
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
background: hsl(var(--color-surface));
transition: border-color 0.2s;
}
.pillar-card:hover {
border-color: hsl(var(--color-primary) / 0.2);
}
.pillar-header {
display: flex;
align-items: center;
gap: 0.75rem;
}
.pillar-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.5rem;
background: hsl(var(--color-primary) / 0.15);
color: hsl(var(--color-primary));
box-shadow: 0 0 12px hsl(var(--color-primary) / 0.12);
flex-shrink: 0;
}
.gestation-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
margin-left: auto;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
background: hsl(var(--color-accent) / 0.12);
color: hsl(var(--color-accent));
font-size: 0.7rem;
font-weight: 500;
font-family: var(--font-mono);
}
.project-card {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
background: hsl(var(--color-bg) / 0.5);
border: 1px solid hsl(var(--color-primary) / 0.1);
}
.project-icon {
display: flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
border-radius: 0.375rem;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
flex-shrink: 0;
}
.shadok-codeuse {
position: absolute;
left: 1%;
top: 4%;
width: clamp(70px, 10vw, 140px);
opacity: 0.24;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-codeuse 9s ease-in-out infinite;
}
.shadok-electricien {
position: absolute;
right: 1%;
top: 3%;
width: clamp(70px, 10vw, 130px);
opacity: 0.22;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-electricien 11s ease-in-out infinite;
}
.shadok-soudeuse {
position: absolute;
left: 3%;
top: 40%;
width: clamp(70px, 10vw, 130px);
opacity: 0.2;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-soudeuse 8s ease-in-out infinite;
}
.shadok-admin-reseau {
position: absolute;
right: 2%;
top: 35%;
width: clamp(70px, 10vw, 130px);
opacity: 0.22;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-admin-reseau 10s ease-in-out infinite;
}
.shadok-imprimeuse3d {
position: absolute;
left: 2%;
bottom: 8%;
width: clamp(70px, 10vw, 135px);
opacity: 0.2;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-imprimeuse3d 12s ease-in-out infinite;
}
.shadok-cryptographe {
position: absolute;
right: 1%;
bottom: 6%;
width: clamp(70px, 10vw, 130px);
opacity: 0.18;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-cryptographe 11s ease-in-out infinite;
}
.shadok-droneuse {
position: absolute;
left: 40%;
bottom: 2%;
width: clamp(70px, 10vw, 130px);
opacity: 0.2;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-droneuse 7s ease-in-out infinite;
}
.shadok-mecanicienne {
position: absolute;
right: 2%;
top: 60%;
width: clamp(70px, 10vw, 140px);
opacity: 0.22;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-mecanicienne 9s ease-in-out infinite;
}
@keyframes shadok-float-codeuse {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes shadok-float-electricien {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-10px) rotate(1deg); }
}
@keyframes shadok-float-soudeuse {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-7px) rotate(-0.8deg); }
}
@keyframes shadok-float-admin-reseau {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-9px); }
}
@keyframes shadok-float-imprimeuse3d {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-11px) rotate(0.5deg); }
}
@keyframes shadok-float-cryptographe {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-8px) rotate(-0.6deg); }
}
@keyframes shadok-float-droneuse {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
@keyframes shadok-float-mecanicienne {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-7px) rotate(1.2deg); }
}
@media (max-width: 768px) {
.shadok-codeuse { display: none; }
.shadok-electricien { display: none; }
.shadok-soudeuse { display: none; }
.shadok-admin-reseau { display: none; }
.shadok-imprimeuse3d { display: none; }
.shadok-cryptographe { display: none; }
.shadok-droneuse { display: none; }
.shadok-mecanicienne { display: none; }
}
</style>

View File

@@ -77,7 +77,7 @@ const palettes: Record<PaletteName, PaletteColors> = {
export const usePaletteStore = defineStore('palette', () => { export const usePaletteStore = defineStore('palette', () => {
const currentPalette = ref<PaletteName>( const currentPalette = ref<PaletteName>(
(import.meta.client && localStorage.getItem('palette') as PaletteName) || 'automne', (import.meta.client && localStorage.getItem('palette') as PaletteName) || 'ete',
) )
const colors = computed(() => palettes[currentPalette.value]) const colors = computed(() => palettes[currentPalette.value])

View File

@@ -12,13 +12,20 @@ export interface ChapterSongLink {
primary: boolean primary: boolean
} }
export interface ChapterPageLink {
chapterSlug: string
page: number
}
export interface BookConfig { export interface BookConfig {
title: string title: string
author: string author: string
description: string description: string
coverImage?: string coverImage?: string
pdfFile?: string
chapters: ChapterMeta[] chapters: ChapterMeta[]
songs: import('./song').Song[] songs: import('./song').Song[]
chapterSongs: ChapterSongLink[] chapterSongs: ChapterSongLink[]
chapterPages: ChapterPageLink[]
defaultPlaylistOrder: string[] defaultPlaylistOrder: string[]
} }

View File

@@ -11,7 +11,7 @@ Ce chapitre est une **invitation à lire la Théorie Relative de la Monnaie** (T
## Le flux monétaire ## Le flux monétaire
En monnaie libre, le flux monétaire est **corrélé à l'existence physique** de chaque être humain. Tant que tu vis, tu co-crées de la monnaie. Le Dividende Universel (DU) est cette part de création, identique pour chaque membre, chaque jour. En monnaie libre, le flux monétaire est **corrélé à l'existence physique** de chaque être humain. Tant que tu vis, tu crées de la monnaie. Le Dividende Universel (DU) est cette part de création, identique pour chaque membre, chaque jour.
## La symétrie spatio-temporelle ## La symétrie spatio-temporelle

View File

@@ -9,7 +9,7 @@ readingTime: "25 min"
### La June est une crypto ### La June est une crypto
Oui, la June (Ğ1) est une **cryptomonnaie** au sens technique : elle repose sur un réseau décentralisé, des algorithmes cryptographiques et une blockchain. Mais elle est fondamentalement différente des cryptos spéculatives par sa **co-création symétrique** (le DU) et l'absence de minage compétitif. Oui, la June (Ğ1) est une **cryptomonnaie** au sens technique : elle repose sur un réseau décentralisé, des algorithmes cryptographiques et une blockchain. Mais elle est fondamentalement différente des cryptos spéculatives par sa **création symétrique** (le DU) et l'absence de minage compétitif.
### DeX vs CeX ### DeX vs CeX

View File

@@ -6,7 +6,7 @@ FROM node:${NODE_VERSION} AS base
ARG PORT=3000 ARG PORT=3000
WORKDIR /src WORKDIR /app
# Build # Build
FROM base AS build FROM base AS build
@@ -19,6 +19,7 @@ RUN pnpm rebuild sharp
COPY . . COPY . .
RUN sh scripts/copy-pdfjs.sh
RUN pnpm run build RUN pnpm run build
# Production # Production
@@ -27,10 +28,11 @@ FROM base AS production
ENV PORT=${PORT} ENV PORT=${PORT}
ENV NODE_ENV=production ENV NODE_ENV=production
COPY --from=build /src/.output /src/.output RUN apt-get update && apt-get -fy install curl git && rm -rf /var/cache/apt/*
COPY --from=build /src/site /src/site
RUN apt-get update && apt-get -fy install curl && rm -rf /var/cache/apt/* COPY --from=build /app/.output /app/.output
COPY --from=build /app/site /app/site
COPY --from=build /app/content /app/content
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:${PORT}/api/health || exit 1 CMD curl -f http://localhost:${PORT}/api/health || exit 1

View File

@@ -0,0 +1,52 @@
## Umami analytics — déployer avec le compose principal
## Usage : docker compose -f docker-compose.yml -f docker-compose.umami.yml up -d
##
## Variables à définir dans .env :
## UMAMI_DB_PASSWORD — mot de passe PostgreSQL Umami (générer avec openssl rand -hex 32)
## UMAMI_APP_SECRET — secret applicatif (générer avec openssl rand -hex 32)
## UMAMI_DOMAIN — ex: stats.librodrome.org
name: librodrome
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
environment:
DATABASE_URL: postgresql://umami:${UMAMI_DB_PASSWORD}@umami-db:5432/umami
DATABASE_TYPE: postgresql
APP_SECRET: ${UMAMI_APP_SECRET}
depends_on:
umami-db:
condition: service_healthy
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.umami.rule=Host(`${UMAMI_DOMAIN:-stats.librodrome.org}`)"
- "traefik.http.routers.umami.entrypoints=websecure"
- "traefik.http.routers.umami.tls.certresolver=letsencrypt"
- "traefik.http.services.umami.loadbalancer.server.port=3000"
networks:
- default
- traefik
umami-db:
image: postgres:15-alpine
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: ${UMAMI_DB_PASSWORD}
volumes:
- umami-db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U umami"]
interval: 5s
timeout: 5s
retries: 10
restart: always
volumes:
umami-db-data:
networks:
traefik:
external: true

View File

@@ -10,6 +10,7 @@ services:
NUXT_PUBLIC_SITE_URL: ${NUXT_PUBLIC_SITE_URL:-https://librodrome.org} NUXT_PUBLIC_SITE_URL: ${NUXT_PUBLIC_SITE_URL:-https://librodrome.org}
NUXT_ADMIN_PASSWORD: ${NUXT_ADMIN_PASSWORD} NUXT_ADMIN_PASSWORD: ${NUXT_ADMIN_PASSWORD}
NUXT_ADMIN_SECRET: ${NUXT_ADMIN_SECRET} NUXT_ADMIN_SECRET: ${NUXT_ADMIN_SECRET}
ADMIN_GIT_SYNC: ${ADMIN_GIT_SYNC:-false}
ports: ports:
- 3000 - 3000
volumes: volumes:

View File

@@ -16,6 +16,28 @@ export default defineNuxtConfig({
'@nuxt/image', '@nuxt/image',
], ],
unocss: {
safelist: [
// Axis block icons (dynamic from YAML)
'i-lucide-monitor', 'i-lucide-coins', 'i-lucide-landmark',
'i-lucide-code-2', 'i-lucide-share-2', 'i-lucide-cloud',
'i-lucide-scale', 'i-lucide-gavel', 'i-lucide-users',
'i-lucide-droplets', 'i-lucide-calendar-heart',
// Action icons
'i-lucide-play', 'i-lucide-book-open', 'i-lucide-sparkles',
'i-lucide-heart-handshake', 'i-lucide-arrow-up-right',
'i-lucide-rocket', 'i-lucide-flask-conical', 'i-lucide-arrow-right',
// Decision page
'i-lucide-vote', 'i-lucide-scroll-text', 'i-lucide-git-branch',
// sweethomeCloud + territoire
'i-lucide-home', 'i-lucide-hard-drive', 'i-lucide-video',
'i-lucide-message-circle', 'i-lucide-bot', 'i-lucide-globe-2',
'i-lucide-map-pin', 'i-lucide-euro', 'i-lucide-package',
'i-lucide-settings', 'i-lucide-layout-grid', 'i-lucide-server',
'i-lucide-chevron-down', 'i-lucide-info',
],
},
app: { app: {
head: { head: {
htmlAttrs: { lang: 'fr' }, htmlAttrs: { lang: 'fr' },
@@ -40,8 +62,21 @@ export default defineNuxtConfig({
runtimeConfig: { runtimeConfig: {
adminPassword: '', adminPassword: '',
adminSecret: '', adminSecret: '',
umamiApiKey: '',
public: { public: {
siteUrl: 'https://librodrome.org', siteUrl: 'https://librodrome.org',
tenantId: 'librodrome',
umamiUrl: '',
umamiWebsiteId: '',
}, },
}, },
nitro: {
externals: {
traceInclude: [
'node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs',
],
},
},
}) })

View File

@@ -3,11 +3,11 @@
"type": "module", "type": "module",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "nuxt build", "build": "nuxt build && node scripts/fix-esm-imports.mjs",
"dev": "nuxt dev", "dev": "nuxt dev",
"generate": "nuxt generate", "generate": "nuxt generate",
"preview": "nuxt preview", "preview": "nuxt preview",
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare && (test -f scripts/copy-pdfjs.sh && sh scripts/copy-pdfjs.sh || true)"
}, },
"dependencies": { "dependencies": {
"@nuxt/content": "^3.11.2", "@nuxt/content": "^3.11.2",
@@ -17,6 +17,7 @@
"@vueuse/nuxt": "^14.2.1", "@vueuse/nuxt": "^14.2.1",
"better-sqlite3": "^12.6.2", "better-sqlite3": "^12.6.2",
"nuxt": "^4.3.1", "nuxt": "^4.3.1",
"pdfjs-dist": "^5.4.624",
"vue": "^3.5.28", "vue": "^3.5.28",
"vue-router": "^4.6.4", "vue-router": "^4.6.4",
"yaml": "^2.8.2" "yaml": "^2.8.2"

141
pnpm-lock.yaml generated
View File

@@ -29,6 +29,9 @@ importers:
nuxt: nuxt:
specifier: ^4.3.1 specifier: ^4.3.1
version: 4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2) version: 4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2)
pdfjs-dist:
specifier: ^5.4.624
version: 5.4.624
vue: vue:
specifier: ^3.5.28 specifier: ^3.5.28
version: 3.5.28(typescript@5.9.3) version: 3.5.28(typescript@5.9.3)
@@ -598,6 +601,81 @@ packages:
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
'@napi-rs/canvas-android-arm64@0.1.95':
resolution: {integrity: sha512-SqTh0wsYbetckMXEvHqmR7HKRJujVf1sYv1xdlhkifg6TlCSysz1opa49LlS3+xWuazcQcfRfmhA07HxxxGsAA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@napi-rs/canvas-darwin-arm64@0.1.95':
resolution: {integrity: sha512-F7jT0Syu+B9DGBUBcMk3qCRIxAWiDXmvEjamwbYfbZl7asI1pmXZUnCOoIu49Wt0RNooToYfRDxU9omD6t5Xuw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@napi-rs/canvas-darwin-x64@0.1.95':
resolution: {integrity: sha512-54eb2Ho15RDjYGXO/harjRznBrAvu+j5nQ85Z4Qd6Qg3slR8/Ja+Yvvy9G4yo7rdX6NR9GPkZeSTf2UcKXwaXw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.95':
resolution: {integrity: sha512-hYaLCSLx5bmbnclzQc3ado3PgZ66blJWzjXp0wJmdwpr/kH+Mwhj6vuytJIomgksyJoCdIqIa4N6aiqBGJtJ5Q==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@napi-rs/canvas-linux-arm64-gnu@0.1.95':
resolution: {integrity: sha512-J7VipONahKsmScPZsipHVQBqpbZx4favaD8/enWzzlGcjiwycOoymL7f4tNeqdjK0su19bDOUt6mjp9gsPWYlw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-arm64-musl@0.1.95':
resolution: {integrity: sha512-PXy0UT1J/8MPG8UAkWp6Fd51ZtIZINFzIjGH909JjQrtCuJf3X6nanHYdz1A+Wq9o4aoPAw1YEUpFS1lelsVlg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.95':
resolution: {integrity: sha512-2IzCkW2RHRdcgF9W5/plHvYFpc6uikyjMb5SxjqmNxfyDFz9/HB89yhi8YQo0SNqrGRI7yBVDec7Pt+uMyRWsg==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-gnu@0.1.95':
resolution: {integrity: sha512-OV/ol/OtcUr4qDhQg8G7SdViZX8XyQeKpPsVv/j3+7U178FGoU4M+yIocdVo1ih/A8GQ63+LjF4jDoEjaVU8Pw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-musl@0.1.95':
resolution: {integrity: sha512-Z5KzqBK/XzPz5+SFHKz7yKqClEQ8pOiEDdgk5SlphBLVNb8JFIJkxhtJKSvnJyHh2rjVgiFmvtJzMF0gNwwKyQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-win32-arm64-msvc@0.1.95':
resolution: {integrity: sha512-aj0YbRpe8qVJ4OzMsK7NfNQePgcf9zkGFzNZ9mSuaxXzhpLHmlF2GivNdCdNOg8WzA/NxV6IU4c5XkXadUMLeA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@napi-rs/canvas-win32-x64-msvc@0.1.95':
resolution: {integrity: sha512-GA8leTTCfdjuHi8reICTIxU0081PhXvl3lzIniLUjeLACx9GubUiyzkwFb+oyeKLS5IAGZFLKnzAf4wm2epRlA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@napi-rs/canvas@0.1.95':
resolution: {integrity: sha512-lkg23ge+rgyhgUwXmlbkPEhuhHq/hUi/gXKH+4I7vO+lJrbNfEYcQdJLIGjKyXLQzgFiiyDAwh5vAe/tITAE+w==}
engines: {node: '>= 10'}
'@napi-rs/wasm-runtime@1.1.1': '@napi-rs/wasm-runtime@1.1.1':
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
@@ -3336,6 +3414,9 @@ packages:
node-mock-http@1.0.4: node-mock-http@1.0.4:
resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==}
node-readable-to-web-readable-stream@0.4.2:
resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==}
node-releases@2.0.27: node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
@@ -3496,6 +3577,10 @@ packages:
pathe@2.0.3: pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
pdfjs-dist@5.4.624:
resolution: {integrity: sha512-sm6TxKTtWv1Oh6n3C6J6a8odejb5uO4A4zo/2dgkHuC0iu8ZMAXOezEODkVaoVp8nX1Xzr+0WxFJJmUr45hQzg==}
engines: {node: '>=20.16.0 || >=22.3.0'}
perfect-debounce@1.0.0: perfect-debounce@1.0.0:
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
@@ -5214,6 +5299,54 @@ snapshots:
- encoding - encoding
- supports-color - supports-color
'@napi-rs/canvas-android-arm64@0.1.95':
optional: true
'@napi-rs/canvas-darwin-arm64@0.1.95':
optional: true
'@napi-rs/canvas-darwin-x64@0.1.95':
optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.95':
optional: true
'@napi-rs/canvas-linux-arm64-gnu@0.1.95':
optional: true
'@napi-rs/canvas-linux-arm64-musl@0.1.95':
optional: true
'@napi-rs/canvas-linux-riscv64-gnu@0.1.95':
optional: true
'@napi-rs/canvas-linux-x64-gnu@0.1.95':
optional: true
'@napi-rs/canvas-linux-x64-musl@0.1.95':
optional: true
'@napi-rs/canvas-win32-arm64-msvc@0.1.95':
optional: true
'@napi-rs/canvas-win32-x64-msvc@0.1.95':
optional: true
'@napi-rs/canvas@0.1.95':
optionalDependencies:
'@napi-rs/canvas-android-arm64': 0.1.95
'@napi-rs/canvas-darwin-arm64': 0.1.95
'@napi-rs/canvas-darwin-x64': 0.1.95
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.95
'@napi-rs/canvas-linux-arm64-gnu': 0.1.95
'@napi-rs/canvas-linux-arm64-musl': 0.1.95
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.95
'@napi-rs/canvas-linux-x64-gnu': 0.1.95
'@napi-rs/canvas-linux-x64-musl': 0.1.95
'@napi-rs/canvas-win32-arm64-msvc': 0.1.95
'@napi-rs/canvas-win32-x64-msvc': 0.1.95
optional: true
'@napi-rs/wasm-runtime@1.1.1': '@napi-rs/wasm-runtime@1.1.1':
dependencies: dependencies:
'@emnapi/core': 1.8.1 '@emnapi/core': 1.8.1
@@ -8512,6 +8645,9 @@ snapshots:
node-mock-http@1.0.4: {} node-mock-http@1.0.4: {}
node-readable-to-web-readable-stream@0.4.2:
optional: true
node-releases@2.0.27: {} node-releases@2.0.27: {}
nopt@8.1.0: nopt@8.1.0:
@@ -8855,6 +8991,11 @@ snapshots:
pathe@2.0.3: {} pathe@2.0.3: {}
pdfjs-dist@5.4.624:
optionalDependencies:
'@napi-rs/canvas': 0.1.95
node-readable-to-web-readable-stream: 0.4.2
perfect-debounce@1.0.0: {} perfect-debounce@1.0.0: {}
perfect-debounce@2.1.0: {} perfect-debounce@2.1.0: {}

607
public/pdfjs/viewer.html Normal file
View File

@@ -0,0 +1,607 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PDF Viewer</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a1a;
color: #e0e0e0;
}
.viewer-layout {
display: flex;
height: 100%;
}
/* Outline panel */
.outline-panel {
width: 260px;
min-width: 260px;
background: #111;
border-right: 1px solid #2a2a2a;
display: flex;
flex-direction: column;
overflow: hidden;
transition: width 0.25s, min-width 0.25s;
}
.outline-panel.collapsed {
width: 0;
min-width: 0;
border-right: none;
}
.outline-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.6rem 0.75rem;
border-bottom: 1px solid #2a2a2a;
flex-shrink: 0;
}
.outline-title {
font-size: 0.75rem;
font-weight: 600;
color: #888;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.outline-actions { display: flex; gap: 0.25rem; }
.outline-btn {
background: none;
border: 1px solid #2a2a2a;
border-radius: 4px;
color: #777;
cursor: pointer;
padding: 0.2rem 0.4rem;
font-size: 0.65rem;
transition: all 0.15s;
}
.outline-btn:hover { border-color: #555; color: #ccc; }
.outline-tree {
flex: 1;
overflow-y: auto;
padding: 0.4rem;
}
.outline-tree::-webkit-scrollbar { width: 4px; }
.outline-tree::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; }
.outline-node { user-select: none; }
.outline-item {
display: flex;
align-items: flex-start;
gap: 0.2rem;
padding: 0.3rem 0.4rem;
border-radius: 4px;
cursor: pointer;
color: #aaa;
font-size: 0.75rem;
line-height: 1.3;
transition: background 0.1s;
}
.outline-item:hover { background: #1e1e1e; color: #fff; }
.outline-item.active { background: #1a1207; color: #e8a040; }
.outline-item .toggle {
flex-shrink: 0;
width: 14px; height: 14px;
display: flex; align-items: center; justify-content: center;
font-size: 0.6rem; color: #555; cursor: pointer;
transition: transform 0.15s; margin-top: 1px;
}
.outline-item .toggle.open { transform: rotate(90deg); }
.outline-item .toggle.empty { visibility: hidden; }
.outline-item .label { flex: 1; min-width: 0; }
.outline-children { padding-left: 0.85rem; overflow: hidden; }
.outline-children.hidden { display: none; }
.outline-empty {
padding: 1.5rem 1rem;
text-align: center;
color: #444;
font-size: 0.8rem;
}
/* Main area */
.main-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Book spread */
.book-spread {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
padding: 1rem;
background: #2a2a2a;
overflow: hidden;
position: relative;
}
.book-spread canvas {
box-shadow: 0 4px 24px rgba(0,0,0,0.5);
background: white;
display: block;
}
.page-slot {
display: flex;
align-items: center;
justify-content: center;
max-height: 100%;
position: relative;
}
.page-slot.empty {
visibility: hidden;
}
/* Page turn transition */
.page-slot canvas {
transition: opacity 1s ease-out, transform 1s ease-out;
}
.page-slot canvas.entering {
opacity: 0;
transform: translateX(var(--enter-dir, 20px));
transition: none;
}
.page-slot canvas.leaving {
opacity: 0;
transform: translateX(var(--leave-dir, -20px));
position: absolute;
transition: opacity 1.2s ease-in, transform 1.2s ease-in;
}
/* Navigation bar */
.nav-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 0.6rem 1rem;
background: #141414;
border-top: 1px solid #2a2a2a;
flex-shrink: 0;
}
.nav-btn {
background: #1e1e1e;
border: 1px solid #333;
border-radius: 6px;
color: #aaa;
cursor: pointer;
padding: 0.4rem 0.8rem;
font-size: 0.85rem;
transition: all 0.15s;
display: flex;
align-items: center;
gap: 0.3rem;
}
.nav-btn:hover:not(:disabled) { background: #2a2a2a; color: #fff; border-color: #555; }
.nav-btn:disabled { opacity: 0.25; cursor: default; }
.nav-info {
font-size: 0.75rem;
color: #777;
font-variant-numeric: tabular-nums;
min-width: 6rem;
text-align: center;
}
.sidebar-btn {
position: absolute;
left: 0; top: 0; bottom: 0;
width: 2rem;
background: transparent;
border: none;
color: #555;
cursor: pointer;
font-size: 1rem;
transition: all 0.15s;
z-index: 5;
}
.sidebar-btn:hover { background: rgba(255,255,255,0.03); color: #aaa; }
.sidebar-btn.hidden { display: none; }
/* Loading */
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #555;
font-size: 0.9rem;
}
@media (max-width: 768px) {
.outline-panel { width: 200px; min-width: 200px; }
.book-spread { padding: 0.5rem; }
}
@media (max-width: 480px) {
.outline-panel { position: absolute; z-index: 20; height: 100%; }
.outline-panel.collapsed { display: none; }
}
</style>
</head>
<body>
<div class="viewer-layout">
<div class="outline-panel" id="outlinePanel">
<div class="outline-header">
<span class="outline-title">Sommaire</span>
<div class="outline-actions">
<button class="outline-btn" onclick="expandAll()">Déplier</button>
<button class="outline-btn" onclick="collapseAll()">Replier</button>
<button class="outline-btn" onclick="toggleSidebar()">&#10005;</button>
</div>
</div>
<div class="outline-tree" id="outlineTree">
<div class="loading">Chargement…</div>
</div>
</div>
<div class="main-area">
<div class="book-spread" id="bookSpread">
<button class="sidebar-btn hidden" id="sidebarBtn" onclick="toggleSidebar()">&#9776;</button>
<div class="loading" id="loadingMsg">Chargement du PDF…</div>
<div class="page-slot" id="slotLeft"></div>
<div class="page-slot" id="slotRight"></div>
</div>
<div class="nav-bar">
<button class="nav-btn" id="prevBtn" onclick="prevSpread()" disabled>&#9664; Précédent</button>
<span class="nav-info" id="pageInfo"></span>
<button class="nav-btn" id="nextBtn" onclick="nextSpread()" disabled>Suivant &#9654;</button>
</div>
</div>
</div>
<script type="module">
import * as pdfjsLib from '/pdfjs/pdf.min.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdfjs/pdf.worker.min.mjs';
const params = new URLSearchParams(location.search);
const hash = location.hash.slice(1);
const hashParams = new URLSearchParams(hash);
const fileUrl = params.get('file') || '/pdf/une-economie-du-don.pdf';
const targetPage = parseInt(hashParams.get('page') || params.get('page') || '1', 10);
let pdfDoc = null;
let currentSpread = 0; // index into spreads[]
let spreads = []; // [[1], [2,3], [4,5], ...] — page 1 alone (cover), then pairs
let pageCanvasCache = new Map();
let outlinePageMap = []; // [{item, pageNum}] for highlighting
let isAnimating = false;
function buildSpreads(numPages) {
spreads = [];
// Page 1 = couverture seule
spreads.push([1]);
// Ensuite par paires : 2-3, 4-5, etc.
for (let i = 2; i <= numPages; i += 2) {
if (i + 1 <= numPages) {
spreads.push([i, i + 1]);
} else {
spreads.push([i]);
}
}
}
function getSpreadForPage(pageNum) {
return spreads.findIndex(s => s.includes(pageNum));
}
async function renderPageCanvas(pageNum) {
if (pageCanvasCache.has(pageNum)) return pageCanvasCache.get(pageNum);
const page = await pdfDoc.getPage(pageNum);
const spread = document.getElementById('bookSpread');
const availH = spread.clientHeight - 32;
const availW = (spread.clientWidth - 40) / 2;
const rawViewport = page.getViewport({ scale: 1 });
const scaleH = availH / rawViewport.height;
const scaleW = availW / rawViewport.width;
const scale = Math.min(scaleH, scaleW, 2.5);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
canvas.dataset.page = pageNum;
const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport }).promise;
pageCanvasCache.set(pageNum, canvas);
return canvas;
}
async function showSpread(index, animate = true) {
if (index < 0 || index >= spreads.length) return;
if (isAnimating) return;
const prevIndex = currentSpread;
const direction = index > prevIndex ? 1 : -1; // 1 = forward, -1 = back
currentSpread = index;
const pages = spreads[index];
const slotLeft = document.getElementById('slotLeft');
const slotRight = document.getElementById('slotRight');
// Collect old canvases for fade-out
const oldCanvases = [...slotLeft.querySelectorAll('canvas'), ...slotRight.querySelectorAll('canvas')];
const shouldAnimate = animate && oldCanvases.length > 0;
if (shouldAnimate) {
isAnimating = true;
// Fade out old canvases with directional slide
oldCanvases.forEach(c => {
c.style.setProperty('--leave-dir', `${-direction * 20}px`);
c.classList.add('leaving');
});
// Wait for fade-out to mostly complete
await new Promise(r => setTimeout(r, 500));
}
slotLeft.innerHTML = '';
slotRight.innerHTML = '';
slotLeft.className = 'page-slot';
slotRight.className = 'page-slot';
if (pages.length === 1) {
const canvas = await renderPageCanvas(pages[0]);
if (shouldAnimate) {
canvas.style.setProperty('--enter-dir', `${direction * 20}px`);
canvas.classList.add('entering');
}
slotLeft.appendChild(canvas);
slotRight.className = 'page-slot empty';
if (shouldAnimate) {
// Double rAF ensures the browser has painted the initial state before transitioning
requestAnimationFrame(() => requestAnimationFrame(() => canvas.classList.remove('entering')));
}
} else {
const [left, right] = await Promise.all([
renderPageCanvas(pages[0]),
renderPageCanvas(pages[1]),
]);
if (shouldAnimate) {
left.style.setProperty('--enter-dir', `${direction * 20}px`);
right.style.setProperty('--enter-dir', `${direction * 20}px`);
left.classList.add('entering');
right.classList.add('entering');
}
slotLeft.appendChild(left);
slotRight.appendChild(right);
if (shouldAnimate) {
requestAnimationFrame(() => requestAnimationFrame(() => {
left.classList.remove('entering');
right.classList.remove('entering');
}));
}
}
if (shouldAnimate) setTimeout(() => { isAnimating = false; }, 1100);
else isAnimating = false;
updateNav();
highlightOutline();
}
function updateNav() {
const pages = spreads[currentSpread];
const info = document.getElementById('pageInfo');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
if (pages.length === 1) {
info.textContent = `Page ${pages[0]} / ${pdfDoc.numPages}`;
} else {
info.textContent = `Pages ${pages[0]}${pages[1]} / ${pdfDoc.numPages}`;
}
prevBtn.disabled = currentSpread <= 0;
nextBtn.disabled = currentSpread >= spreads.length - 1;
}
function prevSpread() { showSpread(currentSpread - 1); }
function nextSpread() { showSpread(currentSpread + 1); }
function goToPage(pageNum) {
const idx = getSpreadForPage(pageNum);
if (idx >= 0) showSpread(idx);
}
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault();
prevSpread();
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ') {
e.preventDefault();
nextSpread();
} else if (e.key === 'Home') {
e.preventDefault();
showSpread(0);
} else if (e.key === 'End') {
e.preventDefault();
showSpread(spreads.length - 1);
}
});
// Resize handling
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
pageCanvasCache.clear();
showSpread(currentSpread, false);
}, 200);
});
// Outline
async function renderOutline() {
const outlineTree = document.getElementById('outlineTree');
const outline = await pdfDoc.getOutline();
if (!outline || outline.length === 0) {
outlineTree.innerHTML = '<div class="outline-empty">Aucun signet dans ce PDF</div>';
return;
}
outlineTree.innerHTML = '';
outlinePageMap = [];
const tree = await buildOutlineTree(outline);
outlineTree.appendChild(tree);
}
async function buildOutlineTree(items) {
const frag = document.createDocumentFragment();
for (const item of items) {
const node = document.createElement('div');
node.className = 'outline-node';
const row = document.createElement('div');
row.className = 'outline-item';
// Resolve page number for this item
let pageNum = null;
try {
let dest = item.dest;
if (typeof dest === 'string') dest = await pdfDoc.getDestination(dest);
if (dest) {
const ref = dest[0];
pageNum = (await pdfDoc.getPageIndex(ref)) + 1;
}
} catch {}
const toggle = document.createElement('span');
toggle.className = 'toggle' + (item.items && item.items.length > 0 ? ' open' : ' empty');
toggle.textContent = '▶';
if (item.items && item.items.length > 0) {
toggle.addEventListener('click', (e) => {
e.stopPropagation();
const children = node.querySelector('.outline-children');
if (children) {
const isHidden = children.classList.toggle('hidden');
toggle.classList.toggle('open', !isHidden);
}
});
}
row.appendChild(toggle);
const label = document.createElement('span');
label.className = 'label';
label.textContent = item.title;
row.appendChild(label);
if (pageNum !== null) {
const pg = pageNum;
row.addEventListener('click', () => goToPage(pg));
outlinePageMap.push({ row, pageNum: pg });
}
node.appendChild(row);
if (item.items && item.items.length > 0) {
const children = document.createElement('div');
children.className = 'outline-children';
children.appendChild(await buildOutlineTree(item.items));
node.appendChild(children);
}
frag.appendChild(node);
}
return frag;
}
function highlightOutline() {
const pages = spreads[currentSpread];
for (const { row, pageNum } of outlinePageMap) {
row.classList.toggle('active', pages.includes(pageNum));
}
}
// Load
async function init() {
const loadingTask = pdfjsLib.getDocument(fileUrl);
pdfDoc = await loadingTask.promise;
buildSpreads(pdfDoc.numPages);
await renderOutline();
document.getElementById('loadingMsg').remove();
const startSpread = getSpreadForPage(targetPage);
await showSpread(startSpread >= 0 ? startSpread : 0, false);
}
init().catch(err => {
document.getElementById('loadingMsg').textContent = `Erreur : ${err.message}`;
document.getElementById('loadingMsg').style.color = '#c44';
console.error(err);
});
// Expose globals
window.prevSpread = prevSpread;
window.nextSpread = nextSpread;
window.expandAll = function() {
const tree = document.getElementById('outlineTree');
tree.querySelectorAll('.outline-children.hidden').forEach(el => el.classList.remove('hidden'));
tree.querySelectorAll('.toggle:not(.empty)').forEach(el => el.classList.add('open'));
};
window.collapseAll = function() {
const tree = document.getElementById('outlineTree');
tree.querySelectorAll('.outline-children').forEach(el => el.classList.add('hidden'));
tree.querySelectorAll('.toggle:not(.empty)').forEach(el => el.classList.remove('open'));
};
window.toggleSidebar = function() {
const panel = document.getElementById('outlinePanel');
const btn = document.getElementById('sidebarBtn');
panel.classList.toggle('collapsed');
btn.classList.toggle('hidden', !panel.classList.contains('collapsed'));
// Recalc on sidebar toggle
setTimeout(() => {
pageCanvasCache.clear();
showSpread(currentSpread, false);
}, 300);
};
</script>
</body>
</html>

12
scripts/copy-pdfjs.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# Copie les fichiers PDF.js nécessaires depuis node_modules vers public/pdfjs/
set -e
DEST="public/pdfjs"
SRC="node_modules/pdfjs-dist/build"
mkdir -p "$DEST"
cp "$SRC/pdf.min.mjs" "$DEST/pdf.min.mjs"
cp "$SRC/pdf.worker.min.mjs" "$DEST/pdf.worker.min.mjs"
echo "[copy-pdfjs] OK"

View File

@@ -0,0 +1,35 @@
/**
* Post-build fix: Pinia CJS (pinia.prod.cjs) fait require('vue').
* Rollup convertit ça en `import require$$0 from 'vue'` (default import).
* Vue 3 n'a pas de default export en ESM → crash Node 22+.
*
* Ce script remplace le default import par un namespace import valide.
*/
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
const file = '.output/server/chunks/build/server.mjs'
if (!existsSync(file)) {
console.log('[fix-esm] server.mjs not found, skipping')
process.exit(0)
}
let code = readFileSync(file, 'utf-8')
// Pattern: import require$$0, { defineComponent, inject, ... } from 'vue'
// → import * as require$$0 from 'vue'; import { defineComponent, ... } from 'vue'
const re = /import\s+(require\$\$\d+)\s*,\s*\{([^}]+)\}\s*from\s*'vue'/g
let patched = false
code = code.replace(re, (match, varName, namedStr) => {
patched = true
const names = namedStr.split(',').map(n => n.trim()).filter(Boolean)
return `import * as ${varName} from 'vue'; import { ${names.join(', ')} } from 'vue'`
})
if (patched) {
writeFileSync(file, code)
console.log('[fix-esm] Patched default import from vue in server.mjs')
} else {
console.log('[fix-esm] No default import found, nothing to patch')
}

View File

@@ -11,6 +11,7 @@ export default defineEventHandler(async (event) => {
const filePath = join(process.cwd(), 'content', 'book', `${slug}.md`) const filePath = join(process.cwd(), 'content', 'book', `${slug}.md`)
await unlink(filePath) await unlink(filePath)
gitSyncContent(`Suppression chapitre ${slug}`, [`content/book/${slug}.md`])
return { ok: true } return { ok: true }
}) })

View File

@@ -18,6 +18,7 @@ export default defineEventHandler(async (event) => {
const content = `---\n${body.frontmatter.trim()}\n---\n${body.body}` const content = `---\n${body.frontmatter.trim()}\n---\n${body.body}`
await writeFile(filePath, content, 'utf-8') await writeFile(filePath, content, 'utf-8')
gitSyncContent(`Mise à jour chapitre ${slug}`, [`content/book/${slug}.md`])
return { ok: true } return { ok: true }
}) })

View File

@@ -24,6 +24,7 @@ readingTime: "5 min"
` `
await writeFile(filePath, content, 'utf-8') await writeFile(filePath, content, 'utf-8')
gitSyncContent(`Nouveau chapitre ${body.slug}`, [`content/book/${body.slug}.md`])
return { ok: true, slug: body.slug } return { ok: true, slug: body.slug }
}) })

View File

@@ -25,5 +25,7 @@ export default defineEventHandler(async (event) => {
}), }),
) )
gitSyncContent('Réorganisation chapitres', ['content/book/'])
return { ok: true } return { ok: true }
}) })

View File

@@ -1,5 +1,6 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const body = await readBody(event) const body = await readBody(event)
await writeYaml('bookplayer.config.yml', body) await writeYaml('bookplayer.config.yml', body)
gitSyncContent('Mise à jour config livre/player', ['site/bookplayer.config.yml'])
return { ok: true } return { ok: true }
}) })

View File

@@ -0,0 +1,39 @@
import { readdir, stat } from 'node:fs/promises'
import { join } from 'node:path'
interface PageEntry {
path: string
label: string
section?: string
}
async function listYamlFiles(dir: string, prefix = ''): Promise<PageEntry[]> {
const entries: PageEntry[] = []
const items = await readdir(dir)
for (const item of items) {
const fullPath = join(dir, item)
const s = await stat(fullPath)
if (s.isDirectory()) {
const subEntries = await listYamlFiles(fullPath, prefix ? `${prefix}/${item}` : item)
entries.push(...subEntries)
}
else if (item.endsWith('.yml')) {
const name = item.replace('.yml', '')
const path = prefix ? `${prefix}/${name}` : name
entries.push({
path,
label: name,
section: prefix || undefined,
})
}
}
return entries
}
export default defineEventHandler(async () => {
const pagesDir = join(process.cwd(), 'site', 'pages')
return await listYamlFiles(pagesDir)
})

View File

@@ -0,0 +1,21 @@
import { mkdir } from 'node:fs/promises'
import { join, dirname } from 'node:path'
export default defineEventHandler(async (event) => {
const path = getRouterParam(event, 'path')
if (!path || !/^[a-z0-9-/]+$/.test(path)) {
throw createError({ statusCode: 400, statusMessage: 'Invalid page path' })
}
const body = await readBody(event)
const relativePath = `pages/${path}.yml`
// Ensure subdirectory exists
const fullPath = join(process.cwd(), 'site', relativePath)
await mkdir(dirname(fullPath), { recursive: true })
await writeYaml(relativePath, body)
gitSyncContent(`Mise à jour page ${path}`, [`site/${relativePath}`])
return { ok: true }
})

View File

@@ -1,11 +0,0 @@
export default defineEventHandler(async (event) => {
const name = getRouterParam(event, 'name')
if (!name || !/^[a-z0-9-]+$/.test(name)) {
throw createError({ statusCode: 400, statusMessage: 'Invalid page name' })
}
const body = await readBody(event)
await writeYaml(`pages/${name}.yml`, body)
return { ok: true }
})

View File

@@ -1,5 +1,6 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const body = await readBody(event) const body = await readBody(event)
await writeYaml('site.yml', body) await writeYaml('site.yml', body)
gitSyncContent('Mise à jour config site', ['site/site.yml'])
return { ok: true } return { ok: true }
}) })

View File

@@ -23,6 +23,7 @@ export default defineEventHandler(async (event) => {
try { try {
await unlink(filePath) await unlink(filePath)
gitSyncContent(`Suppression média ${path}`, [`public/${path}`])
return { ok: true } return { ok: true }
} }
catch { catch {

View File

@@ -33,5 +33,9 @@ export default defineEventHandler(async (event) => {
uploaded.push(`/${subdir}/${safeName}`) uploaded.push(`/${subdir}/${safeName}`)
} }
if (uploaded.length > 0) {
gitSyncContent(`Upload média : ${uploaded.join(', ')}`, uploaded.map(f => `public${f}`))
}
return { ok: true, files: uploaded } return { ok: true, files: uploaded }
}) })

View File

@@ -14,6 +14,7 @@ export default defineEventHandler(async (event) => {
data.messages.splice(index, 1) data.messages.splice(index, 1)
await writeYaml('messages.yml', data) await writeYaml('messages.yml', data)
gitSyncContent(`Suppression message #${id}`, ['site/messages.yml'])
return { ok: true } return { ok: true }
}) })

View File

@@ -18,6 +18,7 @@ export default defineEventHandler(async (event) => {
if (body.author !== undefined) message.author = body.author if (body.author !== undefined) message.author = body.author
await writeYaml('messages.yml', data) await writeYaml('messages.yml', data)
gitSyncContent(`Mise à jour message #${id}`, ['site/messages.yml'])
return { ok: true } return { ok: true }
}) })

View File

@@ -0,0 +1,86 @@
import { join } from 'node:path'
import { readFileSync, existsSync } from 'node:fs'
// Polyfills nécessaires pour pdfjs-dist en Node.js pur (pas de DOM)
// On n'a besoin que du parsing, pas du rendu
if (typeof globalThis.DOMMatrix === 'undefined') {
// @ts-expect-error polyfill minimal pour pdfjs
globalThis.DOMMatrix = class DOMMatrix {
constructor() { return Object.assign(this, { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }) }
isIdentity = true
translate() { return new DOMMatrix() }
scale() { return new DOMMatrix() }
inverse() { return new DOMMatrix() }
multiply() { return new DOMMatrix() }
}
}
if (typeof globalThis.Path2D === 'undefined') {
// @ts-expect-error polyfill stub
globalThis.Path2D = class Path2D { constructor() {} }
}
if (typeof globalThis.ImageData === 'undefined') {
// @ts-expect-error polyfill stub
globalThis.ImageData = class ImageData { constructor(w: number, h: number) { this.width = w; this.height = h; this.data = new Uint8ClampedArray(w * h * 4) } }
}
export default defineEventHandler(async () => {
const config = await readYaml('bookplayer.config.yml')
const pdfFile = config?.book?.pdfFile || '/pdf/une-economie-du-don.pdf'
// Résolution du chemin PDF : dev (public/) et prod (.output/public/)
const cwd = process.cwd()
const candidates = [
join(cwd, 'public', pdfFile),
join(cwd, '.output', 'public', pdfFile),
]
const pdfPath = candidates.find(p => existsSync(p))
if (!pdfPath) {
console.warn('[pdf-outline] PDF non trouvé. cwd:', cwd, 'candidats:', candidates)
return []
}
let data: Uint8Array
try {
data = new Uint8Array(readFileSync(pdfPath))
} catch (err) {
console.warn('[pdf-outline] Erreur lecture PDF:', err)
return []
}
const pdfjsLib = await import('pdfjs-dist/legacy/build/pdf.mjs')
let doc
try {
doc = await pdfjsLib.getDocument({ data, useSystemFonts: true }).promise
} catch (err) {
console.warn('[pdf-outline] Erreur getDocument:', err)
return []
}
const outline = await doc.getOutline()
if (!outline || outline.length === 0) {
doc.destroy()
return []
}
const entries: Array<{ title: string; page: number; level: number }> = []
async function extract(items: any[], level: number) {
for (const item of items) {
let page: number | null = null
try {
let dest = item.dest
if (typeof dest === 'string') dest = await doc.getDestination(dest)
if (dest) page = (await doc.getPageIndex(dest[0])) + 1
} catch {}
if (page !== null) entries.push({ title: item.title, page, level })
if (item.items?.length) await extract(item.items, level + 1)
}
}
await extract(outline, 0)
doc.destroy()
return entries
})

View File

@@ -1,14 +1,14 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const name = getRouterParam(event, 'name') const path = getRouterParam(event, 'path')
if (!name || !/^[a-z0-9-]+$/.test(name)) { if (!path || !/^[a-z0-9-/]+$/.test(path)) {
throw createError({ statusCode: 400, statusMessage: 'Invalid page name' }) throw createError({ statusCode: 400, statusMessage: 'Invalid page path' })
} }
try { try {
return await readYaml(`pages/${name}.yml`) return await readYaml(`pages/${path}.yml`)
} }
catch { catch {
throw createError({ statusCode: 404, statusMessage: `Page "${name}" not found` }) throw createError({ statusCode: 404, statusMessage: `Page "${path}" not found` })
} }
}) })

View File

@@ -1,8 +1,8 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const body = await readBody<{ author: string; email?: string; text: string }>(event) const body = await readBody<{ author: string; email?: string; text: string }>(event)
if (!body.author?.trim() || !body.text?.trim()) { if (!body.text?.trim()) {
throw createError({ statusCode: 400, statusMessage: 'Nom et message requis' }) throw createError({ statusCode: 400, statusMessage: 'Message requis' })
} }
const data = await readYaml<{ messages: any[] }>('messages.yml') const data = await readYaml<{ messages: any[] }>('messages.yml')

View File

@@ -0,0 +1,43 @@
/**
* GET /api/stats
* Public stats endpoint — proxies Umami for cross-instance federation / observatoires.
* Each librodrome instance exposes its own metrics here.
* Observatoires call this endpoint on each instance and aggregate.
*
* Env vars required (private, server-side):
* NUXT_UMAMI_API_KEY — Umami API key (read-only)
* NUXT_UMAMI_WEBSITE_ID — Umami website ID (internal, server-side)
* NUXT_PUBLIC_UMAMI_URL — Umami base URL
* NUXT_PUBLIC_TENANT_ID — e.g. "librodrome" or "librodrome-bordeaux"
*/
export default defineEventHandler(async () => {
const config = useRuntimeConfig()
const { umamiApiKey } = config
const { umamiUrl, umamiWebsiteId, tenantId } = config.public
if (!umamiApiKey || !umamiUrl || !umamiWebsiteId) {
return { tenant: tenantId, configured: false }
}
const endAt = Date.now()
const startAt = endAt - 30 * 24 * 60 * 60 * 1000 // 30 days
const [stats, pageviews] = await Promise.all([
$fetch<Record<string, unknown>>(`${umamiUrl}/api/websites/${umamiWebsiteId}/stats`, {
headers: { 'x-umami-api-key': umamiApiKey },
query: { startAt, endAt },
}).catch(() => null),
$fetch<Record<string, unknown>>(`${umamiUrl}/api/websites/${umamiWebsiteId}/pageviews`, {
headers: { 'x-umami-api-key': umamiApiKey },
query: { startAt, endAt, unit: 'day', timezone: 'Europe/Paris' },
}).catch(() => null),
])
return {
tenant: tenantId,
configured: true,
period: '30d',
stats,
pageviews,
}
})

View File

@@ -1,13 +1,45 @@
export default defineEventHandler((event) => { export default defineEventHandler((event) => {
const path = getRequestURL(event).pathname const path = getRequestURL(event).pathname
// /lire → /economique/modele-eco
if (path.startsWith('/lire')) { if (path.startsWith('/lire')) {
const rest = path.slice(5) // remove '/lire' const rest = path.slice(5) // remove '/lire'
return sendRedirect(event, `/modele-eco${rest || '/'}`, 301) return sendRedirect(event, `/economique/modele-eco${rest || '/'}`, 301)
} }
if (path.startsWith('/ecouter')) { if (path.startsWith('/ecouter')) {
const rest = path.slice(8) // remove '/ecouter' const rest = path.slice(8) // remove '/ecouter'
return sendRedirect(event, `/en-musique${rest || '/'}`, 301) return sendRedirect(event, `/en-musique${rest || '/'}`, 301)
} }
if (path === '/autonomie' || path === '/autonomie/') {
return sendRedirect(event, '/numerique', 301)
}
if (path === '/decision' || path === '/decision/') {
return sendRedirect(event, '/citoyenne/decision', 301)
}
// /modele-eco → /economique/modele-eco
if (path.startsWith('/modele-eco')) {
const rest = path.slice(11) // remove '/modele-eco'
return sendRedirect(event, `/economique/modele-eco${rest || '/'}`, 301)
}
// Redirect old /gestation/* routes to proper sections
if (path.startsWith('/gestation/')) {
const slug = path.slice(11).replace(/\/$/, '')
const numeriquePages = ['logiciel-libre', 'authentification-wot', 'cloud-libre']
if (numeriquePages.includes(slug)) {
return sendRedirect(event, `/numerique/${slug}`, 301)
}
if (slug === 'productions-collectives') {
return sendRedirect(event, '/economique/productions-collectives', 301)
}
if (slug === 'tarifs-eau') {
return sendRedirect(event, '/citoyenne/tarifs-eau', 301)
}
// Fallback
return sendRedirect(event, '/', 301)
}
}) })

44
server/utils/gitSync.ts Normal file
View File

@@ -0,0 +1,44 @@
import { execFile } from 'node:child_process'
import { promisify } from 'node:util'
const exec = promisify(execFile)
const enabled = process.env.ADMIN_GIT_SYNC === 'true'
const cwd = process.cwd()
async function git(...args: string[]): Promise<string> {
const { stdout } = await exec('git', args, { cwd })
return stdout.trim()
}
/**
* Commit et push les modifications admin vers le dépôt git.
* Activé uniquement si ADMIN_GIT_SYNC=true (prod).
* Ne bloque jamais la réponse HTTP — exécution fire-and-forget.
*/
export function gitSyncContent(description: string, paths: string[] = ['.']) {
if (!enabled) return
const run = async () => {
try {
for (const p of paths) {
await git('add', p)
}
// Vérifier s'il y a vraiment des changements stagés
const status = await git('diff', '--cached', '--name-only')
if (!status) return
await git('commit', '-m', `[admin] ${description}`)
await git('push')
console.log(`[gitSync] pushed: ${description}`)
}
catch (err: any) {
console.error(`[gitSync] error:`, err.message ?? err)
}
}
// Fire-and-forget : ne pas bloquer la réponse HTTP
run()
}

View File

@@ -1,6 +1,7 @@
book: book:
title: Une économie du don — enfin concevable title: Une économie du don — enfin concevable
author: Yvv author: Yvv
pdfFile: /pdf/une-economie-du-don.pdf
description: Un livre et 9 chansons pour explorer ensemble les fondements d'une économie fondée sur le don. description: Un livre et 9 chansons pour explorer ensemble les fondements d'une économie fondée sur le don.
coverImage: /images/book-cover.jpg coverImage: /images/book-cover.jpg
license: CC-BY-NC license: CC-BY-NC
@@ -560,6 +561,29 @@ chapterSongs:
- chapterSlug: 11-annexes - chapterSlug: 11-annexes
songId: coder-la-liberte songId: coder-la-liberte
primary: true primary: true
chapterPages:
- chapterSlug: 01-introduction
page: 9
- chapterSlug: 02-don
page: 23
- chapterSlug: 03-mesure
page: 43
- chapterSlug: 04-monnaie
page: 49
- chapterSlug: 05-trm
page: 83
- chapterSlug: 06-economie
page: 121
- chapterSlug: 07-echange
page: 147
- chapterSlug: 08-institution
page: 163
- chapterSlug: 09-greffes
page: 181
- chapterSlug: 10-maintenant
page: 193
- chapterSlug: 11-annexes
page: 199
defaultPlaylistOrder: defaultPlaylistOrder:
- ce-livre-est-une-facon - ce-livre-est-une-facon
- de-quel-don-nous-parlons - de-quel-don-nous-parlons

View File

@@ -1,46 +0,0 @@
kicker: Pourquoi l'autonomie
title: Autonomie
description: Des passages du livre qui éclairent la démarche d'autonomie collective — le fil rouge du projet.
meta:
title: Autonomie
extracts:
- chapter: Introduction
chapterSlug: 01-introduction
text: >
Ben. Pour l'autonomie.
...C'est tout — sauf un repli.
Balkanisation ? que nenni !
Réfuter l'autonomie... c'est fallacieux,
c'est nous bannir en tant qu'adultes, bien vivants,
restez là sans mot dire,
"restez des enfants !"...
mmh, suspect et sans avenir.
- chapter: Introduction
chapterSlug: 01-introduction
text: >
Ne plus subir les agendas. Créer les nôtr'.
On manque de repères ?... Entre autres, ...
Faut les trouver,...
En produisant, les inventer.
- chapter: "Raison d'être d'une monnaie"
chapterSlug: 04-monnaie
text: >
Même accès pour tous.
Même pouvoir de création.
Ce n'est plus "Que la dette soit".
C'est "Que l'équilibre soit".
- chapter: "Créer une économie ?"
chapterSlug: 06-economie
text: >
Le D.U, c'est une mesure.
Pour ne plus obliger. Pour ne plus devoir.
Je donne à mon économie, j'alimente le réservoir.
Je compte sur les autres, sur mon économie,
Pour y trouver ma pleine mesure.
- chapter: "Et maintenant ?… action ?"
chapterSlug: 10-maintenant
text: >
Mais la monnaie n'est pas la richesse.
C'est juste le mètre... pas le tissu.
C'est le baromètre... pas le climat.
Ne confondons pas la carte et le territoire.

22
site/pages/citoyenne.yml Normal file
View File

@@ -0,0 +1,22 @@
meta:
title: Autonomie citoyenne
kicker: Autonomie citoyenne
title: La décision
description: >
Se donner les moyens de la décision collective. Reprendre la main
sur les processus qui structurent notre vie commune — à l'échelle
des bassins de vie.
items:
- id: decision
label: Décision collective
icon: gavel
description: Se donner les moyens de la décision collective.
to: /citoyenne/decision
- id: tarifs-eau
label: Tarifs de l'eau
icon: droplets
description: Application pour obtenir justice sociale et incitation dynamique à la réduction. Permet de confier la décision à la population des communes.
to: /citoyenne/tarifs-eau
gestation: true

View File

@@ -0,0 +1,21 @@
meta:
title: Décision collective — Autonomie citoyenne
kicker: Autonomie citoyenne
title: Plateforme Décision
icon: gavel
description: Se donner les moyens de la décision collective.
features:
- icon: vote
title: Décisions on-chain
text: Des décisions transparentes et vérifiables, inscrites sur la blockchain.
- icon: scroll-text
title: Les Mandats
text: Formaliser et suivre les mandats confiés aux personnes désignées.
- icon: scroll-text
title: Documents de référence
text: Les textes fondateurs et documents qui encadrent la prise de décision.
- icon: git-branch
title: Les Protocoles
text: Les règles et processus qui structurent la décision collective.

View File

@@ -0,0 +1,20 @@
meta:
title: Tarifs de l'eau — Autonomie citoyenne
kicker: Autonomie citoyenne
title: Tarifs de l'eau
icon: droplets
gestation: true
description: >
Application pour obtenir justice sociale et incitation dynamique
à la réduction. Permet de confier la décision à la population des communes.
project:
name: SejeteralO
text: Simulateur de tarification de l'eau — justice sociale et écologie.
content: >
La tarification de l'eau est un levier essentiel de justice sociale
et d'incitation à la réduction de la consommation. SejeteralO permet
aux communes de simuler et comparer différentes grilles tarifaires.

29
site/pages/economique.yml Normal file
View File

@@ -0,0 +1,29 @@
meta:
title: Autonomie économique
kicker: Autonomie économique
title: La création monétaire
description: >
Pour fonctionner autrement, il faut créer une économie qui le permette.
Au fondement de toute économie se trouve la création monétaire — son code,
ses règles, ses bénéficiaires. Tant que ce code est écrit par d'autres,
pour d'autres, aucune autonomie réelle n'est possible.
La monnaie libre propose un autre code : symétrique, transparent, partagé.
items:
- id: monnaie-libre
label: Monnaie libre
icon: g1
description: "La Ğ1 (June) : chaque membre crée la même part de monnaie, chaque jour. Sans dette ni intérêt — le dividende universel."
to: /economique/monnaie-libre
- id: modele-eco
label: Économie du don
icon: scale
description: Un livre et des chansons pour une proposition de modèle économique fondé sur le don.
to: /economique/modele-eco
- id: productions-collectives
label: Productions collectives
icon: users
description: Une plateforme pour faciliter la création d'équipes et la réalisation de productions à l'échelle des bassins de vie.
to: /economique/productions-collectives
gestation: true

View File

@@ -0,0 +1,23 @@
meta:
title: Commandez le livre papier
kicker: Le livre
title: Commandez le livre papier
icon: shopping-bag
description: >
« Une économie du don — enfin concevable » est disponible en version papier.
Vous pouvez le commander en ligne ou le demander en librairie.
bookelis:
label: Commander sur Bookelis
url: https://www.bookelis.com/economie/56818-Une-economie-du-don-enfin-concevable.html
librairie:
title: En librairie
text: >
Le livre est disponible à la commande dans toute librairie jusqu'au printemps 2027.
Cette disponibilité dépend d'un abonnement de référencement Hachette,
qui permet aux libraires de le trouver dans leur catalogue et de le commander
auprès de leur distributeur. Au-delà de cette date, la commande en ligne
sur Bookelis reste disponible.

View File

@@ -0,0 +1,59 @@
meta:
title: Monnaie libre — Autonomie économique
kicker: Autonomie économique
title: Monnaie libre
icon: g1
description: >
Toute économie repose sur un code monétaire.
Le nôtre programme la cavalerie et la machine à faillites.
La monnaie libre recode : chaque membre crée la même part relative de monnaie,
chaque jour. Symétrique dans l'espace, symétrique dans le temps.
content: >
La Théorie Relative de la Monnaie (TRM), formulée par Stéphane Laborde, pose
la question autrement : à quelle condition une monnaie ne privilégie-t-elle
personne dans sa création ? Ni aucun individu parmi ses contemporains,
ni aucune génération par rapport aux suivantes ?
La réponse est la double symétrie.
Symétrie spatiale : chaque membre crée la même part de monnaie que ses voisins.
Symétrie temporelle : les membres d'aujourd'hui ne privent pas ceux de demain.
Le Dividende Universel (DU) est la traduction pratique de ces deux symétries —
une part égale de création monétaire, pour chaque membre certifié, chaque jour.
Ni plus, ni moins.
Ce n'est pas une monnaie complémentaire indexée sur l'euro.
Ce n'est pas une monnaie locale. Ce n'est pas une spéculation.
La Ğ1 (June) est la première implémentation de la TRM — lancée le 8 mars 2017,
plus de 6 000 membres, des milliers d'offres et de demandes,
une conférence biannuelle. Une économie vivante.
L'entrée se fait par la toile de confiance (WoT) : chaque nouveau membre
est certifié par des membres existants, dans un graphe de confiance humain.
Pas de banque, pas d'État, pas de biométrie.
C'est aussi ce qui lie monnaie libre et identité décentralisée —
la même clé ouvre les deux portes.
Le DU comme unité de mesure change la posture.
On ne vend plus — on donne. On n'achète plus — on reçoit.
On ne paye plus — on mesure.
La monnaie libre mesure la gratitude. Pas le prix.
links:
- label: monnaie-libre.fr
href: https://monnaie-libre.fr
icon: external-link
- label: Théorie Relative de la Monnaie
href: https://trm.creationmonetaire.info
icon: book-open
- label: duniter.org — le logiciel
href: https://duniter.org
icon: code-2
- label: Forum Duniter
href: https://forum.duniter.org
icon: message-square
- label: Forum Monnaie Libre
href: https://forum.monnaie-libre.fr
icon: users

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