feat(irpf): architecture enrichie — 9 sections, composants visuels, workflow v2

- 6 composants Astro : Structurel, Politique, Conjoncturel, Kpi, Sondage, Timeline
- content.config.ts : schéma enrichi (kpi_*, opinion_*, sections, rang_eu)
- [slug].astro : nav dynamique, header métriques, sidebar TOC, typographie complète
- _TEMPLATE.mdx : template canonique 9 sections pour chaque fiche
- workflows/build-theme.js : prompt deep-research complet — historique, opinion,
  acteurs, 3 couches d'enjeux, benchmarks pays, propositions IRPF fondées sur
  littérature internationale, lecture politique, MDX complet avec composants

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marc Reverdin 2026-06-06 13:01:02 +02:00
parent e32a3a32fe
commit e8285bd1ff
12 changed files with 1390 additions and 345 deletions

View file

@ -0,0 +1,12 @@
---
// Couche 3 — Conjoncturel : actualité des 6 derniers mois, décisions récentes, polémiques
---
<div style="border-left: 4px solid #d97706; background: #fffbeb; padding: 1.5rem 1.75rem; margin: 2rem 0; border-radius: 0 0.5rem 0.5rem 0;">
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem;">
<span style="background: #d97706; color: white; font-size: 0.65rem; font-weight: 700; padding: 0.25rem 0.625rem; border-radius: 0.25rem; letter-spacing: 0.08em; text-transform: uppercase;">Conjoncturel</span>
<span style="font-size: 0.8rem; color: #475569; font-style: italic;">Actualité des 6 derniers mois — décisions, polémiques, sondages récents</span>
</div>
<div class="layer-content">
<slot />
</div>
</div>

35
src/components/Kpi.astro Normal file
View file

@ -0,0 +1,35 @@
---
interface Props {
valeur: string;
label: string;
source?: string;
annee?: string | number;
tendance?: 'hausse' | 'baisse' | 'stable' | 'critique' | 'alerte';
contexte?: string;
}
const { valeur, label, source, annee, tendance = 'stable', contexte } = Astro.props;
const tendanceConfig: Record<string, { icon: string; couleur: string }> = {
hausse: { icon: '↑', couleur: '#16a34a' },
baisse: { icon: '↓', couleur: '#dc2626' },
stable: { icon: '→', couleur: '#6b7280' },
critique: { icon: '⚠', couleur: '#dc2626' },
alerte: { icon: '!', couleur: '#d97706' },
};
const cfg = tendanceConfig[tendance] ?? tendanceConfig.stable;
---
<div style="display: inline-flex; flex-direction: column; background: white; border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 1rem 1.25rem; margin: 0.4rem; min-width: 140px; text-align: center; vertical-align: top;">
<div style="display: flex; align-items: baseline; justify-content: center; gap: 0.3rem; line-height: 1;">
<span style="font-size: 1.75rem; font-weight: 700; color: #0f172a;">{valeur}</span>
<span style={`font-size: 1rem; font-weight: 600; color: ${cfg.couleur};`}>{cfg.icon}</span>
</div>
<div style="font-size: 0.78rem; font-weight: 500; color: #374151; margin-top: 0.35rem; line-height: 1.3;">{label}</div>
{contexte && <div style="font-size: 0.72rem; color: #6b7280; margin-top: 0.2rem; line-height: 1.3;">{contexte}</div>}
{source && (
<div style="font-size: 0.65rem; color: #9ca3af; margin-top: 0.4rem; border-top: 1px solid #f3f4f6; padding-top: 0.35rem;">
{source}{annee ? ` · ${annee}` : ''}
</div>
)}
</div>

View file

@ -0,0 +1,12 @@
---
// Couche 2 — Dimension politique : ce qui fait débat selon les valeurs et les intérêts
---
<div style="border-left: 4px solid #7c3aed; background: #faf5ff; padding: 1.5rem 1.75rem; margin: 2rem 0; border-radius: 0 0.5rem 0.5rem 0;">
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem;">
<span style="background: #7c3aed; color: white; font-size: 0.65rem; font-weight: 700; padding: 0.25rem 0.625rem; border-radius: 0.25rem; letter-spacing: 0.08em; text-transform: uppercase;">Politique</span>
<span style="font-size: 0.8rem; color: #475569; font-style: italic;">Ce qui fait débat selon les valeurs et les intérêts</span>
</div>
<div class="layer-content">
<slot />
</div>
</div>

View file

@ -0,0 +1,45 @@
---
interface Props {
question: string;
pct: number;
reponse?: string;
source: string;
annee: number | string;
note?: string;
couleur?: string;
}
const {
question,
pct,
reponse = "Oui / D'accord",
source,
annee,
note,
couleur = '#1e3a6e',
} = Astro.props;
const largeur = Math.min(Math.max(pct, 3), 100);
---
<div style="background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 1.25rem 1.5rem; margin: 1rem 0;">
<div style="font-size: 0.875rem; color: #374151; margin-bottom: 1rem; line-height: 1.6; font-style: italic;">
« {question} »
</div>
<div style="display: flex; align-items: center; gap: 1rem;">
<div style="flex: 1; background: #e5e7eb; border-radius: 999px; height: 10px; overflow: hidden;">
<div style={`width: ${largeur}%; background: ${couleur}; border-radius: 999px; height: 10px;`}></div>
</div>
<div style="font-size: 1.75rem; font-weight: 700; color: #0f172a; white-space: nowrap; min-width: 60px; text-align: right;">
{pct}<span style="font-size: 1rem;">%</span>
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem;">
<div style="font-size: 0.75rem; color: #6b7280;">{reponse}</div>
<div style="font-size: 0.7rem; color: #9ca3af; font-weight: 500;">{source} · {annee}</div>
</div>
{note && (
<div style="font-size: 0.72rem; color: #6b7280; margin-top: 0.6rem; padding-top: 0.6rem; border-top: 1px dashed #e5e7eb; font-style: italic;">
{note}
</div>
)}
</div>

View file

@ -0,0 +1,12 @@
---
// Couche 1 — Facteurs structurels : contraintes de long terme, invariants institutionnels
---
<div style="border-left: 4px solid #1e3a6e; background: #f0f4ff; padding: 1.5rem 1.75rem; margin: 2rem 0; border-radius: 0 0.5rem 0.5rem 0;">
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem;">
<span style="background: #1e3a6e; color: white; font-size: 0.65rem; font-weight: 700; padding: 0.25rem 0.625rem; border-radius: 0.25rem; letter-spacing: 0.08em; text-transform: uppercase;">Structurel</span>
<span style="font-size: 0.8rem; color: #475569; font-style: italic;">Ce qui ne change pas facilement — contraintes de fond</span>
</div>
<div class="layer-content">
<slot />
</div>
</div>

View file

@ -0,0 +1,43 @@
---
export interface TimelineItem {
annee: string | number;
titre: string;
description: string;
impact?: 'positif' | 'negatif' | 'neutre' | 'majeur';
}
interface Props {
items: TimelineItem[];
}
const { items } = Astro.props;
const impactConfig: Record<string, { couleur: string; fond: string }> = {
positif: { couleur: '#16a34a', fond: '#f0fdf4' },
negatif: { couleur: '#dc2626', fond: '#fef2f2' },
majeur: { couleur: '#d97706', fond: '#fffbeb' },
neutre: { couleur: '#6b7280', fond: '#f9fafb' },
};
---
<div style="position: relative; padding-left: 2.5rem; margin: 1.5rem 0;">
<!-- Ligne verticale -->
<div style="position: absolute; left: 0.75rem; top: 0.5rem; bottom: 0.5rem; width: 2px; background: linear-gradient(to bottom, #e5e7eb, #e5e7eb);"></div>
{items.map((item) => {
const cfg = impactConfig[item.impact ?? 'neutre'];
return (
<div style="position: relative; margin-bottom: 1.5rem; padding-bottom: 0.5rem;">
<!-- Point sur la ligne -->
<div style={`position: absolute; left: -1.75rem; top: 0.2rem; width: 0.875rem; height: 0.875rem; border-radius: 50%; background: ${cfg.couleur}; border: 2px solid white; box-shadow: 0 0 0 2px ${cfg.couleur};`}></div>
<div style={`background: ${cfg.fond}; border: 1px solid #e5e7eb; border-radius: 0.375rem; padding: 0.875rem 1rem;`}>
<div style="display: flex; align-items: baseline; gap: 0.75rem; margin-bottom: 0.3rem; flex-wrap: wrap;">
<span style={`font-size: 0.75rem; font-weight: 700; color: ${cfg.couleur}; white-space: nowrap; font-variant-numeric: tabular-nums;`}>{item.annee}</span>
<span style="font-size: 0.875rem; font-weight: 600; color: #0f172a; line-height: 1.4;">{item.titre}</span>
</div>
<p style="font-size: 0.8rem; color: #4b5563; margin: 0; line-height: 1.55;">{item.description}</p>
</div>
</div>
);
})}
</div>

View file

