design : accueil console seul, vrai §, curseurs animés, statuts, lecteur

- accueil : la console devient l'accueil unique (variantes B/C, sélecteur
  et ancien accueil retirés) ; curseurs qui montent de l'asservissement et
  dérivent seuls, un axe mis en avant à tour de rôle, arrêt au clic,
  immobiles en prefers-reduced-motion ; « AUTONOMIE » devant chaque axe,
  leviers mis en valeur, ligne des trois autonomies en couleurs
- logo : § de Playfair Display (SIL OFL) vectorisé, composant partagé
  UiSectionSign (en-tête, page événement) + favicon.svg
- UiStatusBadge (production / développement / gestation) sur l'accueil,
  les pages d'axe et les pages projet
- liens libreDecision / SejeteralO fixés sur la prod (plus de repli localhost)
- logiciel libre : bouton v1 dans la carte projet en tête de page
- couverture du livre : recadrage corrigé (BookSection)
- texte courant en Public Sans (zéros non barrés)
- lecteur : la pastille persistante ne masque plus la navigation du
  BookPlayer ; lecture directe du MP3 (les .ogg n'existent pas : 404)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-09-29 05:59:33 +02:00
co-authored by Claude Opus 5.5
parent 4d8fe4895d
commit 201cfbbb50
33 changed files with 416 additions and 2100 deletions
-379
View File
@@ -1,379 +0,0 @@
<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 }"
>
<!-- Overlay link — full card clickable (z-index 0) -->
<component
:is="itemTag(item)"
v-bind="itemAttrs(item)"
class="axis-item-overlay"
:aria-label="item.label"
tabindex="-1"
/>
<!-- Card body (position relative, z-index 1 — above overlay) -->
<div 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>
</div>
<!-- Actions zone — z-index 1, above overlay, boutons ouvrent liens externes -->
<div v-if="item.actions?.length" class="axis-actions">
<!-- Primary row -->
<div class="axis-actions-row">
<component
:is="action.href ? 'a' : 'button'"
v-for="action in primaryActions(item.actions)"
:key="action.id"
v-bind="action.href ? { href: action.href, target: '_blank', rel: 'noopener noreferrer' } : {}"
class="axis-action-btn"
:class="{ 'axis-action-btn--highlight': action.highlight }"
@click.stop="!action.href && handleAction(action.id)"
>
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
{{ action.label }}
</component>
</div>
<!-- Secondary row -->
<div v-if="secondaryActions(item.actions).length" class="axis-actions-secondary">
<template v-for="action in secondaryActions(item.actions)" :key="action.label">
<NuxtLink
v-if="action.to"
:to="action.to"
class="axis-action-btn axis-action-btn--secondary"
@click.stop
>
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
{{ action.label }}
</NuxtLink>
<button
v-else
class="axis-action-btn axis-action-btn--secondary"
@click.stop="handleAction(action.id)"
>
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
{{ action.label }}
</button>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface AxisAction {
id: string
label: string
icon: string
highlight?: boolean
secondary?: boolean
to?: string
href?: 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;
position: relative;
/* overflow: visible pour laisser le tooltip sortir du cadre */
}
/* Lien overlay — couvre toute la carte, AU-DESSUS du contenu */
.axis-item-overlay {
position: absolute;
inset: 0;
z-index: 1;
border-radius: inherit;
}
.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;
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;
position: relative;
z-index: 2;
}
.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>
-237
View File
@@ -1,237 +0,0 @@
<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>
+3 -3
View File
@@ -89,9 +89,9 @@ const titleLine2 = computed(() => titleParts.value[1])
.book-cover-3d {
width: 100%;
max-width: 360px;
aspect-ratio: 3 / 4;
background-size: 200% auto;
background-position: right center;
aspect-ratio: 1756 / 2480;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
border-radius: 0.75rem;
overflow: hidden;
-120
View File
@@ -1,120 +0,0 @@
<template>
<section class="relative overflow-hidden section-padding hero-section">
<!-- 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-[radial-gradient(ellipse_at_top,hsl(12_76%_48%/0.12),transparent_70%)]" />
<!-- Shadok bird decoration -->
<svg class="shadok-bird" viewBox="0 0 180 260" fill="none" aria-hidden="true">
<ellipse cx="90" cy="100" rx="45" ry="40" fill="currentColor" opacity="0.85"/>
<circle cx="130" cy="60" r="22" fill="currentColor" opacity="0.8"/>
<path d="M110 85 Q125 70 128 63" stroke="currentColor" stroke-width="8" stroke-linecap="round" opacity="0.7" fill="none"/>
<circle cx="136" cy="55" r="5" fill="currentColor" opacity="0.3"/>
<circle cx="137" cy="54" r="2" fill="currentColor" opacity="0.6"/>
<polygon points="150,58 175,50 152,65" fill="currentColor" opacity="0.6"/>
<line x1="75" y1="138" x2="60" y2="230" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="105" y1="138" x2="115" y2="230" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<circle cx="66" cy="190" r="4" fill="currentColor" opacity="0.4"/>
<circle cx="111" cy="190" r="4" fill="currentColor" opacity="0.4"/>
<path d="M60 230 L45 233 M60 230 L55 236 M60 230 L65 235" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.5"/>
<path d="M115 230 L100 233 M115 230 L110 236 M115 230 L120 235" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.5"/>
<path d="M48 95 Q20 80 15 65" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.5" fill="none"/>
<path d="M48 100 Q22 92 10 85" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4" fill="none"/>
<path d="M48 105 Q25 102 12 100" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3" fill="none"/>
</svg>
<!-- Shadok boulanger -->
<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"/>
<circle cx="120" cy="92" r="25" fill="currentColor" opacity="0.8"/>
<ellipse cx="120" cy="68" rx="18" ry="22" fill="currentColor" opacity="0.35"/>
<rect x="105" y="78" width="30" height="5" rx="1" fill="currentColor" opacity="0.4"/>
<path d="M110 88 Q114 84 118 88" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.4"/>
<path d="M124 88 Q128 84 132 88" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.4"/>
<path d="M112 102 Q120 108 128 102" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.35"/>
<line x1="160" y1="145" x2="190" y2="135" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<rect x="185" y="125" width="40" height="10" rx="5" fill="currentColor" opacity="0.4" transform="rotate(-15 205 130)"/>
<line x1="80" y1="148" x2="55" y2="175" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="105" y1="200" x2="95" y2="258" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="135" y1="200" x2="145" y2="258" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<rect x="30" y="180" width="50" height="40" rx="4" fill="currentColor" opacity="0.25"/>
<rect x="35" y="185" width="40" height="20" rx="2" fill="currentColor" opacity="0.15"/>
<ellipse cx="55" cy="195" rx="12" ry="6" fill="currentColor" opacity="0.12"/>
</svg>
<!-- Content -->
<div class="container-content relative z-10 px-4">
<div class="mx-auto max-w-2xl">
<HomeTypewriterText
v-if="hero"
:hero="hero"
/>
</div>
</div>
</section>
</template>
<script setup lang="ts">
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>
<style scoped>
.hero-section {
min-height: 70vh;
display: flex;
align-items: center;
justify-content: center;
}
.shadok-bird {
position: absolute;
right: 4%;
top: 12%;
width: clamp(110px, 15vw, 220px);
opacity: 0.3;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float 8s ease-in-out infinite;
z-index: 1;
}
@keyframes shadok-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
.shadok-boulanger {
position: absolute;
left: 3%;
bottom: 8%;
width: clamp(100px, 13vw, 190px);
opacity: 0.25;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-boulanger 9s ease-in-out infinite;
z-index: 1;
}
@keyframes shadok-float-boulanger {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@media (max-width: 768px) {
.shadok-bird { display: none; }
.shadok-boulanger { display: none; }
}
</style>
+146 -63
View File
@@ -1,17 +1,25 @@
<template>
<div class="console" :class="{ 'console--ready': ready, 'console--compact': compact }">
<div
ref="root"
class="console"
:class="{ 'console--compact': compact, 'console--still': reduced }"
@pointerenter="hovered = true"
@pointerleave="hovered = false"
@focusin="hovered = true"
@focusout="hovered = false"
>
<div class="console-board">
<!-- Scale: from « je subis » to « je m'affranchis » (evenement.yml) -->
<!-- Scale, read bottom to top: from asservissement to émancipation -->
<div class="console-scale" aria-hidden="true">
<span>je m'affranchis</span>
<span class="console-ticks"><i v-for="n in 5" :key="n" /></span>
<span>je subis</span>
<span class="console-scale-bottom">{{ bottomLabel }}</span>
<span class="console-scale-line" />
<span class="console-scale-top">{{ topLabel }}</span>
</div>
<div
class="console-faders"
role="tablist"
aria-label="Les trois axes de l'autonomie"
:aria-label="`Les trois axes — ${bottomLabel} ou ${topLabel}`"
@keydown="onKeydown"
>
<button
@@ -23,17 +31,20 @@
role="tab"
class="fader"
:class="{ 'fader--active': axis.key === active }"
:style="{ '--hue': `var(${axis.hue})`, '--level': level(axis.key), '--i': i }"
:style="{ '--hue': `var(${axis.hue})`, '--level': levels[axis.key] ?? 0, '--i': i }"
:aria-selected="axis.key === active"
:aria-controls="`panel-${uid}`"
:tabindex="axis.key === active ? 0 : -1"
@click="select(axis.key)"
@click="choose(axis.key)"
>
<span class="fader-track" aria-hidden="true">
<span class="fader-fill" />
<span class="fader-cap" />
</span>
<span class="fader-name">{{ axis.short }}</span>
<span class="fader-label">
<span class="fader-prefix">{{ prefix }}</span>
<span class="fader-name">{{ axis.short }}</span>
</span>
<span class="fader-lever">{{ axis.lever }}</span>
</button>
</div>
@@ -61,7 +72,7 @@
<NuxtLink v-if="tool.to" :to="tool.to" class="panel-tool">
<span class="panel-tool-name">{{ tool.label }}</span>
<span v-if="tool.presentation" class="panel-tool-app">{{ tool.presentation.title }}</span>
<span v-if="tool.gestation" class="panel-tool-state">en gestation</span>
<span v-if="tool.status || tool.gestation" class="panel-tool-state">{{ (tool.status || 'En gestation').toLowerCase() }}</span>
</NuxtLink>
</li>
</ul>
@@ -89,6 +100,9 @@ import type { AxisKey, HomeAxis, HomeStack } from '~/composables/useHomeData'
const props = withDefaults(defineProps<{
axes: HomeAxis[]
prefix?: string
topLabel?: string
bottomLabel?: string
upstreamLabel?: string
stacks?: HomeStack[]
stacksLabel?: string
@@ -96,6 +110,9 @@ const props = withDefaults(defineProps<{
maxStacks?: number
compact?: boolean
}>(), {
prefix: 'Autonomie',
topLabel: 'Émancipation',
bottomLabel: 'Asservissement',
maxStacks: 8,
upstreamLabel: 'en amont',
stacksLabel: 'En service',
@@ -104,21 +121,58 @@ const props = withDefaults(defineProps<{
const active = defineModel<AxisKey>({ default: 'numerique' })
const uid = useId()
const root = ref<HTMLElement | null>(null)
const tabs = ref<HTMLButtonElement[]>([])
const ready = ref(false)
const current = computed(() => props.axes.find(a => a.key === active.value) ?? props.axes[0])
// Faders rise from « je subis » once mounted; the selected one is pushed to the top.
function level(key: AxisKey) {
if (!ready.value) return 0
return key === active.value ? 0.94 : 0.3
// ── Motion ──────────────────────────────────────────────────────────────
// The cursors move on their own, slowly: each one drifts, and every few
// beats another axis is pushed up towards émancipation. Visitors watch it;
// they can still pick an axis (click, arrows), which stops the rotation.
// SSR renders every cursor at the bottom, so the first move is the rise.
const BEAT_MS = 2600
const BEATS_PER_AXIS = 3
const levels = reactive<Partial<Record<AxisKey, number>>>({})
const reduced = ref(false)
const hovered = ref(false)
const visible = ref(true)
const chosen = ref(false)
let beat = 0
let timer: ReturnType<typeof setInterval> | undefined
function place() {
props.axes.forEach((axis, i) => {
const isActive = axis.key === active.value
if (reduced.value) {
levels[axis.key] = isActive ? 0.94 : 0.3
return
}
const wave = Math.sin(beat * 0.9 + i * 2.1)
levels[axis.key] = isActive ? 0.87 + 0.06 * wave : 0.3 + 0.14 * wave
})
}
function select(key: AxisKey) {
function advance() {
const idx = props.axes.findIndex(a => a.key === active.value)
active.value = props.axes[(idx + 1) % props.axes.length]!.key
}
function onBeat() {
if (!visible.value) return
beat++
if (!chosen.value && !hovered.value && beat % BEATS_PER_AXIS === 0) advance()
place()
}
function choose(key: AxisKey) {
chosen.value = true
active.value = key
}
watch(active, place)
function onKeydown(e: KeyboardEvent) {
const keys = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'Home', 'End']
if (!keys.includes(e.key)) return
@@ -130,12 +184,23 @@ function onKeydown(e: KeyboardEvent) {
if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') next = idx === 0 ? last : idx - 1
if (e.key === 'Home') next = 0
if (e.key === 'End') next = last
active.value = props.axes[next]!.key
choose(props.axes[next]!.key)
nextTick(() => tabs.value[next]?.focus())
}
onMounted(() => {
requestAnimationFrame(() => { ready.value = true })
reduced.value = window.matchMedia('(prefers-reduced-motion: reduce)').matches
requestAnimationFrame(place)
if (reduced.value) return
timer = setInterval(onBeat, BEAT_MS)
const io = new IntersectionObserver(([entry]) => { visible.value = !!entry?.isIntersecting })
if (root.value) io.observe(root.value)
onUnmounted(() => {
clearInterval(timer)
io.disconnect()
})
})
</script>
@@ -155,7 +220,7 @@ onMounted(() => {
.console-board {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.75rem;
gap: 0.9rem;
padding: 1.25rem 1.25rem 1rem;
border-radius: 16px;
background: hsl(var(--color-surface));
@@ -164,32 +229,34 @@ onMounted(() => {
0 18px 40px -24px hsl(var(--color-text) / 0.35);
}
/* Vertical scale, read from bottom to top */
.console-scale {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
align-items: flex-end;
gap: 0.5rem;
height: var(--track-h);
font-family: var(--font-mono);
font-size: 0.62rem;
line-height: 1.2;
color: hsl(var(--color-text) / 0.45);
text-align: right;
writing-mode: vertical-rl;
transform: rotate(180deg);
font-family: var(--font-display);
font-size: 0.72rem;
letter-spacing: 0.02em;
white-space: nowrap;
}
.console-ticks {
display: flex;
flex-direction: column;
gap: calc(var(--track-h) / 9);
align-items: flex-end;
.console-scale-bottom {
color: hsl(var(--color-text) / 0.45);
}
.console-ticks i {
display: block;
width: 0.5rem;
height: 1px;
background: hsl(var(--color-text) / 0.2);
.console-scale-top {
font-weight: 800;
color: hsl(var(--color-text));
}
.console-scale-line {
flex: 1;
width: 1px;
background: linear-gradient(to bottom, hsl(var(--color-text) / 0.12), hsl(var(--color-text) / 0.45));
}
.console-faders {
@@ -203,15 +270,13 @@ onMounted(() => {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
gap: 0.55rem;
padding: 0 0.25rem 0.25rem;
border-radius: 12px;
cursor: pointer;
transition: background 0.2s ease;
}
.fader:hover {
background: hsl(var(--hue) / 0.06);
/* --level is a registered property (main.css): the cursors glide */
transition: --level 2.6s cubic-bezier(0.45, 0.05, 0.55, 0.95);
transition-delay: calc(var(--i) * 160ms);
}
.fader:focus-visible {
@@ -233,6 +298,7 @@ onMounted(() => {
height: calc(var(--level) * (var(--track-h) - var(--cap-h)) + var(--cap-h) / 2);
border-radius: inherit;
background: hsl(var(--hue) / 0.45);
transition: background 0.6s ease;
}
.fader--active .fader-fill {
@@ -267,25 +333,28 @@ onMounted(() => {
background: hsl(var(--hue));
}
.console--ready .fader-fill,
.console--ready .fader-cap {
transition:
height 0.7s var(--ease-out-expo),
transform 0.7s var(--ease-out-expo);
.fader-label {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.1;
}
/* Orchestrated first rise: one fader after the other */
.console--ready .fader-fill,
.console--ready .fader-cap {
transition-delay: calc(var(--i) * 90ms);
.fader-prefix {
font-family: var(--font-mono);
font-size: 0.6rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: hsl(var(--color-text) / 0.5);
}
.fader-name {
font-family: var(--font-display);
font-weight: 700;
font-size: 0.95rem;
font-weight: 800;
font-size: 1.05rem;
letter-spacing: -0.01em;
color: hsl(var(--color-text) / 0.6);
color: hsl(var(--color-text) / 0.65);
transition: color 0.6s ease;
}
.fader--active .fader-name {
@@ -293,11 +362,21 @@ onMounted(() => {
}
.fader-lever {
font-family: var(--font-mono);
font-size: 0.64rem;
line-height: 1.25;
padding: 0.2rem 0.55rem;
border-radius: 20px;
font-family: var(--font-display);
font-weight: 600;
font-size: 0.8rem;
line-height: 1.2;
text-align: center;
color: hsl(var(--color-text) / 0.45);
color: hsl(var(--color-text) / 0.8);
background: hsl(var(--hue) / 0.1);
transition: background 0.6s ease, color 0.6s ease;
}
.fader--active .fader-lever {
color: hsl(var(--color-bg));
background: hsl(var(--hue));
}
/* ── Panel ── */
@@ -455,15 +534,19 @@ onMounted(() => {
@media (max-width: 480px) {
.console { --track-h: 8.5rem; }
.console-board { padding: 1rem 0.75rem 0.75rem; gap: 0.4rem; }
.console-scale { white-space: normal; max-width: 3.4rem; font-size: 0.56rem; }
.console-board { padding: 1rem 0.6rem 0.75rem; gap: 0.35rem; }
.console-scale { font-size: 0.62rem; }
.fader-cap { width: 2.2rem; margin-left: -1.1rem; }
.fader-name { font-size: 0.85rem; }
.fader-name { font-size: 0.88rem; }
.fader-prefix { font-size: 0.52rem; letter-spacing: 0.08em; }
.fader-lever { font-size: 0.68rem; padding: 0.2rem 0.4rem; }
}
@media (prefers-reduced-motion: reduce) {
.console--ready .fader-fill,
.console--ready .fader-cap,
.fader,
.fader-fill,
.fader-name,
.fader-lever,
.panel-enter-active,
.panel-leave-active {
transition: none;
@@ -9,7 +9,12 @@
<span class="va-h1-a">{{ heading[0] }}</span>
<span class="va-h1-b">{{ heading[1] }}</span>
</h1>
<p class="va-axes">{{ heading[3] }}</p>
<p class="va-axes">
<template v-for="(seg, i) in axesLine" :key="i">
<span v-if="seg.hue" class="va-axes-word" :style="{ '--hue': `var(${seg.hue})` }">{{ seg.text }}</span>
<template v-else>{{ seg.text }}</template>
</template>
</p>
<p class="va-means">{{ heading[4] }}</p>
<p v-if="hero.approach" class="va-approach">{{ hero.approach }}</p>
</div>
@@ -19,6 +24,9 @@
<HomeConsole
v-model="active"
:axes="axes"
:prefix="consoleLabels?.prefix"
:top-label="consoleLabels?.top"
:bottom-label="consoleLabels?.bottom"
:stacks="bouquet?.inService"
:stacks-to="lpk?.to"
:stacks-label="lpk ? `En service chez ${lpk.name} :` : 'En service :'"
@@ -51,7 +59,7 @@
<!-- La boîte à outils -->
<section class="va-section va-section--tools">
<div class="container-content">
<h2 class="va-h2">La boîte à outils</h2>
<h2 class="va-h2">{{ toolboxTitle }}</h2>
<HomeToolbox
:axes="axes"
:lpk="lpk"
@@ -74,8 +82,20 @@ defineEmits<{
'open-pdf': []
}>()
const { hero, axes, lpk, bouquet, evenement, book, format, gratewizard } = await useHomeData()
const { hero, axes, lpk, bouquet, evenement, book, format, gratewizard, consoleLabels, toolboxTitle } = await useHomeData()
const heading = computed<string[]>(() => hero.value.heading ?? [])
// « Autonomie numérique, économique, citoyenne. » — each axis word in its own hue
const AXIS_WORDS: Record<string, string> = {
numérique: '--axis-num',
économique: '--axis-eco',
citoyenne: '--axis-cit',
}
const axesLine = computed(() =>
(heading.value[3] ?? '')
.split(/(numérique|économique|citoyenne)/)
.filter(Boolean)
.map(text => ({ text, hue: AXIS_WORDS[text] })))
const active = ref<AxisKey>('numerique')
</script>
@@ -126,9 +146,16 @@ const active = ref<AxisKey>('numerique')
.va-axes {
margin: 1.75rem 0 0;
font-family: var(--font-mono);
font-size: 0.8rem;
color: hsl(var(--color-text) / 0.7);
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(1.3rem, 2.6vw, 1.85rem);
letter-spacing: -0.025em;
line-height: 1.15;
color: hsl(var(--color-text));
}
.va-axes-word {
color: hsl(var(--hue));
}
.va-means {
+10 -8
View File
@@ -9,7 +9,7 @@
<component :is="tool.to ? NuxtLink : 'span'" :to="tool.to" class="tool-name">
{{ tool.label }}
</component>
<span v-if="tool.gestation" class="tool-state">en gestation</span>
<UiStatusBadge class="tool-status" :status="tool.status" :gestation="tool.gestation" :since="tool.since" />
<p class="tool-desc">{{ tool.description }}</p>
<p v-if="tool.presentation" class="tool-app">
<strong>{{ tool.presentation.title }}</strong> — {{ tool.presentation.text }}
@@ -50,6 +50,9 @@
{{ action.label }}
</button>
</template>
<span v-for="action in tool.actions.filter(a => a.note)" :key="`note-${action.label}`" class="tool-note">
{{ action.note }}
</span>
</div>
</div>
</template>
@@ -113,16 +116,15 @@ a.tool-name:hover {
color: hsl(var(--hue));
}
.tool-state {
.tool-status {
margin-left: 0.4rem;
font-family: var(--font-mono);
font-size: 0.64rem;
color: hsl(var(--color-text) / 0.5);
white-space: nowrap;
}
.tool-state::before {
content: '◌ ';
.tool-note {
align-self: center;
font-family: var(--font-mono);
font-size: 0.66rem;
color: hsl(var(--axis-cit));
}
.tool-desc {
+4 -4
View File
@@ -23,14 +23,14 @@
</li>
</ul>
<!-- Numérique : the stacks LPK serves -->
<!-- Numérique : the stacks LaPlanK serves -->
<NuxtLink
v-if="axis.key === 'numerique' && lpk && bouquet"
:to="lpk.to"
class="toolbox-lpk"
>
<span class="toolbox-lpk-head">
<strong>{{ lpk.name }}</strong> <span>{{ lpk.fullName }} — {{ lpk.text }}</span>
<strong>{{ lpk.name }}</strong> <span>{{ lpk.text }}</span>
</span>
<span class="toolbox-lpk-stacks">
<span v-for="s in bouquet.inService" :key="s.name" class="toolbox-lpk-stack">{{ s.name }}</span>
@@ -65,7 +65,7 @@ import type { HomeAxis } from '~/composables/useHomeData'
withDefaults(defineProps<{
axes: HomeAxis[]
lpk?: { name: string, fullName: string, text: string, to: string }
lpk?: { name: string, text: string, to: string }
bouquet?: { title: string, inService: { name?: string }[], stacks: unknown[] } | null
gratewizard?: { title: string, subtitle: string }
showPromise?: boolean
@@ -153,7 +153,7 @@ function onAction(id: string) {
gap: 0.5rem;
}
/* LPK */
/* LaPlanK */
.toolbox-lpk {
display: flex;
flex-direction: column;
-492
View File
@@ -1,492 +0,0 @@
<template>
<div class="vc">
<section class="vc-hero">
<HomeShadoks />
<div class="container-content vc-hero-inner">
<h1 class="vc-h1">
{{ heading[0] }} <span>{{ heading[1] }}</span>
</h1>
<p v-if="format" class="vc-lede">{{ format.text }}</p>
</div>
</section>
<section class="container-content vc-bench" aria-label="La boîte à outils">
<!-- Filters -->
<div class="vc-filters">
<div class="vc-chips" role="group" aria-label="Filtrer par axe">
<button
type="button"
class="vc-chip"
:class="{ 'vc-chip--on': filter === 'all' }"
:aria-pressed="filter === 'all'"
@click="filter = 'all'"
>
Tout
</button>
<button
v-for="axis in axes"
:key="axis.key"
type="button"
class="vc-chip"
:class="{ 'vc-chip--on': filter === axis.key }"
:style="{ '--hue': `var(${axis.hue})` }"
:aria-pressed="filter === axis.key"
@click="filter = axis.key"
>
<span class="vc-chip-dot" aria-hidden="true" />
{{ axis.short }}
</button>
</div>
<label class="vc-toggle">
<input v-model="onlyService" type="checkbox">
<span>{{ bouquet?.statuses?.service ?? 'En service' }} seulement</span>
</label>
</div>
<!-- Axis by axis -->
<div v-for="axis in visibleAxes" :key="axis.key" class="vc-axis" :style="{ '--hue': `var(${axis.hue})` }">
<header class="vc-axis-head">
<p class="vc-axis-lever">{{ axis.lever }}</p>
<h2 class="vc-axis-title">
<NuxtLink :to="axis.to">{{ axis.title }}</NuxtLink>
</h2>
<p v-if="axis.promise" class="vc-axis-promise">{{ axis.promise }}</p>
</header>
<div class="vc-grid">
<HomeToolItem
v-for="tool in toolsOf(axis)"
:key="tool.label"
:tool="tool"
class="vc-card"
@action="onAction"
/>
<button
v-if="axis.key === 'economie' && gratewizard"
type="button"
class="vc-card vc-gw"
@click="launch()"
>
<span class="i-lucide-sparkles h-5 w-5" aria-hidden="true" />
<strong>{{ gratewizard.title }}</strong>
<span>{{ gratewizard.subtitle }}</span>
</button>
<p v-if="!toolsOf(axis).length" class="vc-empty">
Tout est en gestation sur cet axe —
<button type="button" class="vc-link" @click="onlyService = false">afficher aussi les outils en gestation</button>.
</p>
</div>
<!-- LPK bouquet, category by category -->
<div v-if="axis.key === 'numerique' && bouquet && lpk" class="vc-bouquet">
<p class="vc-bouquet-head">
<strong>{{ lpk.name }}</strong> {{ lpk.fullName }} — {{ lpk.text }}
</p>
<ul class="vc-legend" aria-label="Légende des statuts">
<li v-for="(label, key) in bouquet.statuses" :key="key">
<span class="vc-dot" :class="`vc-dot--${key}`" aria-hidden="true" />{{ label }}
</li>
</ul>
<div class="vc-cats">
<div v-for="cat in bouquet.categories" v-show="stacksOf(cat).length" :key="cat.label" class="vc-cat">
<p class="vc-cat-label">
<span :class="`i-lucide-${cat.icon}`" class="h-3.5 w-3.5" aria-hidden="true" />
{{ cat.label }}
</p>
<ul>
<li v-for="s in stacksOf(cat)" :key="s.role + s.name">
<span class="vc-dot" :class="`vc-dot--${s.status}`" :title="bouquet.statuses[s.status]" />
<span class="vc-stack-role">{{ s.role }}</span>
<a v-if="s.url && s.name" :href="s.url" target="_blank" rel="noopener" class="vc-stack-name">{{ s.name }}</a>
<span v-else-if="s.name" class="vc-stack-name">{{ s.name }}</span>
<span class="sr-only">— {{ bouquet.statuses[s.status] }}</span>
</li>
</ul>
</div>
</div>
<NuxtLink :to="lpk.to" class="vc-more">
{{ bouquet.title }}
<span class="i-lucide-arrow-right h-4 w-4" aria-hidden="true" />
</NuxtLink>
</div>
</div>
</section>
<section class="vc-section">
<div class="container-content">
<HomeCitations :lines="hero.citations ?? []" class="vc-cites" />
<HomeFormat
:format="format"
:book="book"
:evenement="evenement"
:hero="hero"
:show-header="false"
@open-player="$emit('open-player')"
@open-pdf="$emit('open-pdf')"
/>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import type { AxisKey, HomeAxis, HomeStack } from '~/composables/useHomeData'
const emit = defineEmits<{
'open-player': []
'open-pdf': []
}>()
const { hero, axes, lpk, bouquet, evenement, book, format, gratewizard } = await useHomeData()
const heading = computed<string[]>(() => hero.value.heading ?? [])
const { launch } = useGrateWizard()
const filter = ref<'all' | AxisKey>('all')
const onlyService = ref(false)
const visibleAxes = computed(() =>
filter.value === 'all' ? axes.value : axes.value.filter(a => a.key === filter.value))
function toolsOf(axis: HomeAxis) {
return onlyService.value ? axis.items.filter(t => !t.gestation) : axis.items
}
function stacksOf(cat: { items: (HomeStack | string)[] }) {
const items = cat.items.filter((i): i is HomeStack => typeof i !== 'string')
return onlyService.value ? items.filter(i => i.status === 'service') : items
}
function onAction(id: string) {
if (id === 'open-player') emit('open-player')
else if (id === 'open-pdf') emit('open-pdf')
}
</script>
<style scoped>
.vc-hero {
position: relative;
overflow: hidden;
padding: clamp(2.5rem, 6vw, 4.5rem) 1rem clamp(1.5rem, 3vw, 2.5rem);
}
.vc-hero-inner {
position: relative;
z-index: 1;
}
.vc-h1 {
margin: 0;
max-width: 20ch;
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(2.2rem, 5.6vw, 4rem);
letter-spacing: -0.035em;
line-height: 1;
color: hsl(var(--color-text));
}
.vc-h1 span {
font-weight: 400;
color: hsl(var(--color-text) / 0.55);
}
.vc-lede {
margin: 1.25rem 0 0;
max-width: 40rem;
font-size: 1.1rem;
line-height: 1.5;
color: hsl(var(--color-text) / 0.75);
}
.vc-bench {
padding: 0 1rem;
}
/* Filters */
.vc-filters {
position: sticky;
top: var(--header-height);
z-index: 5;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin: 0 -1rem 1.5rem;
padding: 0.75rem 1rem;
background: hsl(var(--color-bg) / 0.92);
backdrop-filter: blur(10px);
}
.vc-chips {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.vc-chip {
--hue: var(--color-text);
display: inline-flex;
align-items: center;
gap: 0.45rem;
min-height: 2.25rem;
padding: 0.35rem 0.95rem;
border-radius: 20px;
font-family: var(--font-display);
font-weight: 700;
font-size: 0.9rem;
color: hsl(var(--color-text) / 0.75);
background: hsl(var(--color-surface));
transition: transform 0.1s ease, background 0.2s ease;
}
.vc-chip:hover { transform: translateY(-1px); }
.vc-chip:active { transform: translateY(0); }
.vc-chip--on {
color: hsl(var(--color-bg));
background: hsl(var(--hue));
}
.vc-chip-dot {
width: 0.55rem;
height: 0.55rem;
border-radius: 50%;
background: hsl(var(--hue));
}
.vc-chip--on .vc-chip-dot {
background: hsl(var(--color-bg));
}
.vc-toggle {
display: inline-flex;
align-items: center;
gap: 0.5rem;
min-height: 2.25rem;
font-size: 0.85rem;
color: hsl(var(--color-text) / 0.75);
cursor: pointer;
}
.vc-toggle input {
width: 1.1rem;
height: 1.1rem;
accent-color: hsl(var(--color-primary));
}
/* Axis */
.vc-axis + .vc-axis {
margin-top: 3rem;
}
.vc-axis-head {
display: grid;
gap: 0.2rem;
margin-bottom: 1rem;
padding-left: 0.9rem;
box-shadow: inset 3px 0 0 hsl(var(--hue));
}
.vc-axis-lever {
margin: 0;
font-family: var(--font-mono);
font-size: 0.72rem;
color: hsl(var(--hue));
}
.vc-axis-title {
margin: 0;
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(1.4rem, 3vw, 1.9rem);
letter-spacing: -0.025em;
color: hsl(var(--color-text));
}
.vc-axis-title a:hover { color: hsl(var(--hue)); }
.vc-axis-promise {
margin: 0.2rem 0 0;
max-width: 44rem;
font-size: 0.98rem;
line-height: 1.45;
color: hsl(var(--color-text) / 0.72);
}
.vc-grid {
display: grid;
gap: 0.75rem;
}
@media (min-width: 768px) {
.vc-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
.vc-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.vc-grid > .vc-card {
background: hsl(var(--color-surface));
border-radius: 16px;
padding: 1.1rem;
}
.vc-gw {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.3rem;
text-align: left;
font-size: 0.85rem;
color: hsl(var(--color-text) / 0.7);
background: hsl(var(--hue) / 0.1);
transition: transform 0.12s ease;
}
.vc-gw:hover { transform: translateY(-3px); }
.vc-gw > span:first-child { color: hsl(var(--hue)); }
.vc-gw strong {
font-family: var(--font-display);
font-size: 1.05rem;
color: hsl(var(--color-text));
}
.vc-empty {
margin: 0;
font-size: 0.9rem;
color: hsl(var(--color-text) / 0.6);
}
.vc-link {
color: hsl(var(--hue));
text-decoration: underline;
text-underline-offset: 2px;
}
/* Bouquet */
.vc-bouquet {
margin-top: 1rem;
padding: 1.25rem;
border-radius: 16px;
background: hsl(var(--hue) / 0.08);
}
.vc-bouquet-head {
margin: 0;
font-size: 0.95rem;
line-height: 1.45;
color: hsl(var(--color-text) / 0.75);
}
.vc-bouquet-head strong {
font-family: var(--font-display);
font-size: 1.1rem;
color: hsl(var(--hue));
}
.vc-legend {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 0.3rem 1rem;
margin: 0.75rem 0 1rem;
padding: 0;
font-family: var(--font-mono);
font-size: 0.7rem;
color: hsl(var(--color-text) / 0.6);
}
.vc-legend li {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.vc-cats {
columns: 16rem;
column-gap: 1.5rem;
}
.vc-cat {
break-inside: avoid;
margin-bottom: 1.1rem;
}
.vc-cat-label {
display: flex;
align-items: center;
gap: 0.4rem;
margin: 0 0 0.4rem;
font-family: var(--font-display);
font-weight: 700;
font-size: 0.9rem;
color: hsl(var(--color-text));
}
.vc-cat ul {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.2rem;
}
.vc-cat li {
display: grid;
grid-template-columns: 0.5rem minmax(0, 1fr);
column-gap: 0.45rem;
align-items: baseline;
font-size: 0.82rem;
line-height: 1.4;
}
.vc-dot {
display: inline-block;
flex-shrink: 0;
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
align-self: center;
}
.vc-dot--service { background: hsl(var(--hue)); }
.vc-dot--catalogue { box-shadow: inset 0 0 0 1.5px hsl(var(--hue)); }
.vc-dot--avenir { box-shadow: inset 0 0 0 1px hsl(var(--color-text) / 0.3); }
.vc-cat li > .vc-dot { grid-row: 1; }
.vc-cat li > :not(.vc-dot) { grid-column: 2; }
.vc-stack-role { color: hsl(var(--color-text) / 0.6); }
.vc-stack-name {
font-weight: 700;
color: hsl(var(--color-text));
}
a.vc-stack-name:hover { color: hsl(var(--hue)); }
.vc-more {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-family: var(--font-display);
font-weight: 700;
color: hsl(var(--hue));
}
.vc-section {
padding: clamp(3rem, 7vw, 5rem) 1rem 0;
}
.vc-cites {
margin-bottom: 2.5rem;
}
@media (prefers-reduced-motion: reduce) {
.vc-chip, .vc-gw { transition: none; }
}
</style>
-321
View File
@@ -1,321 +0,0 @@
<template>
<div class="vb">
<section class="vb-hero">
<HomeShadoks />
<div class="container-content vb-hero-inner">
<h1 class="vb-h1">
<span class="vb-h1-a">{{ heading[0] }}</span>
<span class="vb-h1-b">{{ heading[1] }}</span>
</h1>
<p class="vb-axes">{{ heading[3] }}</p>
<a :href="`#${steps[0]?.id}`" class="vb-start">
{{ heading[2] }}
<span class="i-lucide-arrow-down h-4 w-4" aria-hidden="true" />
</a>
</div>
</section>
<div class="container-content vb-layout">
<!-- Step rail: the onboarding is a real sequence, hence the numbers -->
<nav class="vb-rail" aria-label="Parcours">
<ol>
<li v-for="(step, i) in steps" :key="step.id">
<a
:href="`#${step.id}`"
class="vb-rail-link"
:class="{ 'vb-rail-link--on': current === step.id, 'vb-rail-link--done': i < currentIndex }"
:aria-current="current === step.id ? 'step' : undefined"
>
<span class="vb-rail-n">{{ i + 1 }}</span>
<span class="vb-rail-label">{{ step.label }}</span>
</a>
</li>
</ol>
</nav>
<div ref="stepsEl" class="vb-steps">
<section :id="steps[0]!.id" class="vb-step">
<p class="vb-step-n">1 / {{ steps.length }}</p>
<HomeCitations :lines="hero.citations ?? []" />
</section>
<section :id="steps[1]!.id" class="vb-step">
<p class="vb-step-n">2 / {{ steps.length }}</p>
<h2 class="vb-h2">{{ steps[1]!.label }}</h2>
<HomeConsole
v-model="active"
:axes="axes"
:stacks="bouquet?.inService"
:stacks-to="lpk?.to"
:stacks-label="lpk ? `En service chez ${lpk.name} :` : 'En service :'"
compact
/>
</section>
<section :id="steps[2]!.id" class="vb-step">
<p class="vb-step-n">3 / {{ steps.length }}</p>
<h2 class="vb-h2">{{ steps[2]!.label }}</h2>
<HomeToolbox
:axes="axes"
:lpk="lpk"
:bouquet="bouquet"
:gratewizard="gratewizard"
anchors
@open-player="$emit('open-player')"
@open-pdf="$emit('open-pdf')"
/>
</section>
<section :id="steps[3]!.id" class="vb-step">
<p class="vb-step-n">4 / {{ steps.length }}</p>
<HomeFormat
:format="format"
:book="book"
:evenement="evenement"
:hero="hero"
@open-player="$emit('open-player')"
@open-pdf="$emit('open-pdf')"
/>
</section>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { AxisKey } from '~/composables/useHomeData'
defineEmits<{
'open-player': []
'open-pdf': []
}>()
const { hero, axes, lpk, bouquet, evenement, book, format, gratewizard } = await useHomeData()
const heading = computed<string[]>(() => hero.value.heading ?? [])
const active = ref<AxisKey>('numerique')
// Step labels are existing sentences of home.yml
function tidy(s = '') {
const t = s.replace(/^[—\s]+|[—\s.]+$/g, '')
return t.charAt(0).toUpperCase() + t.slice(1)
}
const steps = computed(() => [
{ id: 'emancipation', label: tidy(hero.value.citations?.[0]) },
{ id: 'en-amont', label: tidy(hero.value.approach) },
{ id: 'les-moyens', label: tidy(heading.value[4]) },
{ id: 'le-format', label: tidy(format.value?.title) },
])
const stepsEl = ref<HTMLElement | null>(null)
const current = ref('emancipation')
const currentIndex = computed(() => steps.value.findIndex(s => s.id === current.value))
onMounted(() => {
const observer = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) current.value = e.target.id
// Scrolling back up past a step: the previous one becomes current
else if (e.target.id === current.value && e.boundingClientRect.top > 0) {
const i = steps.value.findIndex(s => s.id === e.target.id)
current.value = steps.value[Math.max(0, i - 1)]!.id
}
}
}, { rootMargin: '-45% 0px -50% 0px' })
stepsEl.value?.querySelectorAll('.vb-step').forEach(el => observer.observe(el))
onUnmounted(() => observer.disconnect())
})
</script>
<style scoped>
.vb-hero {
position: relative;
overflow: hidden;
padding: clamp(3rem, 8vw, 6rem) 1rem clamp(2rem, 5vw, 3.5rem);
}
.vb-hero-inner {
position: relative;
z-index: 1;
}
.vb-h1 {
margin: 0;
display: flex;
flex-direction: column;
font-family: var(--font-display);
letter-spacing: -0.035em;
line-height: 0.98;
}
.vb-h1-a {
font-weight: 800;
font-size: clamp(2.4rem, 7vw, 5.2rem);
color: hsl(var(--color-text));
max-width: 14ch;
}
.vb-h1-b {
margin-top: 0.4rem;
font-weight: 400;
font-size: clamp(1.6rem, 4vw, 2.8rem);
color: hsl(var(--color-text) / 0.55);
}
.vb-axes {
margin: 1.5rem 0 0;
font-family: var(--font-mono);
font-size: 0.8rem;
color: hsl(var(--color-text) / 0.7);
}
.vb-start {
display: inline-flex;
align-items: center;
gap: 0.5rem;
min-height: 2.75rem;
margin-top: 1.75rem;
padding: 0.6rem 1.3rem;
border-radius: 20px;
font-family: var(--font-display);
font-weight: 700;
color: hsl(var(--color-bg));
background: hsl(var(--color-text));
transition: transform 0.1s ease;
}
.vb-start:hover { transform: translateY(-1px); }
.vb-start:active { transform: translateY(0); }
.vb-layout {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 1.5rem;
padding: 0 1rem;
}
@media (min-width: 1024px) {
.vb-layout {
grid-template-columns: 15rem minmax(0, 1fr);
gap: 3rem;
}
}
/* Rail */
.vb-rail {
position: sticky;
top: var(--header-height);
z-index: 5;
align-self: start;
margin: 0 -1rem;
padding: 0.5rem 1rem;
background: hsl(var(--color-bg) / 0.92);
backdrop-filter: blur(10px);
}
.vb-rail ol {
list-style: none;
margin: 0;
padding: 0;
display: flex;
gap: 0.25rem;
overflow-x: auto;
}
@media (min-width: 1024px) {
.vb-rail {
top: calc(var(--header-height) + 2rem);
margin: 0;
padding: 0;
background: none;
backdrop-filter: none;
}
.vb-rail ol {
flex-direction: column;
gap: 0.35rem;
}
}
.vb-rail-link {
display: flex;
align-items: center;
gap: 0.6rem;
min-height: 2.25rem;
padding: 0.35rem 0.7rem 0.35rem 0.35rem;
border-radius: 20px;
white-space: nowrap;
font-size: 0.85rem;
color: hsl(var(--color-text) / 0.55);
transition: background 0.2s ease, color 0.2s ease;
}
@media (min-width: 1024px) {
.vb-rail-link { white-space: normal; line-height: 1.3; }
}
.vb-rail-link:hover {
background: hsl(var(--color-text) / 0.05);
}
.vb-rail-n {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 1.6rem;
height: 1.6rem;
border-radius: 50%;
font-family: var(--font-mono);
font-size: 0.75rem;
box-shadow: inset 0 0 0 1.5px hsl(var(--color-text) / 0.25);
}
.vb-rail-link--done .vb-rail-n {
box-shadow: none;
background: hsl(var(--color-text) / 0.12);
}
.vb-rail-link--on {
color: hsl(var(--color-text));
font-weight: 700;
}
.vb-rail-link--on .vb-rail-n {
box-shadow: none;
color: hsl(var(--color-bg));
background: hsl(var(--color-primary));
}
/* Steps */
.vb-step {
padding: clamp(2rem, 5vw, 3.5rem) 0;
scroll-margin-top: calc(var(--header-height) + 3.5rem);
}
.vb-step + .vb-step {
box-shadow: 0 -1px 0 hsl(var(--color-text) / 0.07);
}
.vb-step-n {
margin: 0 0 1rem;
font-family: var(--font-mono);
font-size: 0.72rem;
color: hsl(var(--color-primary));
}
.vb-h2 {
margin: 0 0 1.5rem;
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(1.6rem, 3.6vw, 2.4rem);
letter-spacing: -0.03em;
line-height: 1.05;
color: hsl(var(--color-text));
max-width: 22ch;
}
@media (prefers-reduced-motion: reduce) {
.vb-start, .vb-rail-link { transition: none; }
}
</style>
-71
View File
@@ -1,71 +0,0 @@
<template>
<nav class="vswitch" aria-label="Variantes de l'accueil">
<span class="vswitch-label">Variante</span>
<NuxtLink
v-for="v in variants"
:key="v.id"
:to="{ path: '/', query: { v: v.id } }"
class="vswitch-item"
:class="{ 'vswitch-item--on': current === v.id }"
:aria-current="current === v.id ? 'page' : undefined"
>
{{ v.label }}
</NuxtLink>
</nav>
</template>
<script setup lang="ts">
defineProps<{
current: string
}>()
const variants = [
{ id: 'a', label: 'A · Console' },
{ id: 'b', label: 'B · Parcours' },
{ id: 'c', label: 'C · Établi' },
{ id: '0', label: 'Actuelle' },
]
</script>
<style scoped>
.vswitch {
position: fixed;
left: 50%;
bottom: 1rem;
z-index: 60;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.3rem;
border-radius: 20px;
background: hsl(var(--color-text));
box-shadow: 0 10px 30px -10px hsl(0 0% 0% / 0.5);
font-size: 0.78rem;
max-width: calc(100vw - 1rem);
overflow-x: auto;
}
.vswitch-label {
padding: 0 0.5rem;
font-family: var(--font-mono);
font-size: 0.68rem;
color: hsl(var(--color-bg) / 0.6);
}
.vswitch-item {
padding: 0.35rem 0.75rem;
border-radius: 20px;
white-space: nowrap;
color: hsl(var(--color-bg) / 0.85);
}
.vswitch-item:hover {
background: hsl(var(--color-bg) / 0.12);
}
.vswitch-item--on {
color: hsl(var(--color-text));
background: hsl(var(--color-bg));
}
</style>
-279
View File
@@ -1,279 +0,0 @@
<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>