diff --git a/src/core/cron/README.md b/src/core/cron/README.md
new file mode 100644
index 0000000..cd635dc
--- /dev/null
+++ b/src/core/cron/README.md
@@ -0,0 +1,132 @@
+# Cron Framework
+
+Ce répertoire est un **wrapper générique autour de `node-cron`**. Il ne connaît aucune tâche spécifique — les modules et features enregistrent leurs propres jobs. Ajouter un nouveau job ne nécessite jamais de modifier `src/core/cron/`.
+
+---
+
+## Structure
+
+```
+src/core/cron/
+└── index.js schedule, stop, stopAll, trigger, validate, isRunning, getJobs, getStatus
+```
+
+---
+
+## Import
+
+```js
+import { schedule, stop, trigger } from '@zen/core/cron';
+```
+
+---
+
+## API
+
+### `schedule(name, cronSchedule, handler, options?)`
+
+Enregistre un job. Si un job du même nom existe déjà, il est stoppé et remplacé.
+
+```js
+schedule('daily-report', '0 9 * * *', async () => {
+ await sendReport();
+});
+
+schedule('every-5min', '*/5 * * * *', async () => {
+ await syncData();
+}, { timezone: 'America/New_York', runOnInit: true });
+```
+
+| Paramètre | Type | Description |
+|-----------|------|-------------|
+| `name` | `string` | Nom unique du job |
+| `cronSchedule` | `string` | Expression cron (5 ou 6 champs) |
+| `handler` | `async Function` | Fonction exécutée à chaque déclenchement |
+| `options.timezone` | `string` | Timezone IANA (défaut : `ZEN_TIMEZONE` ou `America/Toronto`) |
+| `options.runOnInit` | `boolean` | Exécuter immédiatement à l'enregistrement (défaut : `false`) |
+
+Retourne l'instance `node-cron` task.
+
+---
+
+### `stop(name)`
+
+Stoppe et supprime un job par son nom. Retourne `true` si le job existait, `false` sinon.
+
+### `stopAll()`
+
+Stoppe et supprime tous les jobs enregistrés.
+
+### `trigger(name)`
+
+Déclenche manuellement un job sans attendre son prochain tick. Lève une `Error` si le job n'existe pas.
+
+```js
+await trigger('daily-report');
+```
+
+### `validate(expression)`
+
+Valide une expression cron. Retourne `boolean`.
+
+### `isRunning(name)`
+
+Vérifie si un job est actuellement enregistré. Retourne `boolean`.
+
+### `getJobs()`
+
+Retourne la liste des noms de tous les jobs enregistrés (`string[]`).
+
+### `getStatus()`
+
+Retourne les métadonnées de tous les jobs enregistrés.
+
+```js
+{
+ 'daily-report': {
+ schedule: '0 9 * * *',
+ timezone: 'America/Toronto',
+ registeredAt: '2026-04-24T09:00:00.000Z'
+ }
+}
+```
+
+---
+
+## Enregistrer un job depuis un module
+
+Les jobs vivent **avec leur feature ou module**, pas dans le framework. Enregistrer un job dans `initializeZen()` (`src/shared/lib/init.js`) :
+
+```js
+// src/modules/mymodule/cron.js
+import { schedule } from '@zen/core/cron';
+
+export function registerCronJobs() {
+ schedule('mymodule-sync', '*/15 * * * *', async () => {
+ await syncMyModule();
+ });
+}
+```
+
+```js
+// src/shared/lib/init.js
+import { registerCronJobs } from '../../modules/mymodule/cron.js';
+
+registerCronJobs();
+```
+
+---
+
+## Comportement Hot-Reload
+
+Les jobs sont stockés dans `globalThis[Symbol.for('__ZEN_CRON_JOBS__')]` — un store partagé qui survit aux invalidations de cache de modules de Next.js. Un job enregistré deux fois (hot-reload) remplace silencieusement l'ancien plutôt que de créer un doublon.
+
+---
+
+## Gestion des erreurs
+
+Les erreurs levées par un handler sont interceptées et loguées via `fail()` — elles ne font jamais crasher le processus.
+
+```
+✗ Cron daily-report: Connection timeout
+```
diff --git a/src/core/email/README.md b/src/core/email/README.md
new file mode 100644
index 0000000..c7c0ad8
--- /dev/null
+++ b/src/core/email/README.md
@@ -0,0 +1,155 @@
+# Email Framework
+
+Ce répertoire fournit un **wrapper autour de [Resend](https://resend.com)** pour l'envoi d'emails, ainsi qu'un composant de mise en page React Email réutilisable. Il ne connaît aucun template métier — les features créent leurs propres templates et utilisent ce module pour l'envoi.
+
+---
+
+## Structure
+
+```
+src/core/email/
+├── index.js sendEmail, sendBatchEmails
+└── templates/
+ ├── index.js re-export
+ └── BaseLayout.js composant de mise en page React Email
+```
+
+---
+
+## Import
+
+```js
+import { sendEmail, sendBatchEmails } from '@zen/core/email';
+import { BaseLayout } from '@zen/core/email/templates';
+```
+
+---
+
+## Variables d'environnement
+
+| Variable | Obligatoire | Description |
+|----------|-------------|-------------|
+| `ZEN_EMAIL_RESEND_APIKEY` | Oui | Clé API Resend |
+| `ZEN_EMAIL_FROM_ADDRESS` | Oui | Adresse expéditeur par défaut |
+| `ZEN_EMAIL_FROM_NAME` | Non | Nom affiché de l'expéditeur |
+| `ZEN_EMAIL_LOGO` | Non | URL du logo affiché dans `BaseLayout` |
+| `ZEN_EMAIL_LOGO_URL` | Non | URL de destination du lien autour du logo |
+| `ZEN_SUPPORT_EMAIL` | Non | Email affiché dans le footer si `supportSection` est activé |
+| `ZEN_NAME` | Non | Nom de l'application (fallback du nom affiché dans `BaseLayout`) |
+
+---
+
+## API
+
+### `sendEmail(email)`
+
+Envoie un email via Resend. Retourne `{ success, data, error }`.
+
+```js
+const result = await sendEmail({
+ to: 'user@example.com',
+ subject: 'Bienvenue',
+ html: '
Bonjour !
',
+});
+
+if (!result.success) {
+ console.error(result.error);
+}
+```
+
+| Paramètre | Type | Description |
+|-----------|------|-------------|
+| `to` | `string \| string[]` | Destinataire(s) |
+| `subject` | `string` | Objet de l'email |
+| `html` | `string` | Corps HTML |
+| `text` | `string` | Corps texte brut (optionnel) |
+| `from` | `string` | Adresse expéditeur (défaut : `ZEN_EMAIL_FROM_ADDRESS`) |
+| `fromName` | `string` | Nom expéditeur (défaut : `ZEN_EMAIL_FROM_NAME`) |
+| `replyTo` | `string` | Adresse de réponse (optionnel) |
+| `attachments` | `object[]` | Pièces jointes Resend (optionnel) |
+| `tags` | `object[]` | Tags Resend (optionnel) |
+
+---
+
+### `sendBatchEmails(emails)`
+
+Envoie plusieurs emails en une seule requête batch Resend. Retourne `{ success, data, error }`.
+
+```js
+await sendBatchEmails([
+ { to: 'a@example.com', subject: 'Sujet A', html: 'A
' },
+ { to: 'b@example.com', subject: 'Sujet B', html: 'B
' },
+]);
+```
+
+Chaque objet du tableau accepte les mêmes paramètres que `sendEmail`.
+
+---
+
+## BaseLayout
+
+Composant React Email (`@react-email/components`) qui fournit une structure cohérente : logo ou nom de l'app, titre optionnel, contenu, footer avec copyright et lien support.
+
+```jsx
+import { render } from '@react-email/render';
+import { BaseLayout } from '@zen/core/email/templates';
+
+const html = await render(
+
+ Merci pour votre achat.
+
+);
+
+await sendEmail({ to: 'user@example.com', subject: 'Commande confirmée', html });
+```
+
+| Prop | Type | Description |
+|------|------|-------------|
+| `preview` | `string` | Texte de prévisualisation (snippet email) |
+| `title` | `string` | Titre affiché en haut du corps |
+| `children` | `ReactNode` | Contenu de l'email |
+| `companyName` | `string` | Nom affiché si pas de logo (défaut : `ZEN_NAME` ou `ZEN`) |
+| `logoURL` | `string` | URL du logo (défaut : `ZEN_EMAIL_LOGO`) |
+| `supportSection` | `boolean` | Afficher le lien support dans le footer (défaut : `false`) |
+| `supportEmail` | `string` | Email support (défaut : `ZEN_SUPPORT_EMAIL`) |
+
+---
+
+## Créer un template depuis une feature
+
+Les templates vivent **avec leur feature**, pas dans ce répertoire.
+
+```jsx
+// src/features/auth/emails/WelcomeEmail.js
+import { BaseLayout } from '@zen/core/email/templates';
+import { Text, Button } from '@react-email/components';
+
+export const WelcomeEmail = ({ name, loginUrl }) => (
+
+ Bonjour {name}, votre compte est prêt.
+ Se connecter
+
+);
+```
+
+```js
+// src/features/auth/emails/sendWelcome.js
+import { render } from '@react-email/render';
+import { sendEmail } from '@zen/core/email';
+import { WelcomeEmail } from './WelcomeEmail.js';
+
+export async function sendWelcomeEmail({ to, name, loginUrl }) {
+ const html = await render( );
+ return sendEmail({ to, subject: 'Bienvenue !', html });
+}
+```
+
+---
+
+## Gestion des erreurs
+
+`sendEmail` et `sendBatchEmails` ne lèvent jamais d'exception — toute erreur est capturée, loguée via `fail()`, et retournée dans `{ success: false, error }`. L'appelant vérifie `result.success`.
diff --git a/src/core/payments/README.md b/src/core/payments/README.md
new file mode 100644
index 0000000..1eef61e
--- /dev/null
+++ b/src/core/payments/README.md
@@ -0,0 +1,225 @@
+# Payments Framework
+
+Ce répertoire fournit un **wrapper autour de [Stripe](https://stripe.com)** pour la gestion des paiements : sessions de checkout, intents, clients, remboursements et webhooks.
+
+---
+
+## Structure
+
+```
+src/core/payments/
+├── index.js re-export
+└── stripe.js wrapper Stripe
+```
+
+---
+
+## Import
+
+```js
+import {
+ isEnabled,
+ getPublishableKey,
+ createCheckoutSession,
+ createPaymentIntent,
+ getCheckoutSession,
+ getPaymentIntent,
+ verifyWebhookSignature,
+ createCustomer,
+ getOrCreateCustomer,
+ listPaymentMethods,
+ createRefund,
+} from '@zen/core/payments';
+```
+
+---
+
+## Variables d'environnement
+
+| Variable | Obligatoire | Description |
+|----------|-------------|-------------|
+| `STRIPE_SECRET_KEY` | Oui | Clé secrète Stripe (côté serveur) |
+| `STRIPE_PUBLISHABLE_KEY` | Oui | Clé publique Stripe (côté client) |
+| `STRIPE_WEBHOOK_SECRET` | Pour les webhooks | Secret de signature des webhooks Stripe |
+| `ZEN_CURRENCY` | Non | Devise par défaut pour les payment intents (défaut : `cad`) |
+
+---
+
+## API
+
+### `isEnabled()`
+
+Retourne `true` si `STRIPE_SECRET_KEY` et `STRIPE_PUBLISHABLE_KEY` sont définis. Utiliser pour conditionner l'affichage des fonctionnalités de paiement.
+
+```js
+if (isEnabled()) {
+ // afficher le bouton de paiement
+}
+```
+
+---
+
+### `getPublishableKey()`
+
+Retourne la clé publique Stripe, ou `null` si absente. Passer au client pour initialiser Stripe.js ou `@stripe/react-stripe-js`.
+
+```js
+const key = getPublishableKey();
+```
+
+---
+
+### `createCheckoutSession(options)`
+
+Crée une session Stripe Checkout. Retourne la session Stripe.
+
+```js
+const session = await createCheckoutSession({
+ lineItems: [{ price: 'price_xxx', quantity: 1 }],
+ successUrl: 'https://example.com/success',
+ cancelUrl: 'https://example.com/cancel',
+ customerEmail: 'user@example.com',
+ mode: 'payment',
+});
+
+// Rediriger l'utilisateur vers session.url
+```
+
+| Paramètre | Type | Description |
+|-----------|------|-------------|
+| `lineItems` | `object[]` | Lignes de commande Stripe |
+| `successUrl` | `string` | URL de retour après paiement réussi |
+| `cancelUrl` | `string` | URL de retour après annulation |
+| `customerEmail` | `string` | Email pré-rempli dans le formulaire (optionnel) |
+| `clientReferenceId` | `string` | Identifiant interne pour rapprochement (optionnel) |
+| `metadata` | `object` | Métadonnées Stripe (optionnel) |
+| `mode` | `string` | `'payment'`, `'subscription'` ou `'setup'` (défaut : `'payment'`) |
+
+---
+
+### `createPaymentIntent(options)`
+
+Crée un PaymentIntent Stripe. Retourne le PaymentIntent.
+
+```js
+const intent = await createPaymentIntent({
+ amount: 4999, // en centimes
+ currency: 'eur',
+ metadata: { orderId: '123' },
+});
+```
+
+| Paramètre | Type | Description |
+|-----------|------|-------------|
+| `amount` | `number` | Montant en centimes |
+| `currency` | `string` | Devise ISO (défaut : `ZEN_CURRENCY` ou `cad`) |
+| `metadata` | `object` | Métadonnées Stripe (optionnel) |
+| `automaticPaymentMethods` | `object` | Config des méthodes de paiement (défaut : `{ enabled: true }`) |
+
+---
+
+### `getCheckoutSession(sessionId)`
+
+Récupère une session Checkout par son identifiant. À utiliser dans la route `successUrl` pour confirmer le paiement.
+
+```js
+const session = await getCheckoutSession(sessionId);
+```
+
+---
+
+### `getPaymentIntent(paymentIntentId)`
+
+Récupère un PaymentIntent par son identifiant.
+
+```js
+const intent = await getPaymentIntent(paymentIntentId);
+```
+
+---
+
+### `verifyWebhookSignature(payload, signature)`
+
+Vérifie la signature d'un webhook Stripe et retourne l'événement. Lève une erreur si la signature est invalide ou si `STRIPE_WEBHOOK_SECRET` est absent.
+
+```js
+// Next.js Route Handler
+export async function POST(req) {
+ const payload = await req.text();
+ const signature = req.headers.get('stripe-signature');
+
+ let event;
+ try {
+ event = await verifyWebhookSignature(payload, signature);
+ } catch (err) {
+ return new Response('Signature invalide', { status: 400 });
+ }
+
+ if (event.type === 'checkout.session.completed') {
+ // traiter la commande
+ }
+
+ return new Response('OK');
+}
+```
+
+Le `payload` doit être le corps brut de la requête (non parsé).
+
+---
+
+### `createCustomer(options)`
+
+Crée un client Stripe. Retourne le client.
+
+```js
+const customer = await createCustomer({
+ email: 'user@example.com',
+ name: 'Jean Dupont',
+ metadata: { userId: '42' },
+});
+```
+
+---
+
+### `getOrCreateCustomer(email, defaultData)`
+
+Retourne le client Stripe existant pour cet email, ou en crée un nouveau. Utilise une clé d'idempotence dérivée de l'email pour limiter les doublons en cas d'appels concurrents.
+
+```js
+const customer = await getOrCreateCustomer('user@example.com', {
+ name: 'Jean Dupont',
+ metadata: { userId: '42' },
+});
+```
+
+---
+
+### `listPaymentMethods(customerId, type)`
+
+Retourne la liste des méthodes de paiement d'un client.
+
+```js
+const methods = await listPaymentMethods(customer.id, 'card');
+```
+
+Le paramètre `type` est optionnel (défaut : `'card'`).
+
+---
+
+### `createRefund(options)`
+
+Crée un remboursement. Retourne le remboursement Stripe.
+
+```js
+const refund = await createRefund({
+ paymentIntentId: 'pi_xxx',
+ amount: 1000, // partiel, en centimes (optionnel — total si absent)
+ reason: 'requested_by_customer', // optionnel
+});
+```
+
+| Paramètre | Type | Description |
+|-----------|------|-------------|
+| `paymentIntentId` | `string` | Identifiant du PaymentIntent à rembourser |
+| `amount` | `number` | Montant en centimes (optionnel, remboursement total si absent) |
+| `reason` | `string` | Raison Stripe : `duplicate`, `fraudulent`, `requested_by_customer` (optionnel) |
diff --git a/src/core/pdf/README.md b/src/core/pdf/README.md
new file mode 100644
index 0000000..90b8274
--- /dev/null
+++ b/src/core/pdf/README.md
@@ -0,0 +1,142 @@
+# PDF Framework
+
+Ce répertoire re-exporte les primitives de [`@react-pdf/renderer`](https://react-pdf.org) et fournit un utilitaire de nommage de fichiers. Il ne contient aucun template métier — les features créent leurs propres documents et utilisent ce module pour le rendu.
+
+---
+
+## Structure
+
+```
+src/core/pdf/
+└── index.js re-exports @react-pdf/renderer + getFilename
+```
+
+---
+
+## Import
+
+```js
+import {
+ renderToBuffer,
+ Document,
+ Page,
+ View,
+ Text,
+ Image,
+ Link,
+ StyleSheet,
+ Font,
+ getFilename,
+} from '@zen/core/pdf';
+```
+
+---
+
+## API
+
+### `renderToBuffer(element)`
+
+Rend un document React PDF en `Buffer`. Retourne une `Promise`.
+
+```js
+import { renderToBuffer, Document, Page, Text } from '@zen/core/pdf';
+
+const buffer = await renderToBuffer(
+
+
+ Bonjour
+
+
+);
+```
+
+Utiliser ce buffer pour servir le PDF en réponse HTTP ou l'écrire sur disque.
+
+---
+
+### `getFilename(prefix, identifier, date?)`
+
+Retourne un nom de fichier normalisé pour un PDF.
+
+```js
+getFilename('invoice', '12345')
+// 'invoice-12345-2024-01-15.pdf'
+
+getFilename('receipt', 'ORD-99', new Date('2024-06-01'))
+// 'receipt-ORD-99-2024-06-01.pdf'
+```
+
+| Paramètre | Type | Description |
+|-----------|------|-------------|
+| `prefix` | `string` | Type de document (`invoice`, `receipt`, etc.) |
+| `identifier` | `string` | Identifiant unique (numéro de commande, ID, etc.) |
+| `date` | `Date` | Date du document (défaut : aujourd'hui) |
+
+---
+
+### Primitives re-exportées
+
+Toutes les primitives de `@react-pdf/renderer` sont disponibles directement depuis `@zen/core/pdf` :
+
+| Export | Description |
+|--------|-------------|
+| `Document` | Racine d'un document PDF |
+| `Page` | Page du document |
+| `View` | Conteneur (équivalent `div`) |
+| `Text` | Bloc de texte |
+| `Image` | Image (URL ou base64) |
+| `Link` | Lien hypertexte |
+| `StyleSheet` | Création de styles (similaire à `StyleSheet.create` React Native) |
+| `Font` | Enregistrement de polices personnalisées |
+
+---
+
+## Créer un template depuis une feature
+
+Les templates vivent **avec leur feature**, pas dans ce répertoire.
+
+```jsx
+// src/features/orders/pdf/InvoiceDocument.js
+import { Document, Page, View, Text, StyleSheet } from '@zen/core/pdf';
+
+const styles = StyleSheet.create({
+ page: { padding: 40 },
+ title: { fontSize: 20, marginBottom: 16 },
+});
+
+export const InvoiceDocument = ({ order }) => (
+
+
+ Facture #{order.number}
+ {order.customerName}
+
+
+);
+```
+
+```js
+// src/features/orders/pdf/sendInvoice.js
+import { renderToBuffer, getFilename } from '@zen/core/pdf';
+import { InvoiceDocument } from './InvoiceDocument.js';
+
+export async function generateInvoicePdf(order) {
+ const buffer = await renderToBuffer( );
+ const filename = getFilename('invoice', order.number);
+ return { buffer, filename };
+}
+```
+
+```js
+// Next.js Route Handler
+export async function GET(req, { params }) {
+ const order = await getOrder(params.id);
+ const { buffer, filename } = await generateInvoicePdf(order);
+
+ return new Response(buffer, {
+ headers: {
+ 'Content-Type': 'application/pdf',
+ 'Content-Disposition': `attachment; filename="${filename}"`,
+ },
+ });
+}
+```
diff --git a/src/core/themes/README.md b/src/core/themes/README.md
new file mode 100644
index 0000000..803d914
--- /dev/null
+++ b/src/core/themes/README.md
@@ -0,0 +1,153 @@
+# Themes
+
+Ce répertoire gère le thème clair/sombre de l'interface. Il expose des utilitaires client pour lire, appliquer et réagir au thème, ainsi qu'un script d'initialisation à injecter dans `` pour éviter le flash au chargement.
+
+---
+
+## Structure
+
+```
+src/core/themes/
+└── index.js THEME_INIT_SCRIPT, getStoredTheme, applyTheme, getThemeIcon, ThemeWatcher, useTheme
+```
+
+---
+
+## Import
+
+```js
+import {
+ THEME_INIT_SCRIPT,
+ getStoredTheme,
+ applyTheme,
+ getThemeIcon,
+ ThemeWatcher,
+ useTheme,
+} from '@zen/core/themes';
+```
+
+Tous les exports sont marqués `'use client'`.
+
+---
+
+## API
+
+### `THEME_INIT_SCRIPT`
+
+Script inline à injecter dans `` avant le premier rendu. Il lit `localStorage` et applique la classe `dark` sur `` immédiatement, ce qui évite le flash de thème (FOUC).
+
+```jsx
+// app/layout.js
+import { THEME_INIT_SCRIPT } from '@zen/core/themes';
+
+export default function RootLayout({ children }) {
+ return (
+
+
+
+
+ {children}
+
+ );
+}
+```
+
+---
+
+### `getStoredTheme()`
+
+Lit le thème enregistré dans `localStorage`. Retourne `'light'`, `'dark'` ou `'auto'`.
+
+```js
+const theme = getStoredTheme(); // 'light' | 'dark' | 'auto'
+```
+
+---
+
+### `applyTheme(theme)`
+
+Applique un thème en modifiant `document.documentElement` et en mettant à jour `localStorage`. En mode `'auto'`, retire la préférence stockée et suit le système.
+
+| Valeur | Comportement |
+|--------|-------------|
+| `'light'` | Retire la classe `dark`, stocke `'light'` |
+| `'dark'` | Ajoute la classe `dark`, stocke `'dark'` |
+| `'auto'` | Retire la valeur stockée, suit `prefers-color-scheme` |
+
+```js
+applyTheme('dark');
+applyTheme('auto');
+```
+
+---
+
+### `getThemeIcon(theme, systemIsDark)`
+
+Retourne le composant icône correspondant au thème actuel.
+
+| Thème | `systemIsDark` | Icône retournée |
+|-------|----------------|-----------------|
+| `'light'` | - | `Sun01Icon` |
+| `'dark'` | - | `Moon02Icon` |
+| `'auto'` | `true` | `MoonCloudIcon` |
+| `'auto'` | `false` | `SunCloud01Icon` |
+
+```jsx
+const Icon = getThemeIcon(theme, systemIsDark);
+return ;
+```
+
+---
+
+### `ThemeWatcher`
+
+Composant sans rendu qui écoute les changements de `prefers-color-scheme`. Si aucune préférence n'est stockée dans `localStorage`, il met à jour la classe `dark` automatiquement quand le système change.
+
+```jsx
+// Placer une fois dans le layout racine, après THEME_INIT_SCRIPT.
+import { ThemeWatcher } from '@zen/core/themes';
+
+export default function RootLayout({ children }) {
+ return (
+
+
+
+ {children}
+
+
+ );
+}
+```
+
+---
+
+### `useTheme()`
+
+Hook qui expose le thème actuel et une fonction de basculement cyclique. Synchronise l'état avec `localStorage` et le système au montage.
+
+Retourne `{ theme, toggle, systemIsDark }`.
+
+| Propriété | Type | Description |
+|-----------|------|-------------|
+| `theme` | `'light' \| 'dark' \| 'auto'` | Thème actif |
+| `toggle` | `() => void` | Passe au thème suivant dans le cycle |
+| `systemIsDark` | `boolean` | Indique si le système est en mode sombre |
+
+Le cycle de basculement dépend de la préférence système :
+- Système clair : `auto` -> `dark` -> `light` -> `auto`
+- Système sombre : `auto` -> `light` -> `dark` -> `auto`
+
+```jsx
+import { useTheme, getThemeIcon } from '@zen/core/themes';
+
+export function ThemeToggle() {
+ const { theme, toggle, systemIsDark } = useTheme();
+ const Icon = getThemeIcon(theme, systemIsDark);
+
+ return (
+
+
+
+ );
+}
+```
diff --git a/src/core/toast/README.md b/src/core/toast/README.md
new file mode 100644
index 0000000..edb53e3
--- /dev/null
+++ b/src/core/toast/README.md
@@ -0,0 +1,145 @@
+# Toast
+
+Ce répertoire fournit un **système de notifications toast** basé sur un contexte React. Il expose un provider, un hook, et un conteneur à placer dans le layout. Les features utilisent le hook pour déclencher des notifications.
+
+---
+
+## Structure
+
+```
+src/core/toast/
+├── index.js Toast, ToastProvider, useToast, ToastContainer
+├── ToastContext.js contexte, provider, hook useToast
+├── ToastContainer.js conteneur à monter dans le layout
+└── Toast.js composant d'affichage individuel
+```
+
+---
+
+## Import
+
+```js
+import { ToastProvider, useToast, ToastContainer } from '@zen/core/toast';
+```
+
+---
+
+## Mise en place
+
+Entourer le layout avec `ToastProvider` et y placer `ToastContainer`.
+
+```jsx
+import { ToastProvider, ToastContainer } from '@zen/core/toast';
+
+export default function RootLayout({ children }) {
+ return (
+
+ {children}
+
+
+ );
+}
+```
+
+`useToast` lève une erreur si appelé hors du `ToastProvider`.
+
+---
+
+## API
+
+### `useToast()`
+
+Hook qui expose les méthodes et l'état courant des toasts.
+
+```js
+const { success, error, warning, info, addToast, removeToast, clearAllToasts } = useToast();
+```
+
+**Méthodes de raccourci**
+
+```js
+success('Modifications enregistrées.');
+error('La connexion a échoué.');
+warning('Session sur le point d'expirer.');
+info('Une mise à jour est disponible.');
+```
+
+Chaque méthode accepte un message et un objet `options` optionnel pour surcharger les paramètres par défaut.
+
+```js
+success('Fichier importé.', { duration: 3000, dismissible: false });
+```
+
+Toutes retournent l'`id` du toast créé.
+
+**`addToast(toast)`**
+
+Crée un toast à partir d'un objet complet.
+
+```js
+const id = addToast({
+ type: 'success',
+ message: 'Profil mis à jour.',
+ title: 'Enregistré',
+ duration: 4000,
+ dismissible: true,
+});
+```
+
+| Paramètre | Type | Défaut | Description |
+|-----------|------|--------|-------------|
+| `type` | `'success' \| 'error' \| 'warning' \| 'info'` | `'info'` | Variante visuelle |
+| `message` | `string` | — | Corps du toast |
+| `title` | `string` | Selon `type` | Titre affiché (optionnel) |
+| `duration` | `number` | `5000` | Durée en ms avant disparition automatique. `0` pour désactiver |
+| `dismissible` | `boolean` | `true` | Afficher le bouton de fermeture |
+
+Durées par défaut selon le type : `error` → 7000 ms, `warning` → 6000 ms, `success` / `info` → 5000 ms.
+
+**`removeToast(id)`**
+
+Supprime un toast immédiatement par son `id`.
+
+**`clearAllToasts()`**
+
+Supprime tous les toasts actifs.
+
+---
+
+## ToastContainer
+
+Composant à placer une seule fois dans le layout. Affiche les toasts en bas à droite de l'écran, empilés avec une animation de survol.
+
+```jsx
+
+```
+
+| Prop | Type | Défaut | Description |
+|------|------|--------|-------------|
+| `maxToasts` | `number` | `5` | Nombre maximum de toasts visibles simultanément |
+
+Au survol du toast le plus récent, la pile se déploie pour afficher tous les toasts à leur taille réelle.
+
+---
+
+## Déclencher un toast depuis une feature
+
+```js
+// src/features/auth/actions/login.js
+import { useToast } from '@zen/core/toast';
+
+export function useLoginActions() {
+ const { success, error } = useToast();
+
+ async function login(credentials) {
+ const result = await loginRequest(credentials);
+ if (result.success) {
+ success('Connexion réussie.');
+ } else {
+ error('Identifiants incorrects.');
+ }
+ }
+
+ return { login };
+}
+```
diff --git a/src/core/users/README.md b/src/core/users/README.md
new file mode 100644
index 0000000..3241c65
--- /dev/null
+++ b/src/core/users/README.md
@@ -0,0 +1,275 @@
+# Users
+
+Ce répertoire gère les utilisateurs, l'authentification par identifiants, les sessions, les rôles et les permissions. Il constitue la couche de données auth du projet : les features l'appellent, il ne connaît pas les features.
+
+---
+
+## Structure
+
+```
+src/core/users/
+├── index.js re-exports publics
+├── auth.js register, login, mot de passe, vérification email
+├── session.js création, validation, suppression de sessions
+├── queries.js lecture et mise à jour des utilisateurs
+├── roles.js CRUD des rôles, assignation aux utilisateurs
+├── permissions.js hasPermission, getUserPermissions
+├── constants.js PERMISSIONS, PERMISSION_DEFINITIONS, getPermissionGroups
+├── verifications.js tokens de vérification email et de réinitialisation
+├── emailChange.js tokens de changement d'adresse email
+├── password.js hashPassword, verifyPassword, generateToken, generateId
+└── db.js helpers internes
+```
+
+---
+
+## Import
+
+```js
+import {
+ register, login, requestPasswordReset, resetPassword, verifyUserEmail, updateUser,
+ createSession, validateSession, deleteSession, deleteUserSessions, refreshSession,
+ getUserById, getUserByEmail, countUsers, listUsers, updateUserById,
+ createRole, updateRole, deleteRole, listRoles, getRoleById,
+ getUserRoles, assignUserRole, revokeUserRole,
+ hasPermission, getUserPermissions,
+ PERMISSIONS, PERMISSION_DEFINITIONS, getPermissionGroups,
+ hashPassword, verifyPassword, generateToken, generateId,
+} from '@zen/core/users';
+```
+
+---
+
+## API
+
+### `register(userData, options?)`
+
+Crée un compte utilisateur avec vérification des contraintes mot de passe. Le premier utilisateur enregistré reçoit le rôle `admin`. Retourne `{ user, verificationToken }`.
+
+```js
+const { user, verificationToken } = await register(
+ { email: 'alice@example.com', password: 'Secret1', name: 'Alice' },
+ {
+ onEmailVerification: async (email, token) => {
+ await sendVerificationEmail({ to: email, token });
+ },
+ }
+);
+```
+
+| Paramètre | Type | Description |
+|-----------|------|-------------|
+| `email` | `string` | Adresse email (max 254 caractères) |
+| `password` | `string` | Mot de passe (8-128 caractères, au moins 1 majuscule, 1 minuscule, 1 chiffre) |
+| `name` | `string` | Nom affiché (max 100 caractères) |
+| `onEmailVerification` | `async (email, token) => void` | Callback pour envoyer le token de vérification |
+
+---
+
+### `login(credentials, sessionOptions?)`
+
+Vérifie les identifiants et crée une session. Retourne `{ user, session }`. Lève une erreur si les identifiants sont incorrects.
+
+```js
+const { user, session } = await login(
+ { email: 'alice@example.com', password: 'Secret1' },
+ { ipAddress: req.ip, userAgent: req.headers['user-agent'] }
+);
+```
+
+---
+
+### `requestPasswordReset(email)`
+
+Génère un token de réinitialisation (expire dans 1 heure). Retourne `{ success: true, token }` même si l'email est inconnu, pour éviter l'énumération.
+
+```js
+const { token } = await requestPasswordReset('alice@example.com');
+```
+
+---
+
+### `resetPassword(data, options?)`
+
+Valide le token et met à jour le mot de passe. Retourne `{ success: true }`.
+
+```js
+await resetPassword(
+ { email: 'alice@example.com', token, newPassword: 'NewSecret1' },
+ { onPasswordChanged: async (email) => { /* envoyer confirmation */ } }
+);
+```
+
+---
+
+### `verifyUserEmail(userId)`
+
+Marque l'email de l'utilisateur comme vérifié.
+
+```js
+await verifyUserEmail(user.id);
+```
+
+---
+
+### `updateUser(userId, data)`
+
+Met à jour les champs autorisés du profil : `name`, `image`, `language`.
+
+```js
+await updateUser(user.id, { name: 'Alice Martin' });
+```
+
+---
+
+### `createSession(userId, options?)`
+
+Crée une session valide 30 jours. Retourne l'objet session.
+
+```js
+const session = await createSession(user.id, { ipAddress: '127.0.0.1', userAgent: '...' });
+```
+
+---
+
+### `validateSession(token)`
+
+Valide un token de session. Renouvelle automatiquement la session si elle expire dans moins de 20 jours. Retourne `{ session, user, sessionRefreshed }` ou `null`.
+
+```js
+const result = await validateSession(token);
+if (!result) {
+ // session expirée ou invalide
+}
+```
+
+---
+
+### `deleteSession(token)` / `deleteUserSessions(userId)`
+
+Supprime une session ou toutes les sessions d'un utilisateur.
+
+```js
+await deleteSession(token);
+await deleteUserSessions(user.id);
+```
+
+---
+
+### `getUserById(id)` / `getUserByEmail(email)`
+
+Récupère un utilisateur par son id ou son email.
+
+```js
+const user = await getUserById('abc123');
+const user = await getUserByEmail('alice@example.com');
+```
+
+---
+
+### `listUsers(options?)`
+
+Liste les utilisateurs avec pagination et tri. Retourne `{ users, pagination }`.
+
+```js
+const { users, pagination } = await listUsers({ page: 1, limit: 20, sortBy: 'created_at', sortOrder: 'desc' });
+```
+
+| Paramètre | Défaut | Description |
+|-----------|--------|-------------|
+| `page` | `1` | Page courante |
+| `limit` | `10` | Résultats par page (max 100) |
+| `sortBy` | `'created_at'` | Colonne de tri (`id`, `email`, `name`, `role`, `email_verified`, `created_at`) |
+| `sortOrder` | `'desc'` | `'asc'` ou `'desc'` |
+
+---
+
+### `updateUserById(id, fields)`
+
+Met à jour les champs autorisés d'un utilisateur : `name`, `role`, `email_verified`, `image`, `language`.
+
+```js
+await updateUserById(user.id, { role: 'editor', email_verified: true });
+```
+
+---
+
+### Rôles
+
+```js
+const roles = await listRoles();
+const role = await getRoleById(id);
+
+const role = await createRole({ name: 'Éditeur', description: 'Peut publier du contenu', color: '#3b82f6' });
+
+await updateRole(roleId, {
+ name: 'Éditeur senior',
+ permissionKeys: [PERMISSIONS.CONTENT_EDIT, PERMISSIONS.CONTENT_PUBLISH],
+});
+
+await deleteRole(roleId); // impossible sur les rôles système
+
+const userRoles = await getUserRoles(userId);
+await assignUserRole(userId, roleId);
+await revokeUserRole(userId, roleId);
+```
+
+Les rôles système (`is_system = true`) ne peuvent pas être renommés ni supprimés.
+
+---
+
+### Permissions
+
+```js
+const canEdit = await hasPermission(userId, PERMISSIONS.CONTENT_EDIT);
+const keys = await getUserPermissions(userId);
+```
+
+`PERMISSIONS` contient toutes les clés disponibles. `PERMISSION_DEFINITIONS` expose le label, la description et le groupe de chaque permission. `getPermissionGroups()` retourne les permissions regroupées par `group_name`.
+
+| Groupe | Clés |
+|--------|------|
+| Administration | `admin.access` |
+| Contenu | `content.view`, `content.create`, `content.edit`, `content.delete`, `content.publish` |
+| Médias | `media.view`, `media.upload`, `media.delete` |
+| Utilisateurs | `users.view`, `users.edit`, `users.delete` |
+| Rôles | `roles.view`, `roles.manage` |
+| Paramètres | `settings.view`, `settings.manage` |
+
+---
+
+### Changement d'email
+
+```js
+import { createEmailChangeToken, verifyEmailChangeToken, applyEmailChange } from '@zen/core/users/emailChange';
+
+const token = await createEmailChangeToken(userId, 'new@example.com');
+
+// Plus tard, lors de la confirmation :
+const result = await verifyEmailChangeToken(token);
+if (result) {
+ await applyEmailChange(result.userId, result.newEmail);
+}
+```
+
+Le token expire dans 24 heures. `verifyEmailChangeToken` retourne `null` si le token est invalide ou expiré.
+
+---
+
+## Gestion des erreurs
+
+`register`, `login`, `resetPassword` lèvent des erreurs typées (`Error`) avec des messages en français. Les fonctions de requête (`getUserById`, etc.) retournent `null` si l'entrée n'existe pas. Les callbacks `onEmailVerification` et `onPasswordChanged` sont exécutés sans bloquer le flux principal : une erreur dans le callback est loguée mais n'interrompt pas l'opération.
+
+---
+
+## Tables utilisées
+
+| Table | Description |
+|-------|-------------|
+| `zen_auth_users` | Comptes utilisateurs |
+| `zen_auth_accounts` | Identifiants par provider (`credential`) |
+| `zen_auth_sessions` | Sessions actives |
+| `zen_auth_verifications` | Tokens de vérification email, reset mot de passe, changement email |
+| `zen_auth_roles` | Rôles |
+| `zen_auth_role_permissions` | Permissions associées aux rôles |
+| `zen_auth_user_roles` | Rôles assignés aux utilisateurs |