@ -9,8 +9,38 @@ const themesCollection = defineCollection({
description: z.string(), description: z.string(),
ordre: z.number(), ordre: z.number(),
priorite: z.enum(['critique', 'haute', 'normale']).default('normale'), priorite: z.enum(['critique', 'haute', 'normale']).default('normale'),
// Budget & effectifs
budget_etat_milliards: z.number().optional(), budget_etat_milliards: z.number().optional(),
effectifs_publics: z.number().optional(), effectifs_publics: z.number().optional(),
// KPI principal affiché dans le header
kpi_valeur: z.string().optional(),
kpi_label: z.string().optional(),
kpi_tendance: z.enum(['hausse', 'baisse', 'stable', 'critique', 'alerte']).optional(),
kpi_contexte: z.string().optional(),
// Opinion publique (1 chiffre clé pour la sidebar)
opinion_pct: z.number().optional(),
opinion_question: z.string().optional(),
opinion_source: z.string().optional(),
opinion_annee: z.number().optional(),
// Comparaison internationale
rang_eu: z.number().optional(),
nb_pays_compares: z.number().optional(),
// Sections présentes — pilote la nav
sections: z.array(z.enum([
'diagnostic', 'historique', 'enjeux', 'international',
'opinion', 'acteurs', 'propositions', 'scenarios', 'sources',
])).default(['diagnostic', 'enjeux', 'propositions', 'scenarios', 'sources']),
// Qualité
score_sources: z.number().optional(),
nb_propositions: z.number().optional(),
// Tags & sources
tags: z.array(z.string()).default([]), tags: z.array(z.string()).default([]),
sources_data: z.array(z.string()).default([]), sources_data: z.array(z.string()).default([]),
derniere_maj: z.string(), derniere_maj: z.string(),
@ -24,7 +54,7 @@ const propositionsCollection = defineCollection({
titre: z.string(), titre: z.string(),
theme: z.string(), theme: z.string(),
source: z.string(), source: z.string(),
type_source: z.enum(['parti', 'think-tank', 'rapport-officiel', 'programme-presidentiel', 'rapport-parlementaire']), type_source: z.enum(['parti', 'think-tank', 'rapport-officiel', 'programme-presidentiel', 'rapport-parlementaire', 'irpf']),
orientation: z.enum(['gauche', 'centre-gauche', 'centre', 'centre-droit', 'droite', 'autre']).optional(), orientation: z.enum(['gauche', 'centre-gauche', 'centre', 'centre-droit', 'droite', 'autre']).optional(),
annee: z.number(), annee: z.number(),
horizon: z.enum(['court-terme', 'moyen-terme', 'long-terme']).default('moyen-terme'), horizon: z.enum(['court-terme', 'moyen-terme', 'long-terme']).default('moyen-terme'),
@ -52,7 +82,7 @@ const acteursCollection = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/acteurs' }), loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/acteurs' }),
schema: z.object({ schema: z.object({
nom: z.string(), nom: z.string(),
type: z.enum(['think-tank', 'institution-publique', 'parti-politique', 'organisation-internationale', 'media']), type: z.enum(['think-tank', 'institution-publique', 'parti-politique', 'organisation-internationale', 'media', 'syndicat', 'federation']),
orientation: z.enum(['gauche', 'centre-gauche', 'centre', 'centre-droit', 'droite', 'neutre']).optional(), orientation: z.enum(['gauche', 'centre-gauche', 'centre', 'centre-droit', 'droite', 'neutre']).optional(),
specialites: z.array(z.string()).default([]), specialites: z.array(z.string()).default([]),
site: z.string().url().optional(), site: z.string().url().optional(),

View file

@ -0,0 +1,316 @@
---
title: "Titre du thème"
slug: "slug-du-theme"
description: "Résumé en 1-2 phrases — situation, enjeux principaux."
ordre: 99
priorite: haute
# Budget & effectifs
budget_etat_milliards: 0
effectifs_publics: 0
# KPI affiché dans le header
kpi_valeur: "XX%"
kpi_label: "Indicateur principal"
kpi_tendance: stable # hausse | baisse | stable | critique | alerte
kpi_contexte: "vs. UE : XX%"
# Opinion (1 chiffre clé)
opinion_pct: 0
opinion_question: "Question posée aux Français"
opinion_source: "Eurobaromètre"
opinion_annee: 2024
# Comparaison internationale
rang_eu: 0
nb_pays_compares: 27
# Sections disponibles → pilote la nav
sections:
- diagnostic
- historique
- enjeux
- international
- opinion
- acteurs
- propositions
- scenarios
- sources
# Qualité
score_sources: 0
nb_propositions: 0
# Tags & sources
tags: []
sources_data: []
derniere_maj: "2026-06"
---
import Structurel from '../../components/Structurel.astro';
import Politique from '../../components/Politique.astro';
import Conjoncturel from '../../components/Conjoncturel.astro';
import Kpi from '../../components/Kpi.astro';
import Sondage from '../../components/Sondage.astro';
import Timeline from '../../components/Timeline.astro';
## Diagnostic {#diagnostic}
[Intro : 2-3 phrases de synthèse — le paradoxe ou la tension centrale du sujet en France]
### Indicateurs clés
<Kpi valeur="XX%" label="Indicateur A" source="INSEE" annee="2024" tendance="baisse" />
<Kpi valeur="XX Md€" label="Indicateur B" source="OCDE" annee="2024" tendance="stable" />
<Kpi valeur="XXe" label="Rang UE" source="Eurostat" annee="2023" tendance="critique" />
### La situation actuelle
[Analyse détaillée, chiffrée, sourcée. 4-6 paragraphes. Chaque chiffre cite sa source entre parenthèses italiques.]
### Chiffres clés
| Indicateur | France | Référence | Tendance | Source |
|-----------|--------|-----------|---------|--------|
| ... | ... | ... | → | ... |
---
## Historique {#historique}
[Phrase d'intro : pourquoi l'histoire explique la situation actuelle]
### Chronologie des réformes majeures (30 ans)
export const timelineItems = [
{
annee: "1995",
titre: "Exemple réforme",
description: "Description concise de l'impact.",
impact: "neutre"
},
{
annee: "2004",
titre: "Tournant majeur",
description: "Ce qui a changé structurellement.",
impact: "majeur"
},
{
annee: "2017",
titre: "Réforme récente",
description: "Résultats observés.",
impact: "positif"
},
];
<Timeline items={timelineItems} />
### Les tournants décisifs
[Analyse des 2-3 moments qui ont le plus façonné la situation actuelle. Pourquoi ces réformes ont réussi ou échoué.]
---
## Enjeux {#enjeux}
[Intro : les trois couches de lecture du problème]
<Structurel>
### Contraintes de fond
[Ce qui est difficile à changer : démographie, géographie, culture administrative, contraintes constitutionnelles ou européennes, économie politique. Chiffres structurels.]
- **Contrainte 1** : ...
- **Contrainte 2** : ...
- **Invariant** : ...
</Structurel>
<Politique>
### Ce qui fait débat
[Les fractures politiques réelles sur ce sujet. Positions des 6 familles : gauche (LFI/PS), écologistes (EELV), centre (Renaissance/MoDem), droite (LR), extrême droite (RN), souverainistes. Votes parlementaires récents.]
| Famille | Position | Argument central | Textes récents |
|---------|---------|-----------------|---------------|
| Gauche (LFI/PS) | ... | ... | ... |
| Écologistes | ... | ... | ... |
| Centre (Renaissance) | ... | ... | ... |
| Droite (LR) | ... | ... | ... |
| RN | ... | ... | ... |
</Politique>
<Conjoncturel>
### Actualité (6 derniers mois)
[Décisions gouvernementales, textes en cours, polémiques, derniers sondages. Référence aux événements de fin 2025 / début 2026.]
- **[Mois AAAA]** : ...
- **[Mois AAAA]** : ...
- **Texte en cours** : ...
</Conjoncturel>
---
## International {#international}
### Comparaison UE & OCDE
| Pays | Indicateur principal | [Indicateur 2] | [Indicateur 3] | Note |
|------|---------------------|---------------|---------------|------|
| **France** | **XX** | **XX** | **XX** | Situation spécifique |
| Allemagne | XX | XX | XX | — |
| Suède | XX | XX | XX | Modèle de référence |
| Espagne | XX | XX | XX | — |
| Moyenne UE | XX | XX | XX | — |
### Ce que recommandent les institutions
**OCDE :** [Recommandation principale, numéro de rapport, année]
**Commission européenne :** [Position / Country Report France 2024]
**FMI :** [Recommandation Article IV 2024]
**Cour des comptes :** [Recommandation rapport récent]
**France Stratégie :** [Recommandation publication récente]
### Bonnes pratiques — ce qui fonctionne ailleurs
**[Pays modèle 1] — [Mesure]** : [Description et résultats mesurés]
**[Pays modèle 2] — [Mesure]** : [Description et résultats mesurés]
---
## Opinion {#opinion}
### Ce qu'en pensent les Français
<Sondage
question="[Question posée dans le sondage]"
pct={73}
reponse="Oui / D'accord / Satisfait"
source="Eurobaromètre"
annee={2024}
note="UE27 : XX% · Écart France/UE : +X points"
/>
<Sondage
question="[Deuxième question — confiance dans les institutions]"
pct={45}
reponse="Fait confiance"
source="CEVIPOF Baromètre de la confiance"
annee={2025}
/>
### Évolution dans le temps
[Analyse de la tendance : est-ce que l'opinion s'améliore ou se dégrade ? Depuis quand ? Quels événements ont influencé l'opinion ?]
| Année | % satisfaction | Source |
|-------|---------------|--------|
| 2018 | XX% | ... |
| 2020 | XX% | ... |
| 2022 | XX% | ... |
| 2024 | XX% | ... |
### Clivages dans l'opinion
[Différences significatives par âge, revenu, territoire, orientation politique]
---
## Acteurs {#acteurs}
### Institutions décisionnaires
| Institution | Rôle | Tutelle | Budget | Responsable |
|------------|------|---------|--------|------------|
| Ministère ... | ... | Premier ministre | XX Md€ | [Ministre] |
| [Agence/établissement] | ... | [Ministère] | XX M€ | ... |
### Think tanks — par orientation
| Think tank | Orientation | Spécialité | Publications récentes |
|-----------|------------|-----------|----------------------|
| Institut Montaigne | Centre-droit | ... | [Titre rapport, 2024] |
| Terra Nova | Centre-gauche | ... | [Titre rapport, 2024] |
| iFRAP | Libéral | ... | [Titre rapport, 2024] |
| Fondapol | Centre-droit | ... | ... |
| OFCE | Centre-gauche | ... | ... |
| CAE | Neutre/expert | ... | ... |
### Syndicats, fédérations, organisations professionnelles
[Les acteurs intermédiaires qui pèsent sur les décisions dans ce secteur]
---
## Propositions {#propositions}
### Comparatif des positions partisanes
| Parti | Proposition principale | Coût estimé | Horizon | Cohérence |
|-------|----------------------|------------|---------|-----------|
| LFI | ... | XX Md€ | CT | ... |
| PS | ... | XX Md€ | MT | ... |
| EELV | ... | XX Md€ | LT | ... |
| Renaissance | ... | XX Md€ | MT | ... |
| LR | ... | XX Md€ | MT | ... |
| RN | ... | XX Md€ | CT | ... |
### Recommandations institutionnelles
[Synthèse des recommandations OCDE + CE + Cour des comptes + France Stratégie, classées par faisabilité politique]
### Propositions IRPF
> **Notre analyse** : [1 paragraphe de cadrage — pourquoi ces propositions, quelle philosophie]
**Proposition 1 — [Titre court]**
[Description, impact attendu, coût, horizon, conditions de réussite]
**Proposition 2 — [Titre court]**
[Description, impact attendu, coût, horizon, conditions de réussite]
**Proposition 3 — [Titre court]**
[Description, impact attendu, coût, horizon, conditions de réussite]
---
## Scénarios 20272032 {#scenarios}
### Scénario A — Continuité (probabilité : XX%)
**Hypothèse** : [statu quo politique]
**Impact** : [dégradation / stabilisation]
**Risques** : ...
### Scénario B — Réforme graduelle (probabilité : XX%)
**Hypothèse** : [coalition + X réformes adoptées]
**Impact** : [amélioration mesurable]
**Conditions** : ...
### Scénario C — Rupture (probabilité : XX%)
**Hypothèse** : [changement de majorité / crise déclenchante]
**Impact** : [transformation structurelle]
**Conditions** : ...
---
## Sources {#sources}
### Sources primaires
- [Auteur, *Titre*, Institution, Année. URL]
### Sources secondaires
- [...]
### Données
- [Base de données, institution, accès]

View file

@ -88,7 +88,7 @@ const rapports = (await getCollection('rapports')).slice(0, 3);
</div> </div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;"> <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;">
{themes.slice(0, 6).map((theme) => ( {themes.slice(0, 6).map((theme) => (
<a href={`/themes/${theme.slug}/`} style="background: white; border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 1.5rem; text-decoration: none; display: block;" class="theme-card"> <a href={`/themes/${theme.data.slug}/`} style="background: white; border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 1.5rem; text-decoration: none; display: block;" class="theme-card">
<div style="font-weight: 600; color: #0f172a; margin-bottom: 0.5rem;">{theme.data.title}</div> <div style="font-weight: 600; color: #0f172a; margin-bottom: 0.5rem;">{theme.data.title}</div>
<p style="font-size: 0.85rem; color: #6b7280; line-height: 1.5; margin-bottom: 1rem;">{theme.data.description}</p> <p style="font-size: 0.85rem; color: #6b7280; line-height: 1.5; margin-bottom: 1rem;">{theme.data.description}</p>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;"> <div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">

View file

@ -16,17 +16,44 @@ const { Content } = await render(theme);
const allPropositions = await getCollection('propositions'); const allPropositions = await getCollection('propositions');
const propositions = allPropositions.filter(p => p.data.theme === theme.data.slug); const propositions = allPropositions.filter(p => p.data.theme === theme.data.slug);
// Sections configurées dans le frontmatter → nav dynamique
const SECTION_LABELS: Record<string, string> = {
diagnostic: 'Diagnostic',
historique: 'Historique',
enjeux: 'Enjeux',
international: 'International',
opinion: 'Opinion',
acteurs: 'Acteurs',
propositions: 'Propositions',
scenarios: 'Scénarios',
sources: 'Sources',
};
const navSections = theme.data.sections.map(s => ({
id: s,
label: SECTION_LABELS[s] ?? s,
}));
function orientationBadge(o?: string) { function orientationBadge(o?: string) {
const map: Record<string, string> = { const map: Record<string, string> = {
'gauche': 'background:#fee2e2;color:#991b1b', 'gauche': 'background:#fee2e2;color:#991b1b',
'centre-gauche': 'background:#fce7f3;color:#9d174d', 'centre-gauche': 'background:#fce7f3;color:#9d174d',
'centre': 'background:#dbeafe;color:#1e40af', 'centre': 'background:#dbeafe;color:#1e40af',
'centre-droit': 'background:#ede9fe;color:#5b21b6', 'centre-droit': 'background:#ede9fe;color:#5b21b6',
'droite': 'background:#f3e8ff;color:#6b21a8', 'droite': 'background:#f3e8ff;color:#6b21a8',
'neutre': 'background:#f3f4f6;color:#374151', 'neutre': 'background:#f3f4f6;color:#374151',
'irpf': 'background:#0f172a;color:white',
}; };
return map[o ?? 'neutre'] ?? map['neutre']; return map[o ?? 'neutre'] ?? map['neutre'];
} }
function tendanceCouleur(t?: string) {
return { hausse: '#16a34a', baisse: '#dc2626', stable: '#6b7280', critique: '#dc2626', alerte: '#d97706' }[t ?? 'stable'] ?? '#6b7280';
}
function tendanceIcon(t?: string) {
return { hausse: '↑', baisse: '↓', stable: '→', critique: '⚠', alerte: '!' }[t ?? 'stable'] ?? '→';
}
--- ---
<BaseLayout <BaseLayout
@ -45,54 +72,96 @@ function orientationBadge(o?: string) {
</div> </div>
</div> </div>
<!-- Header --> <!-- Header enrichi -->
<section style="background: linear-gradient(135deg, #0f172a, #1e3a6e); color: white; padding: 3rem 1.5rem;"> <section style="background: linear-gradient(135deg, #0f172a 0%, #1e3a6e 100%); color: white; padding: 3rem 1.5rem 2.5rem;">
<div style="max-width: 1200px; margin: 0 auto; display: grid; grid-template-columns: 1fr auto; gap: 2rem; align-items: start;"> <div style="max-width: 1200px; margin: 0 auto;">
<div> <div style="display: grid; grid-template-columns: 1fr auto; gap: 2rem; align-items: start;">
{theme.data.priorite === 'critique' && ( <div>
<span style="background: #ef4444; color: white; font-size: 0.7rem; font-weight: 700; padding: 0.2rem 0.625rem; border-radius: 0.25rem; letter-spacing: 0.05em; margin-bottom: 1rem; display: inline-block;">PRIORITÉ CRITIQUE</span> {theme.data.priorite === 'critique' && (
)} <span style="background: #ef4444; color: white; font-size: 0.65rem; font-weight: 700; padding: 0.2rem 0.625rem; border-radius: 0.25rem; letter-spacing: 0.06em; margin-bottom: 0.875rem; display: inline-block;">PRIORITÉ CRITIQUE</span>
<h1 style="font-family: 'Playfair Display', serif; font-size: 2.5rem; margin-bottom: 1rem;">{theme.data.title}</h1>
<p style="color: #cbd5e1; font-size: 1.05rem; line-height: 1.7; max-width: 700px;">{theme.data.description}</p>
<div style="display: flex; gap: 1rem; margin-top: 1.5rem; flex-wrap: wrap;">
{theme.data.budget_etat_milliards && (
<div style="background: rgba(255,255,255,0.1); padding: 0.5rem 1rem; border-radius: 0.375rem;">
<div style="font-size: 1.25rem; font-weight: 700;">{theme.data.budget_etat_milliards} Md€</div>
<div style="font-size: 0.75rem; color: #93c5fd;">Budget État 2025</div>
</div>
)} )}
<div style="background: rgba(255,255,255,0.1); padding: 0.5rem 1rem; border-radius: 0.375rem;"> {theme.data.priorite === 'haute' && (
<div style="font-size: 1.25rem; font-weight: 700;">{propositions.length}</div> <span style="background: #d97706; color: white; font-size: 0.65rem; font-weight: 700; padding: 0.2rem 0.625rem; border-radius: 0.25rem; letter-spacing: 0.06em; margin-bottom: 0.875rem; display: inline-block;">PRIORITÉ HAUTE</span>
<div style="font-size: 0.75rem; color: #93c5fd;">Propositions recensées</div> )}
<h1 style="font-family: 'Playfair Display', serif; font-size: 2.5rem; font-weight: 700; margin: 0 0 0.875rem; line-height: 1.15;">{theme.data.title}</h1>
<p style="color: #cbd5e1; font-size: 1.05rem; line-height: 1.7; max-width: 680px; margin: 0 0 1.75rem;">{theme.data.description}</p>
<!-- Métriques header -->
<div style="display: flex; gap: 0.875rem; flex-wrap: wrap;">
{theme.data.kpi_valeur && (
<div style="background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.15); padding: 0.75rem 1.25rem; border-radius: 0.5rem; min-width: 130px;">
<div style={`font-size: 1.5rem; font-weight: 700; display: flex; align-items: baseline; gap: 0.3rem; color: white;`}>
{theme.data.kpi_valeur}
<span style={`font-size: 0.9rem; color: ${tendanceCouleur(theme.data.kpi_tendance)};`}>{tendanceIcon(theme.data.kpi_tendance)}</span>
</div>
<div style="font-size: 0.72rem; color: #93c5fd; margin-top: 0.2rem;">{theme.data.kpi_label}</div>
{theme.data.kpi_contexte && <div style="font-size: 0.65rem; color: #64748b; margin-top: 0.15rem;">{theme.data.kpi_contexte}</div>}
</div>
)}
{theme.data.budget_etat_milliards && (
<div style="background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.15); padding: 0.75rem 1.25rem; border-radius: 0.5rem;">
<div style="font-size: 1.5rem; font-weight: 700; color: white;">{theme.data.budget_etat_milliards} Md€</div>
<div style="font-size: 0.72rem; color: #93c5fd; margin-top: 0.2rem;">Budget État 2025</div>
</div>
)}
{theme.data.opinion_pct && (
<div style="background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.15); padding: 0.75rem 1.25rem; border-radius: 0.5rem;">
<div style="font-size: 1.5rem; font-weight: 700; color: white;">{theme.data.opinion_pct}%</div>
<div style="font-size: 0.72rem; color: #93c5fd; margin-top: 0.2rem; max-width: 140px; line-height: 1.3;">{theme.data.opinion_question ?? 'Satisfaction'}</div>
{theme.data.opinion_source && <div style="font-size: 0.65rem; color: #64748b; margin-top: 0.15rem;">{theme.data.opinion_source} · {theme.data.opinion_annee}</div>}
</div>
)}
<div style="background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.15); padding: 0.75rem 1.25rem; border-radius: 0.5rem;">
<div style="font-size: 1.5rem; font-weight: 700; color: white;">{propositions.length}</div>
<div style="font-size: 0.72rem; color: #93c5fd; margin-top: 0.2rem;">Propositions</div>
</div>
{theme.data.score_sources && (
<div style="background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.15); padding: 0.75rem 1.25rem; border-radius: 0.5rem;">
<div style="font-size: 1.5rem; font-weight: 700; color: white;">{theme.data.score_sources}</div>
<div style="font-size: 0.72rem; color: #93c5fd; margin-top: 0.2rem;">Sources vérifiées</div>
</div>
)}
</div> </div>
</div> </div>
</div>
<div style="text-align: right;"> <!-- Meta droite -->
<div style="font-size: 0.75rem; color: #6b7280;">Mis à jour</div> <div style="text-align: right; flex-shrink: 0;">
<div style="font-size: 0.875rem; color: #93c5fd;">{theme.data.derniere_maj}</div> <div style="font-size: 0.7rem; color: #64748b;">Mis à jour</div>
<div style="margin-top: 1rem;"> <div style="font-size: 0.875rem; color: #93c5fd; margin-bottom: 1rem;">{theme.data.derniere_maj}</div>
<div style="font-size: 0.75rem; color: #6b7280; margin-bottom: 0.5rem;">Sources data</div> {theme.data.rang_eu && (
{theme.data.sources_data.map(s => ( <div style="margin-bottom: 1rem;">
<div style="font-size: 0.75rem; color: #93c5fd;">{s}</div> <div style="font-size: 0.7rem; color: #64748b;">Rang UE</div>
))} <div style="font-size: 1.25rem; font-weight: 700; color: white;">{theme.data.rang_eu}<span style="font-size: 0.75rem; color: #64748b;">/{theme.data.nb_pays_compares ?? 27}</span></div>
</div>
)}
<div>
<div style="font-size: 0.7rem; color: #64748b; margin-bottom: 0.4rem;">Sources data</div>
{theme.data.sources_data.slice(0, 6).map(s => (
<div style="font-size: 0.72rem; color: #93c5fd; line-height: 1.6;">{s}</div>
))}
</div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- Nav interne --> <!-- Nav interne dynamique -->
<nav style="background: white; border-bottom: 1px solid #e5e7eb; padding: 0 1.5rem; position: sticky; top: 64px; z-index: 40;"> <nav style="background: white; border-bottom: 2px solid #e5e7eb; padding: 0 1.5rem; position: sticky; top: 0; z-index: 40; box-shadow: 0 1px 3px rgba(0,0,0,0.06);">
<div style="max-width: 1200px; margin: 0 auto; display: flex; gap: 2rem; overflow-x: auto;"> <div style="max-width: 1200px; margin: 0 auto; display: flex; gap: 0; overflow-x: auto; scrollbar-width: none;">
{['Diagnostic', 'Enjeux', 'Propositions', 'Scénarios', 'Sources'].map(anchor => ( {navSections.map(({ id, label }) => (
<a href={`#${anchor.toLowerCase().replace('é','e').replace('s','s')}`} style="padding: 1rem 0; font-size: 0.875rem; font-weight: 500; color: #374151; text-decoration: none; white-space: nowrap; border-bottom: 2px solid transparent;" class="nav-anchor"> <a
{anchor} href={`#${id}`}
style="padding: 0.875rem 1rem; font-size: 0.82rem; font-weight: 500; color: #4b5563; text-decoration: none; white-space: nowrap; border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.15s;"
class="nav-anchor"
>
{label}
</a> </a>
))} ))}
</div> </div>
</nav> </nav>
<!-- Contenu --> <!-- Corps : article + sidebar -->
<div style="max-width: 1200px; margin: 0 auto; padding: 3rem 1.5rem; display: grid; grid-template-columns: 1fr 280px; gap: 3rem; align-items: start;"> <div style="max-width: 1200px; margin: 0 auto; padding: 2.5rem 1.5rem 4rem; display: grid; grid-template-columns: 1fr 300px; gap: 3rem; align-items: start;">
<article> <article>
<div class="prose-irpf"> <div class="prose-irpf">
@ -100,44 +169,173 @@ function orientationBadge(o?: string) {
</div> </div>
</article> </article>
<aside style="position: sticky; top: 120px;"> <!-- Sidebar enrichie -->
<aside style="position: sticky; top: 60px; display: flex; flex-direction: column; gap: 1.25rem;">
<!-- Propositions -->
{propositions.length > 0 && ( {propositions.length > 0 && (
<div style="background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 1.25rem; margin-bottom: 1.5rem;"> <div style="background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 1.25rem;">
<div style="font-weight: 600; font-size: 0.875rem; color: #0f172a; margin-bottom: 1rem;">Propositions recensées ({propositions.length})</div> <div style="font-weight: 600; font-size: 0.82rem; color: #0f172a; margin-bottom: 0.875rem; text-transform: uppercase; letter-spacing: 0.04em;">
<div style="display: flex; flex-direction: column; gap: 0.75rem;"> Propositions · {propositions.length}
{propositions.slice(0, 5).map(p => ( </div>
<div style="display: flex; flex-direction: column; gap: 0.625rem;">
{propositions.slice(0, 6).map(p => (
<div style="background: white; border: 1px solid #e5e7eb; border-radius: 0.375rem; padding: 0.75rem;"> <div style="background: white; border: 1px solid #e5e7eb; border-radius: 0.375rem; padding: 0.75rem;">
<div style="font-size: 0.8rem; font-weight: 500; color: #0f172a; margin-bottom: 0.35rem; line-height: 1.4;">{p.data.titre}</div> <div style="font-size: 0.78rem; font-weight: 500; color: #0f172a; margin-bottom: 0.4rem; line-height: 1.45;">{p.data.titre}</div>
<div style="display: flex; gap: 0.35rem; flex-wrap: wrap; align-items: center;"> <div style="display: flex; gap: 0.35rem; flex-wrap: wrap; align-items: center;">
<span style={`font-size: 0.65rem; padding: 0.15rem 0.4rem; border-radius: 0.2rem; font-weight: 500; ${orientationBadge(p.data.orientation)}`}> <span style={`font-size: 0.62rem; padding: 0.15rem 0.4rem; border-radius: 0.2rem; font-weight: 600; ${orientationBadge(p.data.orientation)}`}>
{p.data.source} {p.data.source}
</span> </span>
<span style="font-size: 0.65rem; color: #9ca3af;">{p.data.annee}</span> <span style="font-size: 0.62rem; color: #9ca3af;">{p.data.horizon} · {p.data.annee}</span>
</div> </div>
</div> </div>
))} ))}
</div> </div>
{propositions.length > 5 && ( {propositions.length > 6 && (
<a href="/propositions/" style="display: block; text-align: center; margin-top: 0.75rem; font-size: 0.8rem; color: #1e3a6e; text-decoration: none;"> <a href="/propositions/" style="display: block; text-align: center; margin-top: 0.75rem; font-size: 0.78rem; color: #1e3a6e; text-decoration: none; font-weight: 500;">
Voir les {propositions.length - 5} autres → + {propositions.length - 6} autres propositions →
</a> </a>
)} )}
</div> </div>
)} )}
<!-- Sections -->
<div style="background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 1.25rem;"> <div style="background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 1.25rem;">
<div style="font-weight: 600; font-size: 0.875rem; color: #0f172a; margin-bottom: 0.75rem;">Mots-clés</div> <div style="font-weight: 600; font-size: 0.82rem; color: #0f172a; margin-bottom: 0.875rem; text-transform: uppercase; letter-spacing: 0.04em;">Contenu</div>
<div style="display: flex; flex-wrap: wrap; gap: 0.4rem;"> <div style="display: flex; flex-direction: column; gap: 0.25rem;">
{theme.data.tags.map(tag => ( {navSections.map(({ id, label }) => (
<span style="background: white; border: 1px solid #e5e7eb; color: #374151; font-size: 0.75rem; padding: 0.25rem 0.625rem; border-radius: 0.25rem;">#{tag}</span> <a href={`#${id}`} style="font-size: 0.8rem; color: #374151; text-decoration: none; padding: 0.3rem 0.5rem; border-radius: 0.25rem; display: block;" class="toc-link">
{label}
</a>
))} ))}
</div> </div>
</div> </div>
<!-- Tags -->
<div style="background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 1.25rem;">
<div style="font-weight: 600; font-size: 0.82rem; color: #0f172a; margin-bottom: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em;">Mots-clés</div>
<div style="display: flex; flex-wrap: wrap; gap: 0.4rem;">
{theme.data.tags.map(tag => (
<span style="background: white; border: 1px solid #e5e7eb; color: #374151; font-size: 0.72rem; padding: 0.2rem 0.5rem; border-radius: 0.2rem;">#{tag}</span>
))}
</div>
</div>
</aside> </aside>
</div> </div>
</BaseLayout> </BaseLayout>
<style> <style>
.nav-anchor:hover { color: #1e3a6e; border-bottom-color: #1e3a6e !important; } .nav-anchor:hover,
.nav-anchor:focus { color: #1e3a6e !important; border-bottom-color: #1e3a6e !important; }
.toc-link:hover { background: #e5e7eb; color: #0f172a !important; }
/* Typographie du corps de fiche */
.prose-irpf h2 {
font-family: 'Playfair Display', serif;
font-size: 1.6rem;
font-weight: 700;
color: #0f172a;
margin: 3rem 0 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #e5e7eb;
scroll-margin-top: 80px;
}
.prose-irpf h3 {
font-size: 1.1rem;
font-weight: 600;
color: #1e3a6e;
margin: 1.75rem 0 0.75rem;
scroll-margin-top: 80px;
}
.prose-irpf h4 {
font-size: 0.95rem;
font-weight: 600;
color: #374151;
margin: 1.25rem 0 0.5rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.prose-irpf p {
font-size: 0.9rem;
line-height: 1.8;
color: #374151;
margin: 0.75rem 0;
}
.prose-irpf strong { color: #0f172a; }
.prose-irpf em { color: #4b5563; }
.prose-irpf table {
width: 100%;
border-collapse: collapse;
margin: 1.25rem 0;
font-size: 0.82rem;
}
.prose-irpf th {
background: #0f172a;
color: white;
padding: 0.6rem 0.875rem;
text-align: left;
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.prose-irpf td {
padding: 0.6rem 0.875rem;
border-bottom: 1px solid #e5e7eb;
color: #374151;
vertical-align: top;
line-height: 1.5;
}
.prose-irpf tr:nth-child(even) td { background: #f8fafc; }
.prose-irpf tr:hover td { background: #f0f4ff; }
.prose-irpf ul, .prose-irpf ol {
padding-left: 1.5rem;
margin: 0.75rem 0;
}
.prose-irpf li {
font-size: 0.88rem;
line-height: 1.75;
color: #374151;
margin-bottom: 0.25rem;
}
.prose-irpf blockquote {
border-left: 4px solid #1e3a6e;
background: #f0f4ff;
padding: 1rem 1.25rem;
margin: 1.5rem 0;
border-radius: 0 0.375rem 0.375rem 0;
font-style: italic;
color: #374151;
}
.prose-irpf blockquote p { margin: 0; }
.prose-irpf a {
color: #1e3a6e;
text-decoration: underline;
text-decoration-color: #bfdbfe;
}
.prose-irpf a:hover { text-decoration-color: #1e3a6e; }
.prose-irpf code {
background: #f1f5f9;
padding: 0.1rem 0.35rem;
border-radius: 0.2rem;
font-size: 0.82rem;
color: #0f172a;
}
/* Couches visuelles : héritage du slot */
.layer-content p { font-size: 0.88rem; }
.layer-content ul { padding-left: 1.25rem; }
.layer-content li { font-size: 0.85rem; }
.layer-content table { font-size: 0.8rem; }
/* Scroll offset pour nav sticky */
[id] { scroll-margin-top: 80px; }
</style> </style>

View file

@ -27,7 +27,7 @@ const autres = themes.filter(t => t.data.priorite !== 'critique');
</div> </div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;"> <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;">
{critiques.map(theme => ( {critiques.map(theme => (
<a href={`/themes/${theme.slug}/`} style="background: white; border: 2px solid #fca5a5; border-radius: 0.5rem; padding: 1.5rem; text-decoration: none; display: block;" class="theme-card"> <a href={`/themes/${theme.data.slug}/`} style="background: white; border: 2px solid #fca5a5; border-radius: 0.5rem; padding: 1.5rem; text-decoration: none; display: block;" class="theme-card">
<div style="font-weight: 700; color: #0f172a; font-size: 1.05rem; margin-bottom: 0.5rem;">{theme.data.title}</div> <div style="font-weight: 700; color: #0f172a; font-size: 1.05rem; margin-bottom: 0.5rem;">{theme.data.title}</div>
<p style="font-size: 0.85rem; color: #6b7280; line-height: 1.5; margin-bottom: 1rem;">{theme.data.description}</p> <p style="font-size: 0.85rem; color: #6b7280; line-height: 1.5; margin-bottom: 1rem;">{theme.data.description}</p>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;"> <div style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
@ -52,7 +52,7 @@ const autres = themes.filter(t => t.data.priorite !== 'critique');
</div> </div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;"> <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;">
{autres.map(theme => ( {autres.map(theme => (
<a href={`/themes/${theme.slug}/`} style="background: white; border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 1.5rem; text-decoration: none; display: block;" class="theme-card"> <a href={`/themes/${theme.data.slug}/`} style="background: white; border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 1.5rem; text-decoration: none; display: block;" class="theme-card">
<div style="font-weight: 600; color: #0f172a; margin-bottom: 0.5rem;">{theme.data.title}</div> <div style="font-weight: 600; color: #0f172a; margin-bottom: 0.5rem;">{theme.data.title}</div>
<p style="font-size: 0.85rem; color: #6b7280; line-height: 1.5; margin-bottom: 1rem;">{theme.data.description}</p> <p style="font-size: 0.85rem; color: #6b7280; line-height: 1.5; margin-bottom: 1rem;">{theme.data.description}</p>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;"> <div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">

File diff suppressed because it is too large Load diff