Refonte accueil : hero typo statique, axes icônes, menu italic, page numérique

- Hero : 5 lignes typographiques alternées (bold/light/accent/caps/italic),
  citations et axes dans un bloc discret dépliable
- Icônes axes : Ğ1 custom, balance (éco don), graphe (WoT), marteau (décision),
  pictos plus lumineux (glow)
- Menu : Autonomie en italique + grand, Événement majuscule
- Page /autonomie renommée /numerique avec redirect 301
- Sceau hexagramme 益 Yì dans le layout, BookSection dans /modele-eco
- Fonts Syne + Space Grotesk, dark theme éclairci
- Popup GrateWizard agrandie (480×860)
- Actions AxisBlock : primary côte à côte, secondary séparé dessous

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-03-03 06:34:30 +01:00
parent 082a17d09b
commit fbc2867163
17 changed files with 615 additions and 336 deletions

View File

@@ -16,8 +16,8 @@ export default defineAppConfig({
gratewizard: {
url: import.meta.dev ? 'http://localhost:3001' : 'https://gratewizard.axiom-team.fr',
popup: {
width: 420,
height: 720,
width: 480,
height: 860,
},
},
libredecision: {

View File

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

View File

@@ -5,9 +5,9 @@
:root {
--color-primary: 18 80% 45%;
--color-accent: 32 85% 50%;
--color-bg: 220 12% 15%;
--color-surface: 220 10% 19%;
--color-surface-light: 220 8% 24%;
--color-bg: 215 8% 22%;
--color-surface: 213 7% 27%;
--color-surface-light: 210 6% 32%;
--color-text: 0 0% 100%;
--color-text-muted: 0 0% 65%;
@@ -15,8 +15,8 @@
--player-height: 0rem;
--sidebar-width: 280px;
--font-display: 'Outfit', sans-serif;
--font-sans: 'Inter', sans-serif;
--font-display: 'Syne', sans-serif;
--font-sans: 'Space Grotesk', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);

View File

@@ -24,7 +24,8 @@
>
<!-- Item icon -->
<div v-if="item.icon" class="axis-item-icon" :class="`axis-item-icon--${color}`">
<div :class="iconClass(item.icon)" class="h-5 w-5" />
<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">
@@ -40,15 +41,31 @@
<!-- Actions zone (separate from card link) -->
<div v-if="item.actions?.length" class="axis-actions">
<button
v-for="action in item.actions"
:key="action.id"
class="axis-action-btn"
@click.stop="handleAction(action.id)"
>
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
{{ action.label }}
</button>
<!-- Primary row -->
<div class="axis-actions-row">
<button
v-for="action in primaryActions(item.actions)"
:key="action.id"
class="axis-action-btn"
:class="{ 'axis-action-btn--highlight': action.highlight }"
@click.stop="handleAction(action.id)"
>
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
{{ action.label }}
</button>
</div>
<!-- Secondary row -->
<div v-if="secondaryActions(item.actions).length" class="axis-actions-secondary">
<button
v-for="action in secondaryActions(item.actions)"
:key="action.id"
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>
</div>
</div>
</div>
</div>
@@ -60,6 +77,8 @@ interface AxisAction {
id: string
label: string
icon: string
highlight?: boolean
secondary?: boolean
}
interface AxisItem {
@@ -86,6 +105,14 @@ const emit = defineEmits<{
'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')
@@ -179,13 +206,22 @@ function itemAttrs(item: AxisItem) {
}
.axis-item-icon--primary {
background: hsl(var(--color-primary) / 0.1);
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.1);
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 {
@@ -204,12 +240,26 @@ function itemAttrs(item: AxisItem) {
}
.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);
}
.axis-actions-row {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
padding: 0.75rem 1.25rem;
border-top: 1px solid hsl(var(--color-text) / 0.06);
background: hsl(var(--color-bg) / 0.4);
}
.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 {
@@ -232,4 +282,28 @@ function itemAttrs(item: AxisItem) {
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);
}
</style>

View File

@@ -0,0 +1,135 @@
<template>
<section class="section-padding">
<div class="container-content">
<div class="grid items-center gap-12 md:grid-cols-2">
<!-- Book cover -->
<UiScrollReveal>
<div class="book-cover-wrapper relative">
<!-- Shadok pumper -->
<svg class="shadok-pumper" viewBox="0 0 200 240" fill="none" aria-hidden="true">
<ellipse cx="100" cy="130" rx="55" ry="65" fill="currentColor" opacity="0.9"/>
<ellipse cx="100" cy="60" rx="30" ry="28" fill="currentColor" opacity="0.85"/>
<circle cx="88" cy="54" r="6" fill="currentColor" opacity="0.2"/>
<circle cx="112" cy="54" r="6" fill="currentColor" opacity="0.2"/>
<circle cx="90" cy="53" r="2.5" fill="currentColor" opacity="0.5"/>
<circle cx="114" cy="53" r="2.5" fill="currentColor" opacity="0.5"/>
<polygon points="100,68 115,78 85,78" fill="currentColor" opacity="0.6"/>
<line x1="80" y1="192" x2="70" y2="230" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity="0.7"/>
<line x1="120" y1="192" x2="130" y2="230" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity="0.7"/>
<line x1="70" y1="230" x2="55" y2="232" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.5"/>
<line x1="130" y1="230" x2="145" y2="232" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.5"/>
<line x1="155" y1="110" x2="190" y2="90" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity="0.6"/>
<line x1="190" y1="90" x2="190" y2="120" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity="0.6"/>
<rect x="180" y="118" width="18" height="40" rx="3" fill="currentColor" opacity="0.4"/>
</svg>
<div class="book-cover-3d">
<img
:src="content?.book.coverImage"
:alt="content?.book.coverAlt"
class="book-cover-img"
/>
</div>
</div>
</UiScrollReveal>
<!-- Content + CTAs -->
<div>
<UiScrollReveal>
<p class="mb-2 font-mono text-sm tracking-widest text-accent uppercase">{{ content?.book.kicker }}</p>
<h2 class="heading-section font-display font-bold tracking-tight text-white">
{{ content?.book.title }}
</h2>
</UiScrollReveal>
<UiScrollReveal :delay="100">
<p class="mt-4 text-lg leading-relaxed text-white/60">
{{ content?.book.description }}
</p>
</UiScrollReveal>
<UiScrollReveal :delay="200">
<div class="mt-8 flex flex-col gap-3 sm:flex-row sm:gap-4">
<UiBaseButton @click="$emit('open-player')">
<div class="i-lucide-play mr-2 h-5 w-5" />
{{ content?.book.cta.player }}
</UiBaseButton>
<UiBaseButton variant="accent" @click="$emit('open-pdf')">
<div class="i-lucide-book-open mr-2 h-5 w-5" />
{{ content?.book.cta.pdf }}
</UiBaseButton>
</div>
</UiScrollReveal>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
defineEmits<{
'open-player': []
'open-pdf': []
}>()
const { data: content } = await usePageContent('home')
</script>
<style scoped>
.book-cover-wrapper {
perspective: 800px;
display: flex;
justify-content: center;
}
.book-cover-3d {
aspect-ratio: 3 / 4;
border-radius: 0.75rem;
overflow: hidden;
border: 1px solid hsl(var(--color-text) / 0.1);
box-shadow:
0 12px 40px hsl(var(--color-text) / 0.15),
0 0 0 1px hsl(var(--color-text) / 0.08);
transition: transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1),
box-shadow 0.5s ease;
max-width: 360px;
}
.book-cover-3d:hover {
transform: rotateY(-8deg) rotateX(3deg) scale(1.02);
box-shadow:
12px 16px 48px hsl(var(--color-text) / 0.2),
0 0 0 1px hsl(var(--color-primary) / 0.2);
}
.book-cover-img {
width: 200%;
height: 100%;
object-fit: cover;
transform: translateX(-50%);
}
.heading-section {
font-size: clamp(1.625rem, 4vw, 2.125rem);
}
.shadok-pumper {
position: absolute;
right: 3%;
bottom: 8%;
width: clamp(90px, 12vw, 180px);
opacity: 0.28;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float 10s ease-in-out infinite;
z-index: 1;
}
@keyframes shadok-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@media (max-width: 768px) {
.shadok-pumper { display: none; }
}
</style>

View File

@@ -2,7 +2,7 @@
<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.15),transparent_70%)]" />
<div class="absolute inset-0 bg-[radial-gradient(ellipse_at_top,hsl(12_76%_48%/0.12),transparent_70%)]" />
<!-- Shadok bird decoration -->
<svg class="shadok-bird" viewBox="0 0 180 260" fill="none" aria-hidden="true">
@@ -46,8 +46,8 @@
<div class="container-content relative z-10 px-4">
<div class="mx-auto max-w-2xl">
<HomeTypewriterText
v-if="sentences.length"
:sentences="sentences"
v-if="hero"
:hero="hero"
/>
</div>
</div>
@@ -55,19 +55,17 @@
</template>
<script setup lang="ts">
import type { TypewriterSentence } from '~/composables/useTypewriter'
const { data: content } = await usePageContent('home')
const sentences = computed<TypewriterSentence[]>(() => {
const raw = (content.value as any)?.hero?.typewriter?.sentences
if (!Array.isArray(raw)) return []
return raw.map((s: any) => ({
text: s.text,
style: s.style || 'title',
stays: !!s.stays,
separator: !!s.separator,
}))
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 : [],
}
})
</script>

View File

@@ -1,171 +1,260 @@
<template>
<ClientOnly>
<div class="hero-text" @click="handleClick">
<!-- Locked sentences (stays: true, already revealed) -->
<TransitionGroup name="lock">
<div v-for="(item, i) in lockedSentences" :key="`lock-${i}`">
<div v-if="item.separator" class="hero-separator" />
<p v-else class="hero-line" :class="styleClass(item.style)">
{{ item.text }}
</p>
</div>
</TransitionGroup>
<!-- Active sentence pure CSS opacity fade -->
<div class="hero-active-zone">
<p
v-show="currentText"
class="hero-line hero-active"
:class="[styleClass(currentStyle), { 'is-visible': isVisible }]"
>
{{ currentText }}
</p>
</div>
</div>
<!-- SEO / no-JS fallback -->
<template #fallback>
<div class="hero-text">
<p
v-for="(sentence, i) in stayingSentences"
<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="styleClass(sentence.style)"
>
{{ sentence.text }}
</p>
: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>
</div>
</template>
</ClientOnly>
</details>
</div>
</template>
<script setup lang="ts">
import type { TypewriterSentence } from '~/composables/useTypewriter'
interface HeroAxis {
label: string
value: string
}
const props = defineProps<{
sentences: TypewriterSentence[]
interface HeroData {
heading: string[]
citations: string[]
approach: string
axes: HeroAxis[]
}
defineProps<{
hero: HeroData
}>()
const stayingSentences = computed(() => props.sentences.filter(s => s.stays))
const {
currentText,
currentStyle,
isVisible,
lockedSentences,
isComplete,
start,
skipToEnd,
} = useTypewriter(props.sentences)
onMounted(() => {
start()
})
function handleClick() {
if (!isComplete.value) {
skipToEnd()
}
}
function styleClass(style?: string) {
if (style === 'citation') return 'hero-line--citation'
if (style === 'text') return 'hero-line--body'
return 'hero-line--title'
}
</script>
<style scoped>
.hero-text {
.hero-content {
text-align: center;
min-height: 16rem;
display: flex;
flex-direction: column;
justify-content: center;
cursor: pointer;
align-items: center;
gap: 0;
}
/* ── Lines ── */
/* ── 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 {
margin: 0;
padding: 0.3em 0;
display: block;
font-family: var(--font-display);
line-height: 1.4;
line-height: 1.3;
}
.hero-line--title {
font-weight: 600;
font-size: clamp(1.3rem, 3.5vw, 2.2rem);
/* 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;
}
.hero-line--citation {
/* 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(1.05rem, 2.8vw, 1.5rem);
color: hsl(var(--color-text) / 0.7);
max-width: 40ch;
margin-inline: auto;
font-size: clamp(0.95rem, 2.2vw, 1.2rem);
color: hsl(var(--color-text) / 0.65);
margin-top: 0.3em;
}
.hero-line--body {
font-weight: 400;
font-size: clamp(1rem, 2.5vw, 1.3rem);
color: hsl(var(--color-text) / 0.6);
max-width: 46ch;
margin-inline: auto;
/* ── Discrete aside block ── */
.hero-aside {
width: 100%;
max-width: 36em;
}
/* ── Separator ── */
.hero-separator {
width: 4rem;
height: 2px;
margin: 1.25rem auto;
background: linear-gradient(
to right,
transparent,
hsl(var(--color-primary) / 0.45),
transparent
);
border-radius: 1px;
}
/* ── Active zone ── */
.hero-active-zone {
min-height: 5rem;
display: flex;
.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-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;
position: relative;
}
/* ── Active sentence — opacity fade ── */
.hero-active {
opacity: 0;
transition: opacity 1s ease;
.hero-axis dt {
font-family: var(--font-display);
font-weight: 600;
font-size: 0.9rem;
color: hsl(var(--color-primary));
}
.hero-active.is-visible {
opacity: 1;
.hero-axis dt::after {
content: ' →';
color: hsl(var(--color-text) / 0.2);
font-weight: 400;
}
/* ── Locked sentences entrance ── */
.lock-enter-active {
transition: opacity 0.8s ease-out, transform 0.8s ease-out;
}
.lock-enter-from {
opacity: 0;
transform: translateY(8px);
}
.lock-move {
transition: transform 0.5s ease;
.hero-axis dd {
font-family: var(--font-sans);
font-size: 0.88rem;
color: hsl(var(--color-text) / 0.5);
margin: 0;
}
</style>

View File

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

View File

@@ -1,109 +0,0 @@
export interface TypewriterSentence {
text: string
style?: 'title' | 'citation' | 'text'
stays?: boolean
separator?: boolean
}
interface SequenceOptions {
fadeMs?: number
holdMs?: number
gapMs?: number
}
export function useTypewriter(sentences: TypewriterSentence[], options: SequenceOptions = {}) {
const {
fadeMs = 1000,
holdMs = 2800,
gapMs = 300,
} = options
const currentText = ref('')
const currentStyle = ref<string>('title')
const isVisible = ref(false)
const lockedSentences = ref<TypewriterSentence[]>([])
const isComplete = ref(false)
let currentIdx = -1
let timer: ReturnType<typeof setTimeout> | null = null
function clearTimer() {
if (timer) {
clearTimeout(timer)
timer = null
}
}
function next() {
currentIdx++
if (currentIdx >= sentences.length) {
currentText.value = ''
isComplete.value = true
return
}
const sentence = sentences[currentIdx]
if (sentence.separator) {
lockedSentences.value = [...lockedSentences.value, { text: '', separator: true }]
}
// Set text while invisible
currentText.value = sentence.text
currentStyle.value = sentence.style || 'title'
// Fade in on next frame
requestAnimationFrame(() => {
isVisible.value = true
})
// After fade-in + hold → fade out
timer = setTimeout(() => {
isVisible.value = false
// After fade-out completes → lock if stays, then next
timer = setTimeout(() => {
if (sentence.stays) {
lockedSentences.value = [...lockedSentences.value, { ...sentence }]
}
timer = setTimeout(next, gapMs)
}, fadeMs)
}, fadeMs + holdMs)
}
function start() {
next()
}
function skipToEnd() {
clearTimer()
isVisible.value = false
currentText.value = ''
const locked: TypewriterSentence[] = []
for (const sentence of sentences) {
if (sentence.separator) {
locked.push({ text: '', separator: true })
}
if (sentence.stays) {
locked.push({ ...sentence })
}
}
lockedSentences.value = locked
currentIdx = sentences.length - 1
isComplete.value = true
}
onUnmounted(clearTimer)
return {
currentText: readonly(currentText),
currentStyle: readonly(currentStyle),
isVisible,
lockedSentences: readonly(lockedSentences),
isComplete: readonly(isComplete),
start,
skipToEnd,
}
}

View File

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

View File

@@ -74,6 +74,13 @@
</svg>
<div class="container-content">
<!-- Page de couverture du livre -->
<HomeBookSection
class="mb-16"
@open-player="showBookPlayer = true"
@open-pdf="showPdfReader = true"
/>
<header class="mb-12 text-center">
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-bold tracking-tight text-white">
@@ -118,6 +125,9 @@
</ul>
</div>
</div>
<BookPlayer v-model="showBookPlayer" />
<BookPdfReader v-model="showPdfReader" />
</div>
</template>
@@ -135,6 +145,9 @@ useHead({
const { data: chapters } = await useAsyncData('book-toc', () =>
queryCollection('book').order('order', 'ASC').all(),
)
const showBookPlayer = ref(false)
const showPdfReader = ref(false)
</script>
<style scoped>

View File

@@ -117,10 +117,10 @@ definePageMeta({
layout: 'default',
})
const { data: content } = await usePageContent('autonomie')
const { data: content } = await usePageContent('numerique')
useHead({
title: content.value?.meta?.title ?? 'Autonomie',
title: content.value?.meta?.title ?? 'Autonomie numérique',
})
</script>

View File

@@ -20,9 +20,9 @@ export default defineNuxtConfig({
safelist: [
// Axis block icons (dynamic from YAML)
'i-lucide-monitor', 'i-lucide-coins', 'i-lucide-landmark',
'i-lucide-code-2', 'i-lucide-fingerprint', 'i-lucide-cloud',
'i-lucide-circle-dollar-sign', 'i-lucide-heart-handshake', 'i-lucide-users',
'i-lucide-scale', 'i-lucide-droplets', 'i-lucide-calendar-heart',
'i-lucide-code-2', 'i-lucide-share-2', 'i-lucide-cloud',
'i-lucide-scale', 'i-lucide-gavel', 'i-lucide-users',
'i-lucide-droplets', 'i-lucide-calendar-heart',
// Action icons
'i-lucide-play', 'i-lucide-book-open', 'i-lucide-sparkles',
// Decision page

View File

@@ -10,4 +10,8 @@ export default defineEventHandler((event) => {
const rest = path.slice(8) // remove '/ecouter'
return sendRedirect(event, `/en-musique${rest || '/'}`, 301)
}
if (path === '/autonomie' || path === '/autonomie/') {
return sendRedirect(event, '/numerique', 301)
}
})

View File

@@ -1,37 +1,34 @@
hero:
typewriter:
sentences:
- text: "Construire une autonomie collective"
style: title
stays: false
- text: "à l'échelle des bassins de vie."
style: title
stays: false
- text: "Pousser les curseurs de l'autonomie numérique, économique et politique — et s'en donner les moyens"
style: title
stays: true
- text: "Il s'agit d'émancipation"
style: citation
stays: false
- text: "Les trois dimensions qui nous émancipent sont le numérique, l'économie et le politique"
style: citation
stays: false
- text: "Ce sont les 3 axes de l'espace dans lequel nous naviguons"
style: citation
stays: true
separator: true
- text: "Dans chaque dimension, nous adressons ce qui est le plus en amont"
style: text
stays: false
- text: "Pour le numérique, c'est le code source"
style: text
stays: false
- text: "Pour l'économie, c'est la création monétaire"
style: text
stays: false
- text: "Pour le politique, c'est la prise de décision — comment on choisit, comment on décide"
style: text
stays: true
heading:
- "Construire une autonomie collective."
- "à l'échelle des bassins de vie"
- "Pousser les curseurs"
- "Autonomie numérique, économique, citoyenne."
- "— s'en donner les moyens —"
citations:
- "Il s'agit d'émancipation."
- "Les trois dimensions qui nous émancipent sont le numérique, l'économie et le politique."
- "Elles peuvent nous asservir tout autant."
- "Ce sont les 3 axes de l'espace dans lequel nous naviguons."
approach: "Dans chaque dimension, nous adressons ce qui est le plus en amont"
axes:
- label: numérique
value: le code source
- label: économie
value: la création monétaire
- label: citoyenne
value: la décision
book:
kicker: Modèle économique
title: Une économie du don — enfin concevable
description: Un livre et quelques chansons pour une proposition de modèle économique fondé sur le don. Le livre est
accompagné de chansons qui le racontent, un peu autrement.
coverImage: /images/book-cover-spread.jpg
coverAlt: Couverture — Une économie du don, enfin concevable
cta:
player: Présentation musicale
pdf: Lecture du livre
axes:
numerique:
@@ -50,7 +47,7 @@ axes:
description: Une toile de confiance décentralisée, sans autorité centrale. Chaque identité est certifiée par ses pairs.
to: /gestation/authentification-wot
gestation: true
icon: fingerprint
icon: share-2
presentation:
title: trustWallet
text: Gestionnaire de confiances.
@@ -69,21 +66,23 @@ axes:
- label: Monnaie libre
description: "La Ğ1 (June) : une monnaie co-créée par ses membres, sans dette ni intérêt. Le dividende universel comme base."
href: https://monnaie-libre.fr
icon: circle-dollar-sign
icon: g1
- label: Économie du don
description: Un livre et des chansons pour une proposition de modèle économique fondé sur le don.
to: /modele-eco
icon: heart-handshake
icon: scale
actions:
- id: open-player
label: Présentation musicale
icon: play
highlight: true
- id: open-pdf
label: Lecture du livre
icon: book-open
- id: launch-gratewizard
label: grateWizard
icon: sparkles
secondary: true
- label: Productions collectives
description: Une plateforme pour faciliter la création d'équipes et la réalisation de productions à l'échelle des bassins de vie.
to: /gestation/productions-collectives
@@ -96,7 +95,7 @@ axes:
- label: Décision collective
description: Se donner les moyens de la décision collective.
to: /decision
icon: scale
icon: gavel
- label: Tarifs de l'eau
description: Application pour obtenir justice sociale et incitation dynamique à la réduction. Permet de confier la décision à la population des communes.
to: /gestation/tarifs-eau

View File

@@ -1,8 +1,8 @@
kicker: Pourquoi l'autonomie
title: Autonomie
description: Des passages du livre qui éclairent la démarche d'autonomie collective — le fil rouge du projet.
kicker: Autonomie numérique
title: Le code source
description: Des passages du livre qui éclairent la démarche d'autonomie numérique — maîtriser le code source, c'est maîtriser l'outil.
meta:
title: Autonomie
title: Autonomie numérique
extracts:
- chapter: Introduction
chapterSlug: 01-introduction

View File

@@ -4,14 +4,16 @@ identity:
racontent, autrement.
url: https://librodrome.org
navigation:
- label: Numérique
to: /#numerique
- label: Économique
to: /#economique
- label: Citoyenne
to: /#citoyenne
- label: Événement
to: /evenement
axes:
- label: numérique
to: /numerique
- label: économique
to: /modele-eco
- label: citoyenne
to: /decision
extra:
- label: Événement
to: /evenement
footer:
credits: © 2026 Le librodrome — Productions collectives
links: