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
+2 -2
View File
@@ -21,9 +21,9 @@ export default defineAppConfig({
},
},
libredecision: {
url: import.meta.dev ? 'http://localhost:3002' : 'https://decision.librodrome.org',
url: 'https://decision.librodrome.org',
},
sejeteral0: {
url: import.meta.dev ? 'http://localhost:3009' : 'https://collectivites.librodrome.org',
url: 'https://collectivites.librodrome.org',
},
})
+8 -1
View File
@@ -16,7 +16,7 @@
--sidebar-width: 280px;
--font-display: 'Bricolage Grotesque', system-ui, sans-serif;
--font-sans: 'Atkinson Hyperlegible', system-ui, sans-serif;
--font-sans: 'Public Sans', system-ui, sans-serif;
--font-logo: 'Syne', sans-serif;
/* One hue per axis — overridden by each palette (stores/palette.ts) */
@@ -29,6 +29,13 @@
--ease-in-out-expo: cubic-bezier(0.87, 0, 0.13, 1);
}
/* Fader level of the home console — registered so it can transition smoothly */
@property --level {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
*,
*::before,
*::after {
+9
View File
@@ -378,6 +378,15 @@ function onTouchEnd(e: TouchEvent) {
}
// ── Lifecycle ──
// While the reader is open, the persistent mini player (above it) lifts up
// so it does not cover the reader navigation.
if (import.meta.client) {
watch(isOpen, (open) => {
document.documentElement.classList.toggle('book-player-open', open)
}, { immediate: true })
onUnmounted(() => document.documentElement.classList.remove('book-player-open'))
}
watch(isOpen, async (open) => {
if (open) {
showSommaire.value = false
-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>
+145 -62
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-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>
+13 -18
View File
@@ -3,22 +3,7 @@
<div class="container-content flex h-[var(--header-height)] items-center justify-between px-4">
<!-- Logo -->
<NuxtLink to="/" class="logo-link flex items-center gap-2.5">
<svg class="logo-icon" viewBox="0 0 46 78" fill="none" aria-hidden="true">
<defs>
<linearGradient id="sect-grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="hsl(var(--color-primary))"/>
<stop offset="100%" stop-color="hsl(var(--color-accent))"/>
</linearGradient>
</defs>
<path
d="M 33 10 C 26 7 18 7 14 11 C 8 15 7 24 10 31 C 13 38 21 40 27 44 C 33 48 38 55 35 62 C 32 69 24 72 17 70 C 10 68 8 72 10 75 C 12 78 20 79 28 76"
stroke="url(#sect-grad)"
stroke-width="5"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
/>
</svg>
<UiSectionSign class="logo-icon" />
<span class="logo-text">{{ site?.identity.name }}</span>
</NuxtLink>
@@ -88,9 +73,19 @@ const AXIS_HUES = ['--axis-num', '--axis-eco', '--axis-cit']
}
.logo-icon {
width: 1.6rem;
height: 2rem;
width: auto;
height: 2.1rem;
flex-shrink: 0;
transform: rotate(-8deg);
transition: transform 0.2s ease;
}
.logo-link:hover .logo-icon {
transform: rotate(0deg) scale(1.08);
}
@media (prefers-reduced-motion: reduce) {
.logo-icon { transition: none; }
}
.logo-text {
@@ -179,6 +179,7 @@ onClickOutside(widgetRef, () => {
.mini-player {
position: fixed;
bottom: 1rem;
transition: bottom 0.3s ease;
right: max(1rem, calc((100vw - 80rem) / 2));
z-index: 70;
display: flex;
@@ -186,6 +187,11 @@ onClickOutside(widgetRef, () => {
align-items: flex-end;
}
/* Book player open: stay above its navigation bar */
:global(html.book-player-open .mini-player) {
bottom: 5.5rem;
}
/* ═══════════════════════════════════════
PILL
═══════════════════════════════════════ */
+16
View File
@@ -0,0 +1,16 @@
<template>
<!-- § — glyphe de Playfair Display 700 (SIL OFL), vectorisé ; dégradé primaire → accent -->
<svg class="section-sign" viewBox="0 0 511 970" aria-hidden="true">
<defs>
<linearGradient :id="gradId" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="hsl(var(--color-primary))" />
<stop offset="100%" stop-color="hsl(var(--color-accent))" />
</linearGradient>
</defs>
<path :fill="`url(#${gradId})`" d="M277 0Q320 0 349 7.5Q378 15 395 22Q426 37 443.5 59.5Q461 82 461 115Q461 143 441.5 164Q422 185 387 185Q354 185 332.5 165.5Q311 146 311 116Q311 92 324.5 71Q338 50 362 42Q357 34 344.5 29Q332 24 316 21.5Q300 19 284 19Q246 19 223 32.5Q200 46 190 66.5Q180 87 180 110Q180 150 200 177.5Q220 205 256 230Q292 255 340 287Q394 324 432.5 354.5Q471 385 491 418Q511 451 511 495Q511 548 475.5 593Q440 638 352 677L343 667Q373 646 388 623Q403 600 403 567Q403 536 386 511.5Q369 487 333.5 459Q298 431 241 394Q178 353 135.5 318.5Q93 284 71.5 248Q50 212 50 167Q50 116 78.5 78.5Q107 41 158.5 20.5Q210 0 277 0ZM158 292 168 303Q137 323 122.5 346Q108 369 108 403Q108 433 124.5 458Q141 483 177 511Q213 539 270 576Q332 617 374.5 651.5Q417 686 439 722Q461 758 461 803Q461 853 432 891Q403 929 352 949.5Q301 970 233 970Q190 970 161 962.5Q132 955 115 948Q85 933 67 910.5Q49 888 49 855Q49 827 69 806Q89 785 123 785Q157 785 178 804.5Q199 824 199 854Q199 878 185.5 898.5Q172 919 148 928Q152 935 165 940.5Q178 946 194.5 948.5Q211 951 226 951Q265 951 287.5 937.5Q310 924 320.5 903Q331 882 331 860Q331 820 311 792.5Q291 765 255 740Q219 715 171 683Q116 646 78 615.5Q40 585 20 551.5Q0 518 0 475Q0 421 34.5 376Q69 331 158 292Z" />
</svg>
</template>
<script setup lang="ts">
const gradId = `sect-grad-${useId()}`
</script>
+69
View File
@@ -0,0 +1,69 @@
<template>
<span v-if="label" class="status" :class="`status--${tone}`">
<span class="status-dot" aria-hidden="true" />
<span>{{ label }}</span>
<span v-if="since" class="status-since">· {{ since }}</span>
</span>
</template>
<script setup lang="ts">
// Project maturity, as written in YAML: `status: En production` (+ `since`),
// `status: En développement`, or the legacy `gestation: true`.
const props = defineProps<{
status?: string
gestation?: boolean
since?: string
}>()
const label = computed(() => props.status || (props.gestation ? 'En gestation' : ''))
const tone = computed(() => {
const s = label.value.toLowerCase()
if (s.includes('production')) return 'prod'
if (s.includes('développement') || s.includes('developpement')) return 'dev'
return 'gest'
})
</script>
<style scoped>
.status {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.15rem 0.6rem;
border-radius: 20px;
font-family: var(--font-mono);
font-size: 0.68rem;
font-weight: 500;
line-height: 1.5;
white-space: nowrap;
vertical-align: middle;
color: hsl(var(--color-text) / 0.75);
background: hsl(var(--color-text) / 0.06);
}
.status-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
flex-shrink: 0;
}
/* En production : plein */
.status--prod .status-dot { background: hsl(var(--axis-cit)); }
.status--prod { color: hsl(var(--axis-cit)); background: hsl(var(--axis-cit) / 0.1); }
/* En développement : demi-plein */
.status--dev .status-dot {
background: linear-gradient(to top, hsl(var(--axis-eco)) 50%, transparent 50%);
box-shadow: inset 0 0 0 1.5px hsl(var(--axis-eco));
}
.status--dev { color: hsl(var(--axis-eco)); background: hsl(var(--axis-eco) / 0.1); }
/* En gestation : cercle */
.status--gest .status-dot { box-shadow: inset 0 0 0 1.5px hsl(var(--color-text) / 0.45); }
.status-since {
opacity: 0.75;
}
</style>
+3 -17
View File
@@ -71,23 +71,11 @@ export function useAudioPlayer() {
store.setSong(song)
firedMilestones.clear()
// Try OGG first, fall back to MP3
const oggPath = song.file.replace(/\.mp3$/, '.ogg')
const canOgg = el.canPlayType('audio/ogg; codecs=vorbis')
el.src = canOgg ? oggPath : song.file
// Only MP3 files are published (no .ogg on disk nor in production):
// asking for OGG first cost a 404 and a delay on every track.
el.src = song.file
el.volume = store.volume
try {
await el.play()
store.play()
startTimeUpdate()
trackAudioPlay(song.id, song.title)
}
catch {
// If OGG failed, try MP3
if (el.src !== song.file) {
el.src = song.file
try {
await el.play()
store.play()
@@ -98,8 +86,6 @@ export function useAudioPlayer() {
console.error('Playback failed:', err)
}
}
}
}
function pause() {
getAudio().pause()
+9 -2
View File
@@ -13,6 +13,8 @@ export interface HomeAction {
secondary?: boolean
to?: string
href?: string
/** Short status line shown under the button (e.g. the v1 in production) */
note?: string
}
export interface HomeTool {
@@ -21,6 +23,9 @@ export interface HomeTool {
to?: string
href?: string
gestation?: boolean
/** « En production », « En développement »… — wins over `gestation` */
status?: string
since?: string
icon?: string
presentation?: { title: string, text: string }
actions?: HomeAction[]
@@ -86,9 +91,9 @@ export async function useHomeData() {
})
const lpk = computed(() => (site.value as any)?.lpk as
{ name: string, fullName: string, text: string, role: string, to: string } | undefined)
{ name: string, text: string, role: string, to: string } | undefined)
/** Bouquet sweethomeCloud, flattened — the stacks served by LPK */
/** Bouquet sweetHome, flattened — the stacks served by LaPlanK */
const bouquet = computed(() => {
const section = ((cloud.value as any)?.sections ?? []).find((s: any) => s.type === 'territoire')
const b = section?.bouquet
@@ -139,6 +144,8 @@ export async function useHomeData() {
evenement,
book: computed(() => (home.value as any)?.book),
format: computed(() => (home.value as any)?.format as { title: string, text: string } | undefined),
toolboxTitle: computed(() => (home.value as any)?.toolbox?.title as string | undefined),
consoleLabels: computed(() => (home.value as any)?.console as { prefix: string, top: string, bottom: string } | undefined),
gratewizard: computed(() => (home.value as any)?.gratewizard),
}
}
-1
View File
@@ -16,7 +16,6 @@ interface SiteContent {
}
lpk?: {
name: string
fullName: string
text: string
role: string
to: string
+1 -4
View File
@@ -86,10 +86,7 @@
<p style="color: hsl(var(--color-text-muted))" class="leading-relaxed">
{{ content.project.text }}
</p>
<span v-if="content?.gestation" class="gestation-badge mt-3">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
<UiStatusBadge class="mt-3" :status="content?.status" :gestation="content?.gestation" />
</div>
<!-- Extended content -->
+7 -4
View File
@@ -352,6 +352,7 @@
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
Décision collective
</h2>
<UiStatusBadge class="ml-auto" v-bind="statusOf('decision')" />
</div>
<p class="leading-relaxed mt-3" style="color: hsl(var(--color-text-muted))">
Se donner les moyens de la décision collective.
@@ -376,10 +377,7 @@
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
Tarifs de l'eau
</h2>
<span class="gestation-badge">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
<UiStatusBadge class="ml-auto" v-bind="statusOf('tarifs-eau')" />
</div>
<p class="leading-relaxed mt-3" style="color: hsl(var(--color-text-muted))">
Application pour obtenir justice sociale et incitation dynamique à la réduction.
@@ -404,6 +402,10 @@ definePageMeta({
})
const { data: content } = await usePageContent('citoyenne')
const statusOf = (id: string) => {
const item = ((content.value as any)?.items ?? []).find((i: any) => i.id === id) ?? {}
return { status: item.status, since: item.since, gestation: item.gestation }
}
const appConfig = useAppConfig()
const decisionUrl = (appConfig.libredecision as { url: string })?.url ?? '#'
@@ -449,6 +451,7 @@ useSeoPage({
.item-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
}
+7 -4
View File
@@ -396,6 +396,7 @@
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
Monnaie libre
</h2>
<UiStatusBadge class="ml-auto" v-bind="statusOf('monnaie-libre')" />
</div>
<p class="leading-relaxed mt-3" style="color: hsl(var(--color-text-muted))">
La Ğ1 (June) : une monnaie co-créée par ses membres, sans dette ni intérêt. Le dividende universel comme base.
@@ -424,10 +425,7 @@
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
Productions collectives
</h2>
<span class="gestation-badge">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
<UiStatusBadge class="ml-auto" v-bind="statusOf('productions-collectives')" />
</div>
<p class="leading-relaxed mt-3" style="color: hsl(var(--color-text-muted))">
Une plateforme pour faciliter la création d'équipes et la réalisation de productions à l'échelle des bassins de vie. Passer la seconde.
@@ -451,6 +449,10 @@ definePageMeta({
})
const { data: content } = await usePageContent('economique')
const statusOf = (id: string) => {
const item = ((content.value as any)?.items ?? []).find((i: any) => i.id === id) ?? {}
return { status: item.status, since: item.since, gestation: item.gestation }
}
useSeoPage({
title: content.value?.meta?.title ?? 'Autonomie économique',
@@ -484,6 +486,7 @@ const showPdfReader = ref(false)
.item-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
}
+1
View File
@@ -17,6 +17,7 @@
<p class="text-lg leading-relaxed" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
<UiStatusBadge class="mt-4" :status="content?.status" :since="content?.since" />
</div>
<!-- Navigation prev / index / next -->
@@ -18,10 +18,9 @@
{{ content?.description }}
</p>
<span v-if="content?.gestation" class="gestation-badge mx-auto mb-8">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
<div class="mb-8 text-center">
<UiStatusBadge :status="content?.status" :gestation="content?.gestation" />
</div>
<!-- Content -->
<div v-if="content?.content" class="prose-block mb-10">
+14 -10
View File
@@ -332,20 +332,16 @@
<div class="container-content relative z-10 text-center">
<!-- Logo SVG vectoriel — inline pour currentColor -->
<div class="evt-logo-wrap mx-auto mb-6" aria-hidden="true">
<svg viewBox="0 0 64 80" fill="none" class="evt-logo">
<path d="M 28 6 C 44 2,52 12,47 14 C 40 18,16 18,12 28 C 8 38,16 50,28 54 C 38 56,52 60,52 68 C 52 76,42 78,34 74"
stroke="currentColor" stroke-width="16" stroke-linecap="round" stroke-linejoin="round" opacity="0.12"/>
<path d="M 28 6 C 44 2,52 12,47 14 C 40 18,16 18,12 28 C 8 38,16 50,28 54 C 38 56,52 60,52 68 C 52 76,42 78,34 74"
stroke="currentColor" stroke-width="9" stroke-linecap="round" stroke-linejoin="round" opacity="0.38"/>
<path d="M 28 6 C 44 2,52 12,47 14 C 40 18,16 18,12 28 C 8 38,16 50,28 54 C 38 56,52 60,52 68 C 52 76,42 78,34 74"
stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" opacity="0.92"/>
</svg>
<UiSectionSign class="evt-logo" />
</div>
<p class="mb-3 font-mono text-sm tracking-widest text-accent uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-extrabold tracking-tight" style="color: hsl(var(--color-text))">
{{ content?.title }}
</h1>
<p v-if="evtContent?.format" class="evt-format font-display">
{{ evtContent.format }}
</p>
<p v-if="evtContent?.subtitle" class="mt-2 text-base italic" style="color: hsl(var(--color-text-muted))">
{{ evtContent.subtitle }}
</p>
@@ -447,6 +443,14 @@ useSeoPage({
</script>
<style scoped>
.evt-format {
margin-top: 0.35rem;
font-weight: 600;
font-size: clamp(1.25rem, 3vw, 1.75rem);
letter-spacing: -0.02em;
color: hsl(var(--axis-cit));
}
.page-title {
font-size: clamp(2.5rem, 6vw, 3.5rem);
}
@@ -657,12 +661,12 @@ useSeoPage({
align-items: center;
justify-content: center;
color: hsl(var(--color-primary));
filter: drop-shadow(0 0 14px hsl(var(--color-primary) / 0.25));
filter: drop-shadow(0 6px 14px hsl(var(--color-primary) / 0.25));
}
.evt-logo {
width: 100%;
height: 100%;
width: auto;
}
/* ── Badge gestation hero ── */
+1 -30
View File
@@ -1,27 +1,7 @@
<template>
<div>
<HomeVariantParcours
v-if="variant === 'b'"
@open-player="showBookPlayer = true"
@open-pdf="showPdfReader = true"
/>
<HomeVariantEtabli
v-else-if="variant === 'c'"
@open-player="showBookPlayer = true"
@open-pdf="showPdfReader = true"
/>
<template v-else-if="variant === '0'">
<HomeHeroSection />
<HomeAxisGrid @open-player="showBookPlayer = true" @open-pdf="showPdfReader = true" />
</template>
<HomeVariantConsole
v-else
@open-player="showBookPlayer = true"
@open-pdf="showPdfReader = true"
/>
<HomeLanding @open-player="showBookPlayer = true" @open-pdf="showPdfReader = true" />
<HomeMessages />
<HomeVariantSwitch v-if="showSwitch" :current="variant" />
<BookPlayer v-model="showBookPlayer" />
<BookPdfReader v-model="showPdfReader" />
</div>
@@ -33,15 +13,6 @@ useSeoPage({
description: 'Construire une autonomie collective à l\'échelle des bassins de vie. Un livre, des chansons et des outils pour l\'émancipation.',
})
// Home variants under review: ?v=a (console, default) · ?v=b (parcours) · ?v=c (établi) · ?v=0 (previous home)
const route = useRoute()
const VARIANTS = ['a', 'b', 'c', '0'] as const
const variant = computed(() => {
const v = String(route.query.v ?? 'a')
return (VARIANTS as readonly string[]).includes(v) ? v : 'a'
})
const showSwitch = computed(() => route.query.v !== undefined)
const showBookPlayer = ref(false)
const showPdfReader = ref(false)
</script>
+27 -12
View File
@@ -247,10 +247,19 @@
<p style="color: hsl(var(--color-text-muted))" class="leading-relaxed">
{{ content.project.text }}
</p>
<span v-if="content?.gestation" class="gestation-badge mt-3">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
<UiStatusBadge
class="mt-3"
:status="content.project.status"
:gestation="content.project.gestation || content?.gestation"
:since="content.project.since"
/>
<div v-if="content.project.action?.href" class="project-action mt-4">
<UiBaseButton :href="content.project.action.href" target="_blank">
<div class="i-lucide-external-link mr-2 h-4 w-4" />
{{ content.project.action.label }}
</UiBaseButton>
<span v-if="content.project.action.note" class="project-action-note">{{ content.project.action.note }}</span>
</div>
</div>
</div>
@@ -477,7 +486,7 @@
</div>
</div>
<!-- Bouquet sweethomeCloud -->
<!-- Bouquet sweetHome -->
<div v-if="section.bouquet" class="territoire-bouquet">
<h3 class="territoire-bouquet-title">
<div class="i-lucide-package h-4 w-4" />
@@ -621,13 +630,6 @@
</div><!-- /sections-area -->
<div v-if="slug === 'logiciel-libre'" class="text-center mt-6">
<UiBaseButton href="https://axiom-team.fr/dons" target="_blank">
<div class="i-lucide-external-link mr-2 h-4 w-4" />
Voir la v1 bridée (premier flux)
</UiBaseButton>
</div>
<div v-if="slug === 'tarifs-eau'" class="text-center mt-6">
<UiBaseButton :href="sejeteral0Url" target="_blank">
<div class="i-lucide-external-link mr-2 h-4 w-4" />
@@ -1650,6 +1652,19 @@ useSeoPage({
color: hsl(var(--color-primary) / 0.5);
}
.project-action {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 1rem;
}
.project-action-note {
font-family: var(--font-mono);
font-size: 0.72rem;
color: hsl(var(--axis-cit));
}
/* Bouquet — status of each stack */
.territoire-bouquet-provider {
font-size: 0.8rem;
+11 -5
View File
@@ -319,7 +319,7 @@
<NuxtLink v-if="lpk" :to="lpk.to" class="lpk-card">
<span class="lpk-name">{{ lpk.name }}</span>
<span class="lpk-body">
<span class="lpk-full">{{ lpk.fullName }} — {{ lpk.text }}</span>
<span class="lpk-full">{{ lpk.text }}</span>
<span class="lpk-role">{{ lpk.role }}</span>
</span>
<span class="i-lucide-arrow-right h-5 w-5 lpk-arrow" aria-hidden="true" />
@@ -342,10 +342,7 @@
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
{{ pillar.label }}
</h2>
<span v-if="pillar.gestation" class="gestation-badge">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
<UiStatusBadge class="ml-auto" :status="pillar.status" :gestation="pillar.gestation" :since="pillar.since" />
</div>
<p class="leading-relaxed whitespace-pre-line mt-3" style="color: hsl(var(--color-text-muted))">{{ pillar.text }}</p>
@@ -376,6 +373,7 @@
<div :class="`i-lucide-${action.icon}`" class="h-3.5 w-3.5" />
{{ action.label }}
</a>
<span v-for="action in (pillar as any).actions.filter((a: any) => a.note)" :key="`n-${action.label}`" class="pillar-action-note">{{ action.note }}</span>
</div>
</div>
</div>
@@ -439,6 +437,13 @@ useSeoPage({
line-height: 1.45;
}
.pillar-action-note {
align-self: center;
font-family: var(--font-mono);
font-size: 0.7rem;
color: hsl(var(--axis-cit));
}
.lpk-arrow { color: hsl(var(--color-primary)); flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) {
@@ -520,6 +525,7 @@ useSeoPage({
.pillar-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
}
+2 -1
View File
@@ -91,10 +91,11 @@ export default defineNuxtConfig({
head: {
htmlAttrs: { lang: 'fr' },
link: [
{ rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
{ rel: 'preconnect', href: 'https://fonts.bunny.net' },
{
rel: 'stylesheet',
href: 'https://fonts.bunny.net/css?family=bricolage-grotesque:400,500,600,700,800|atkinson-hyperlegible:400,400i,700,700i|jetbrains-mono:400,500,700|syne:400',
href: 'https://fonts.bunny.net/css?family=bricolage-grotesque:400,500,600,700,800|public-sans:400,400i,600,700,700i|jetbrains-mono:400,500,700|syne:400',
},
],
meta: [
+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-230 0 970 970">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#d4520f"/>
<stop offset="100%" stop-color="#e02b41"/>
</linearGradient>
</defs>
<path fill="url(#g)" d="M277 0Q320 0 349 7.5Q378 15 395 22Q426 37 443.5 59.5Q461 82 461 115Q461 143 441.5 164Q422 185 387 185Q354 185 332.5 165.5Q311 146 311 116Q311 92 324.5 71Q338 50 362 42Q357 34 344.5 29Q332 24 316 21.5Q300 19 284 19Q246 19 223 32.5Q200 46 190 66.5Q180 87 180 110Q180 150 200 177.5Q220 205 256 230Q292 255 340 287Q394 324 432.5 354.5Q471 385 491 418Q511 451 511 495Q511 548 475.5 593Q440 638 352 677L343 667Q373 646 388 623Q403 600 403 567Q403 536 386 511.5Q369 487 333.5 459Q298 431 241 394Q178 353 135.5 318.5Q93 284 71.5 248Q50 212 50 167Q50 116 78.5 78.5Q107 41 158.5 20.5Q210 0 277 0ZM158 292 168 303Q137 323 122.5 346Q108 369 108 403Q108 433 124.5 458Q141 483 177 511Q213 539 270 576Q332 617 374.5 651.5Q417 686 439 722Q461 758 461 803Q461 853 432 891Q403 929 352 949.5Q301 970 233 970Q190 970 161 962.5Q132 955 115 948Q85 933 67 910.5Q49 888 49 855Q49 827 69 806Q89 785 123 785Q157 785 178 804.5Q199 824 199 854Q199 878 185.5 898.5Q172 919 148 928Q152 935 165 940.5Q178 946 194.5 948.5Q211 951 226 951Q265 951 287.5 937.5Q310 924 320.5 903Q331 882 331 860Q331 820 311 792.5Q291 765 255 740Q219 715 171 683Q116 646 78 615.5Q40 585 20 551.5Q0 518 0 475Q0 421 34.5 376Q69 331 158 292Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB