Files
librodrome/app/pages/numerique/[slug].vue
Yvv 07449de187
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Pages détail numérique : sommaire flottant, nav ctx, shadoks geek, contenu enrichi
- [slug].vue : sommaire sticky (overflow:clip sur parent), prev/next en haut, 6 shadoks geek (pinguin+USB, web-of-trust, rubber-duck, caféine, debugger loupe, rack serveur)
- Nouveaux types de sections : territoire (bouquet sweethomeCloud, 2 modèles éco, tableau matériel dépliable), projet (carte gestation)
- cloud-libre.yml : section sweethomeCloud complète avec infra 50 000 hab. (~2€/an/hab)
- authentification-wot.yml : trustWallet, correction WoT Duniter (Ed25519+Scrypt, sigQty=5, stepMax=3), DID/VC standards
- logiciel-libre.yml : carte projet wishBounty
- home.yml + numerique.yml : cloud-libre → sweethomeCloud, description RGPD/local-first
- AxisBlock.vue : bulles de présentation inline dans les cards (plus de tooltip absolu)
- Analytics : useTracking.ts (Umami), docker-compose.umami.yml, /api/stats fédération
- nuxt.config.ts : config Umami runtime

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 04:40:48 +01:00

1903 lines
60 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="page-outer section-padding">
<!-- Shadok pinguin tenant une clé USB, petit Tux à ses pieds -->
<svg class="shadok-dl shadok-dl-pinguin" viewBox="0 0 170 210" fill="none" aria-hidden="true">
<!-- Small Tux at bottom -->
<ellipse cx="30" cy="175" rx="12" ry="16" fill="currentColor" opacity="0.2"/>
<circle cx="30" cy="158" r="9" fill="currentColor" opacity="0.22"/>
<ellipse cx="30" cy="173" rx="6" ry="9" fill="currentColor" opacity="0.1"/>
<polygon points="30,157 25,154 30,161" fill="currentColor" opacity="0.3"/>
<ellipse cx="25" cy="192" rx="6" ry="2.5" fill="currentColor" opacity="0.2"/>
<ellipse cx="35" cy="192" rx="6" ry="2.5" fill="currentColor" opacity="0.2"/>
<!-- Shadok body upright -->
<ellipse cx="105" cy="110" rx="20" ry="26" fill="currentColor" opacity="0.25"/>
<!-- Head looking at USB key -->
<circle cx="100" cy="70" r="15" fill="currentColor" opacity="0.3"/>
<circle cx="95" cy="68" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="104" cy="67" r="1.8" fill="currentColor" opacity="0.45"/>
<polygon points="88,72 80,69 88,76" fill="currentColor" opacity="0.35"/>
<!-- Left arm holding USB key -->
<line x1="88" y1="100" x2="68" y2="90" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<rect x="42" y="82" width="26" height="14" rx="3" fill="currentColor" opacity="0.28"/>
<rect x="32" y="86" width="10" height="6" rx="1" fill="currentColor" opacity="0.22"/>
<line x1="50" y1="85" x2="60" y2="85" stroke="currentColor" stroke-width="1" opacity="0.2"/>
<line x1="50" y1="89" x2="60" y2="89" stroke="currentColor" stroke-width="1" opacity="0.2"/>
<!-- Right arm relaxed -->
<line x1="122" y1="105" x2="140" y2="125" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Legs -->
<line x1="97" y1="133" x2="87" y2="192" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="113" y1="133" x2="123" y2="192" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="85" cy="195" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="125" cy="195" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok toile au centre d'une web of trust, bras ouverts -->
<svg class="shadok-dl shadok-dl-toile" viewBox="0 0 180 210" fill="none" aria-hidden="true">
<!-- Nodes web -->
<circle cx="30" cy="50" r="3" fill="currentColor" opacity="0.18"/>
<circle cx="155" cy="55" r="3" fill="currentColor" opacity="0.16"/>
<circle cx="18" cy="140" r="3" fill="currentColor" opacity="0.14"/>
<circle cx="162" cy="142" r="3" fill="currentColor" opacity="0.16"/>
<circle cx="80" cy="18" r="3" fill="currentColor" opacity="0.18"/>
<circle cx="148" cy="185" r="2.5" fill="currentColor" opacity="0.14"/>
<circle cx="15" cy="92" r="2.5" fill="currentColor" opacity="0.12"/>
<!-- Connections from center -->
<line x1="90" y1="105" x2="30" y2="50" stroke="currentColor" stroke-width="1" opacity="0.12"/>
<line x1="90" y1="105" x2="155" y2="55" stroke="currentColor" stroke-width="1" opacity="0.12"/>
<line x1="90" y1="105" x2="18" y2="140" stroke="currentColor" stroke-width="1" opacity="0.1"/>
<line x1="90" y1="105" x2="162" y2="142" stroke="currentColor" stroke-width="1" opacity="0.1"/>
<line x1="90" y1="105" x2="80" y2="18" stroke="currentColor" stroke-width="1" opacity="0.12"/>
<line x1="90" y1="105" x2="15" y2="92" stroke="currentColor" stroke-width="0.8" opacity="0.1"/>
<line x1="30" y1="50" x2="80" y2="18" stroke="currentColor" stroke-width="0.8" opacity="0.08"/>
<line x1="155" y1="55" x2="162" y2="142" stroke="currentColor" stroke-width="0.8" opacity="0.08"/>
<!-- Body — bras ouverts -->
<ellipse cx="90" cy="118" rx="20" ry="26" fill="currentColor" opacity="0.25"/>
<circle cx="90" cy="76" r="15" fill="currentColor" opacity="0.3"/>
<circle cx="84" cy="74" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="95" cy="73" r="1.8" fill="currentColor" opacity="0.45"/>
<!-- Beak front-facing proud -->
<polygon points="90,83 85,87 95,87" fill="currentColor" opacity="0.35"/>
<!-- Arms spread wide -->
<line x1="72" y1="108" x2="32" y2="88" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="108" y1="108" x2="150" y2="90" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<circle cx="31" cy="88" r="2.5" fill="currentColor" opacity="0.3"/>
<circle cx="151" cy="90" r="2.5" fill="currentColor" opacity="0.3"/>
<!-- Legs -->
<line x1="82" y1="141" x2="72" y2="196" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="98" y1="141" x2="110" y2="196" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="70" cy="199" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="112" cy="199" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok nas — ployant sous le poids d'un rack serveur -->
<svg class="shadok-dl shadok-dl-nas" viewBox="0 0 170 210" fill="none" aria-hidden="true">
<!-- Server rack on head, slightly tilted -->
<rect x="32" y="10" width="82" height="52" rx="4" fill="currentColor" opacity="0.2" transform="rotate(-2 73 36)"/>
<!-- Server units -->
<rect x="38" y="15" width="68" height="7" rx="2" fill="currentColor" opacity="0.15" transform="rotate(-2 72 19)"/>
<rect x="38" y="25" width="68" height="7" rx="2" fill="currentColor" opacity="0.15" transform="rotate(-2 72 29)"/>
<rect x="38" y="35" width="68" height="7" rx="2" fill="currentColor" opacity="0.15" transform="rotate(-2 72 39)"/>
<rect x="38" y="45" width="68" height="7" rx="2" fill="currentColor" opacity="0.15" transform="rotate(-2 72 49)"/>
<!-- LED indicators -->
<circle cx="46" cy="19" r="1.5" fill="currentColor" opacity="0.3"/>
<circle cx="46" cy="29" r="1.5" fill="currentColor" opacity="0.28"/>
<circle cx="46" cy="39" r="1.5" fill="currentColor" opacity="0.32"/>
<circle cx="46" cy="49" r="1.5" fill="currentColor" opacity="0.25"/>
<!-- Body bent forward under weight -->
<ellipse cx="85" cy="120" rx="22" ry="25" fill="currentColor" opacity="0.25" transform="rotate(8 85 120)"/>
<!-- Head strained under rack -->
<circle cx="78" cy="82" r="15" fill="currentColor" opacity="0.3"/>
<circle cx="73" cy="80" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="83" cy="79" r="1.8" fill="currentColor" opacity="0.45"/>
<!-- Beak grimacing left -->
<polygon points="65,84 57,81 65,88" fill="currentColor" opacity="0.35"/>
<!-- Arms up holding rack -->
<line x1="68" y1="110" x2="50" y2="70" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="104" y1="109" x2="112" y2="67" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Legs pliant sous l'effort -->
<line x1="75" y1="142" x2="58" y2="193" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.4"/>
<line x1="97" y1="142" x2="114" y2="193" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="56" cy="196" rx="11" ry="3.5" fill="currentColor" opacity="0.3"/>
<ellipse cx="116" cy="196" rx="11" ry="3.5" fill="currentColor" opacity="0.3"/>
<!-- Sweat drops -->
<ellipse cx="48" cy="90" rx="2" ry="3" fill="currentColor" opacity="0.2"/>
<ellipse cx="44" cy="102" rx="1.5" ry="2.5" fill="currentColor" opacity="0.15"/>
</svg>
<!-- Shadok debugger — loupe géante sur un minuscule bug, yeux écarquillés -->
<svg class="shadok-dl shadok-dl-debugger" viewBox="0 0 180 215" fill="none" aria-hidden="true">
<!-- Loupe — grande, tenue à bout de bras -->
<circle cx="38" cy="55" r="30" stroke="currentColor" stroke-width="3.5" fill="none" opacity="0.28"/>
<circle cx="38" cy="55" r="23" fill="currentColor" opacity="0.06"/>
<!-- Manche loupe -->
<line x1="62" y1="78" x2="80" y2="98" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity="0.3"/>
<!-- Minuscule bug dans la loupe -->
<ellipse cx="36" cy="53" rx="5" ry="4" fill="currentColor" opacity="0.35"/>
<line x1="32" y1="50" x2="28" y2="46" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity="0.3"/>
<line x1="36" y1="49" x2="36" y2="44" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity="0.3"/>
<line x1="40" y1="50" x2="44" y2="46" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity="0.3"/>
<line x1="32" y1="57" x2="28" y2="61" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity="0.3"/>
<line x1="40" y1="57" x2="44" y2="61" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity="0.3"/>
<!-- Exclamation dans la loupe -->
<text x="50" y="45" font-size="10" fill="currentColor" opacity="0.4" font-family="monospace">!</text>
<!-- Body — légèrement penché -->
<ellipse cx="125" cy="118" rx="20" ry="26" fill="currentColor" opacity="0.25" transform="rotate(-5 125 118)"/>
<!-- Head — très proche de la loupe, yeux écarquillés -->
<circle cx="118" cy="75" r="16" fill="currentColor" opacity="0.3"/>
<!-- Cercles sous les yeux (nuits blanches) -->
<ellipse cx="112" cy="76" rx="5" ry="4" stroke="currentColor" stroke-width="1" fill="none" opacity="0.2"/>
<ellipse cx="124" cy="75" rx="5" ry="4" stroke="currentColor" stroke-width="1" fill="none" opacity="0.2"/>
<!-- Yeux très grands -->
<circle cx="113" cy="75" r="2.5" fill="currentColor" opacity="0.55"/>
<circle cx="123" cy="74" r="2.5" fill="currentColor" opacity="0.55"/>
<!-- Beak — pointant vers la loupe -->
<polygon points="104,78 96,75 104,83" fill="currentColor" opacity="0.35"/>
<!-- Bras gauche tenant loupe -->
<line x1="108" y1="108" x2="82" y2="95" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Bras droit -->
<line x1="140" y1="110" x2="158" y2="130" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Jambes -->
<line x1="117" y1="141" x2="107" y2="198" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="133" y1="141" x2="143" y2="198" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="105" cy="201" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="145" cy="201" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok rubber duck — assis par terre, canard en caoutchouc face à lui, bras croisés, explication intense -->
<svg class="shadok-dl shadok-dl-duck" viewBox="0 0 180 210" fill="none" aria-hidden="true">
<!-- Rubber duck — stylisé, face au shadok -->
<ellipse cx="145" cy="165" rx="18" ry="14" fill="currentColor" opacity="0.22"/>
<circle cx="145" cy="148" r="11" fill="currentColor" opacity="0.25"/>
<!-- Duck beak -->
<path d="M155 148 L165 146 L155 152 Z" fill="currentColor" opacity="0.3"/>
<!-- Duck eye -->
<circle cx="150" cy="145" r="2" fill="currentColor" opacity="0.4"/>
<!-- Duck wing -->
<path d="M128 162 Q135 158 130 170" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.2"/>
<!-- Shadok — assis, jambes en avant -->
<ellipse cx="82" cy="130" rx="22" ry="20" fill="currentColor" opacity="0.25" transform="rotate(20 82 130)"/>
<!-- Head — très expressif, se penche vers le canard -->
<circle cx="95" cy="90" r="16" fill="currentColor" opacity="0.3"/>
<circle cx="89" cy="88" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="100" cy="87" r="2" fill="currentColor" opacity="0.45"/>
<!-- Beak pointing right at duck -->
<polygon points="108,92 116,89 108,96" fill="currentColor" opacity="0.35"/>
<!-- Bulle de dialogue -->
<path d="M115 72 Q140 60 148 72 Q155 82 145 88 Q135 94 118 88 L115 94 L113 86 Q108 80 115 72Z" fill="currentColor" opacity="0.1"/>
<text x="120" y="82" font-size="7" fill="currentColor" opacity="0.35" font-family="monospace">WTF?</text>
<!-- Bras pointant vers duck -->
<line x1="105" y1="118" x2="130" y2="140" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Bras gauche appuyé au sol -->
<line x1="62" y1="122" x2="42" y2="148" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Jambes en avant (assis) -->
<line x1="72" y1="148" x2="42" y2="185" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="92" y1="152" x2="115" y2="185" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="40" cy="188" rx="10" ry="3.5" fill="currentColor" opacity="0.3"/>
<ellipse cx="117" cy="188" rx="10" ry="3.5" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok caféine — énorme tasse, cernes, jambes qui tremblent -->
<svg class="shadok-dl shadok-dl-cafeine" viewBox="0 0 170 215" fill="none" aria-hidden="true">
<!-- Tasses vides au sol -->
<ellipse cx="25" cy="195" rx="8" ry="4" fill="currentColor" opacity="0.18"/>
<rect x="17" y="183" width="16" height="12" rx="2" fill="currentColor" opacity="0.15"/>
<ellipse cx="52" cy="198" rx="6" ry="3" fill="currentColor" opacity="0.14"/>
<rect x="46" y="188" width="12" height="10" rx="2" fill="currentColor" opacity="0.12"/>
<!-- Énorme tasse à 2 mains -->
<rect x="48" y="68" width="52" height="45" rx="5" fill="currentColor" opacity="0.25"/>
<ellipse cx="74" cy="68" rx="26" ry="6" fill="currentColor" opacity="0.2"/>
<!-- Vapeur -->
<path d="M62 62 Q65 52 62 44" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.25" stroke-linecap="round"/>
<path d="M74 58 Q78 47 74 38" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.22" stroke-linecap="round"/>
<path d="M86 62 Q89 52 86 44" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.2" stroke-linecap="round"/>
<!-- Anse tasse -->
<path d="M100 78 Q118 78 118 90 Q118 102 100 102" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.22"/>
<!-- Logo tasse (skull) -->
<text x="62" y="95" font-size="14" fill="currentColor" opacity="0.18" font-family="monospace">☕</text>
<!-- Body — tassé, fatigué -->
<ellipse cx="90" cy="130" rx="20" ry="24" fill="currentColor" opacity="0.25" transform="rotate(5 90 130)"/>
<!-- Head — yeux mi-clos, cernes -->
<circle cx="88" cy="92" r="15" fill="currentColor" opacity="0.28"/>
<!-- Cernes (demi-cercles sous yeux) -->
<path d="M80 93 Q83 97 86 93" stroke="currentColor" stroke-width="1.2" fill="none" opacity="0.3"/>
<path d="M90 92 Q93 96 96 92" stroke="currentColor" stroke-width="1.2" fill="none" opacity="0.3"/>
<!-- Yeux mi-clos -->
<line x1="79" y1="90" x2="85" y2="90" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
<line x1="90" y1="89" x2="96" y2="89" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
<!-- Beak -->
<polygon points="78,97 70,94 78,101" fill="currentColor" opacity="0.35"/>
<!-- Les deux bras tenant la tasse -->
<line x1="72" y1="116" x2="56" y2="100" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="108" y1="118" x2="108" y2="100" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Jambes qui tremblent (légèrement en zigzag) -->
<path d="M80 152 Q76 165 80 175 Q84 185 78 196" stroke="currentColor" stroke-width="3" stroke-linecap="round" fill="none" opacity="0.4"/>
<path d="M100 152 Q104 165 100 175 Q96 185 102 196" stroke="currentColor" stroke-width="3" stroke-linecap="round" fill="none" opacity="0.4"/>
<ellipse cx="76" cy="199" rx="10" ry="3.5" fill="currentColor" opacity="0.3"/>
<ellipse cx="104" cy="199" rx="10" ry="3.5" fill="currentColor" opacity="0.3"/>
</svg>
<div class="container-content">
<!-- Header — centré, max-2xl -->
<div class="mx-auto max-w-2xl text-center mb-10">
<div class="section-icon mx-auto mb-6">
<div :class="`i-lucide-${content?.icon ?? 'monitor'}`" class="h-12 w-12" />
</div>
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase">
{{ content?.kicker }}
</p>
<h1 class="font-display text-3xl font-bold mb-4" style="color: hsl(var(--color-text))">
{{ content?.title ?? slug }}
</h1>
<p class="text-lg leading-relaxed" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
</div>
<!-- Project card (legacy pages) -->
<div v-if="content?.project" class="mx-auto max-w-2xl mb-8">
<div class="project-card">
<div class="project-icon">
<div class="i-lucide-rocket h-5 w-5" />
</div>
<h2 class="font-display text-xl font-semibold mb-2" style="color: hsl(var(--color-text))">
{{ content.project.name }}
</h2>
<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>
</div>
</div>
<!-- Extended content (legacy) -->
<div v-if="content?.content" class="mx-auto max-w-2xl mb-10">
<div class="prose-block">
<p class="leading-relaxed whitespace-pre-line" style="color: hsl(var(--color-text-muted))">
{{ content.content }}
</p>
</div>
</div>
<!-- Navigation prev / index / next — en haut -->
<nav class="ctx-nav mx-auto max-w-2xl mb-10">
<NuxtLink v-if="prevItem?.to" :to="prevItem.to" class="ctx-nav-btn ctx-nav-prev">
<div class="i-lucide-arrow-left h-4 w-4 shrink-0" />
<span>{{ prevItem.label }}</span>
</NuxtLink>
<div v-else class="ctx-nav-spacer" />
<NuxtLink to="/numerique" class="ctx-nav-btn ctx-nav-index">
<div class="i-lucide-layout-grid h-4 w-4" />
<span>Numérique</span>
</NuxtLink>
<NuxtLink v-if="nextItem?.to" :to="nextItem.to" class="ctx-nav-btn ctx-nav-next">
<span>{{ nextItem.label }}</span>
<div class="i-lucide-arrow-right h-4 w-4 shrink-0" />
</NuxtLink>
<div v-else class="ctx-nav-spacer" />
</nav>
<!-- Zone sections — position:relative pour ancrer le sidebar absolu -->
<div class="sections-area">
<!-- Sidebar sommaire — absolu, ne perturbe pas le flux, sticky en scroll -->
<aside v-if="sommaire.length > 1" class="page-sidebar">
<nav class="sommaire-sidebar">
<p class="sommaire-sidebar-title">Sur cette page</p>
<ol class="sommaire-sidebar-list">
<li v-for="(entry, ei) in sommaire" :key="ei">
<a :href="`#${entry.id}`" class="sommaire-sidebar-link">
<span class="sommaire-n">{{ ei + 1 }}</span>
<span>{{ entry.title }}</span>
</a>
</li>
</ol>
</nav>
</aside>
<!-- Rich sections — mx-auto conservé, centrage intact -->
<template v-if="content?.sections?.length">
<div
v-for="(section, si) in content.sections"
:key="si"
:id="`s${si}`"
class="mb-12 section-anchor"
:class="['equivalents', 'tiers', 'territoire'].includes(section.type) ? 'max-w-4xl mx-auto' : 'max-w-2xl mx-auto'"
>
<!-- ── ARGUMENTS ── -->
<template v-if="section.type === 'arguments'">
<h2 class="section-title">{{ section.title }}</h2>
<div class="args-grid">
<div v-for="(arg, i) in section.items" :key="i" class="arg-card">
<div class="arg-icon">
<div :class="`i-lucide-${arg.icon}`" class="h-5 w-5" />
</div>
<h3 class="arg-title">{{ arg.title }}</h3>
<p class="arg-text">{{ arg.text }}</p>
</div>
</div>
</template>
<!-- ── FICHE ── -->
<template v-else-if="section.type === 'fiche'">
<h2 class="section-title">{{ section.title }}</h2>
<div class="fiche-steps">
<div v-for="(step, i) in section.steps" :key="i" class="fiche-step">
<div class="step-num">{{ step.n }}</div>
<div class="step-body">
<h3 class="step-title">{{ step.title }}</h3>
<p class="step-text">{{ step.text }}</p>
<p v-if="step.tip" class="step-tip">
<span class="i-lucide-lightbulb h-3.5 w-3.5 inline-block mr-1 align-middle" />
{{ step.tip }}
</p>
</div>
</div>
</div>
</template>
<!-- ── EQUIVALENTS ── -->
<template v-else-if="section.type === 'equivalents'">
<h2 class="section-title">{{ section.title }}</h2>
<div class="equiv-categories">
<div v-for="(cat, ci) in section.categories" :key="ci" class="equiv-cat">
<h3 class="equiv-cat-label">{{ cat.label }}</h3>
<div class="equiv-rows">
<component
:is="item.url ? 'a' : 'div'"
v-for="(item, ii) in cat.items"
:key="ii"
v-bind="item.url ? { href: item.url, target: '_blank', rel: 'noopener' } : {}"
class="equiv-row"
:class="{ 'equiv-row--no-link': !item.url }"
>
<span class="equiv-from">{{ item.from }}</span>
<span class="equiv-arrow">→</span>
<span class="equiv-to">{{ item.to }}</span>
<span class="equiv-note">{{ item.note }}</span>
<span v-if="item.url" class="i-lucide-external-link equiv-ext h-3.5 w-3.5" />
</component>
</div>
</div>
</div>
</template>
<!-- ── LLM ── -->
<template v-else-if="section.type === 'llm'">
<h2 class="section-title">{{ section.title }}</h2>
<p class="llm-intro">{{ section.text }}</p>
<div class="llm-meta">
<span class="llm-badge">
<span class="i-lucide-cpu h-3.5 w-3.5" />
{{ section.tool }}
</span>
<span class="llm-badge">
<span class="i-lucide-zap h-3.5 w-3.5" />
{{ section.model }}
</span>
<span class="llm-badge">
<span class="i-lucide-memory-stick h-3.5 w-3.5" />
{{ section.ram }}
</span>
</div>
<div class="llm-commands">
<code v-for="(cmd, ci) in section.commands" :key="ci" class="llm-cmd">{{ cmd }}</code>
</div>
<ul class="llm-rules">
<li v-for="(rule, ri) in section.rules" :key="ri" class="llm-rule">
<span class="i-lucide-shield-check h-3.5 w-3.5 shrink-0 mt-0.5" />
{{ rule }}
</li>
</ul>
</template>
<!-- ── ATELIER ── -->
<template v-else-if="section.type === 'atelier'">
<h2 class="section-title">{{ section.title }}</h2>
<p class="atelier-text">{{ section.text }}</p>
<p class="atelier-format">
<span class="i-lucide-calendar h-4 w-4 inline-block mr-1 align-middle" />
{{ section.format }}
</p>
<ul class="atelier-programme">
<li v-for="(item, pi) in section.programme" :key="pi" class="atelier-item">
<div :class="`i-lucide-${item.icon}`" class="h-4 w-4 shrink-0 mt-0.5 text-primary" />
{{ item.label }}
</li>
</ul>
</template>
<!-- ── INSIGHT ── -->
<template v-else-if="section.type === 'insight'">
<div class="insight-box" :class="`insight-box--${section.variant || 'info'}`">
<div class="insight-header">
<div :class="`i-lucide-${section.icon || 'info'}`" class="h-5 w-5 shrink-0" />
<h2 class="insight-title">{{ section.title }}</h2>
</div>
<p class="insight-text">{{ section.text }}</p>
<ul v-if="section.points?.length" class="insight-points">
<li v-for="(pt, pi) in section.points" :key="pi">{{ pt }}</li>
</ul>
</div>
</template>
<!-- ── TIERS ── -->
<template v-else-if="section.type === 'tiers'">
<h2 class="section-title">{{ section.title }}</h2>
<div class="tiers-grid">
<div v-for="(tier, ti) in section.tiers" :key="ti" class="tier-card">
<div class="tier-top">
<span class="tier-level">{{ tier.level }}</span>
<span class="tier-badge">{{ tier.badge }}</span>
</div>
<div class="tier-icon">
<div :class="`i-lucide-${tier.icon}`" class="h-5 w-5" />
</div>
<h3 class="tier-title">{{ tier.title }}</h3>
<p class="tier-text">{{ tier.text }}</p>
<ul class="tier-tools">
<li v-for="(tool, gi) in tier.tools" :key="gi">{{ tool }}</li>
</ul>
</div>
</div>
</template>
<!-- ── TERRITOIRE ── -->
<template v-else-if="section.type === 'territoire'">
<div class="territoire-card">
<!-- Header -->
<div class="territoire-header">
<div class="territoire-icon">
<div :class="`i-lucide-${section.icon || 'map-pin'}`" class="h-6 w-6" />
</div>
<div>
<h2 class="territoire-title">{{ section.title }}</h2>
<p class="territoire-text">{{ section.text }}</p>
</div>
</div>
<!-- Modèles économiques -->
<div v-if="section.economies?.length" class="territoire-economies">
<div v-for="(eco, ei) in section.economies" :key="ei" class="territoire-eco">
<div class="territoire-eco-icon">
<span v-if="eco.icon === 'g1'" class="territoire-g1">Ğ1</span>
<div v-else :class="`i-lucide-${eco.icon}`" class="h-4 w-4" />
</div>
<div>
<p class="territoire-eco-label">{{ eco.label }}</p>
<p class="territoire-eco-desc">{{ eco.desc }}</p>
</div>
</div>
</div>
<!-- Bouquet sweethomeCloud -->
<div v-if="section.bouquet" class="territoire-bouquet">
<h3 class="territoire-bouquet-title">
<div class="i-lucide-package h-4 w-4" />
{{ section.bouquet.title }}
</h3>
<div class="territoire-bouquet-grid">
<div
v-for="(cat, ci) in section.bouquet.categories"
:key="ci"
class="territoire-bouquet-cat"
>
<div class="territoire-bouquet-cat-header">
<div :class="`i-lucide-${cat.icon}`" class="h-3.5 w-3.5" />
<span>{{ cat.label }}</span>
</div>
<ul class="territoire-bouquet-items">
<li v-for="(item, ii) in cat.items" :key="ii">{{ item }}</li>
</ul>
</div>
</div>
</div>
<!-- Estimation matérielle (toggle natif) -->
<details v-if="section.estimate" class="territoire-estimate">
<summary class="territoire-estimate-toggle">
<div class="i-lucide-server h-4 w-4" />
{{ section.estimate.toggle }}
<div class="i-lucide-chevron-down territoire-chevron h-4 w-4" />
</summary>
<div class="territoire-estimate-body">
<p v-if="section.estimate.note" class="territoire-estimate-note">
<span class="i-lucide-info h-3.5 w-3.5 inline-block mr-1 align-middle" />
{{ section.estimate.note }}
</p>
<div class="territoire-hw-table">
<div class="territoire-hw-header">
<span>Rôle</span>
<span>Matériel</span>
<span>Coût indicatif</span>
</div>
<div
v-for="(hw, hi) in section.estimate.hardware"
:key="hi"
class="territoire-hw-row"
>
<div class="territoire-hw-role">{{ hw.role }}</div>
<div class="territoire-hw-detail">
<span>{{ hw.detail }}</span>
<span class="territoire-hw-usage">{{ hw.usage }}</span>
<span v-if="hw.note" class="territoire-hw-note">{{ hw.note }}</span>
</div>
<div class="territoire-hw-cost">{{ hw.cost }}</div>
</div>
</div>
<div v-if="section.estimate.totals" class="territoire-totals">
<div class="territoire-total-row">
<span class="territoire-total-label">Investissement initial</span>
<span class="territoire-total-value">{{ section.estimate.totals.invest }}</span>
</div>
<div class="territoire-total-row">
<span class="territoire-total-label">Fonctionnement</span>
<span class="territoire-total-value">{{ section.estimate.totals.annual }}</span>
</div>
<div class="territoire-total-row territoire-total-row--highlight">
<span class="territoire-total-label">Par habitant</span>
<span class="territoire-total-value">{{ section.estimate.totals.per_person }}</span>
</div>
<p v-if="section.estimate.totals.note" class="territoire-totals-note">
{{ section.estimate.totals.note }}
</p>
</div>
</div>
</details>
</div>
</template>
<!-- ── PROJET (gestation) ── -->
<template v-else-if="section.type === 'projet'">
<div class="projet-card">
<div class="projet-header">
<div class="projet-icon">
<div :class="`i-lucide-${section.icon || 'rocket'}`" class="h-6 w-6" />
</div>
<div class="projet-header-text">
<p class="projet-kicker">{{ section.kicker }}</p>
<h2 class="projet-title">{{ section.title }}</h2>
</div>
<span class="projet-badge">
<div class="i-lucide-flask-conical h-3 w-3" />
{{ section.badge || 'En gestation' }}
</span>
</div>
<p class="projet-text">{{ section.text }}</p>
<ul v-if="section.features?.length" class="projet-features">
<li v-for="(feat, fi) in section.features" :key="fi" class="projet-feature">
<div :class="`i-lucide-${feat.icon}`" class="h-4 w-4 shrink-0" />
{{ feat.label }}
</li>
</ul>
</div>
</template>
<!-- ── LINKS ── -->
<template v-else-if="section.type === 'links'">
<h2 class="section-title">{{ section.title }}</h2>
<ul class="links-list">
<li v-for="(lnk, li) in section.items" :key="li">
<a :href="lnk.url" target="_blank" rel="noopener" class="link-row">
<span class="link-label">{{ lnk.label }}</span>
<span class="link-desc">{{ lnk.desc }}</span>
<span class="i-lucide-external-link h-3.5 w-3.5 text-primary/50 shrink-0" />
</a>
</li>
</ul>
</template>
</div>
</template>
</div><!-- /sections-area -->
<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" />
Lancer SejeteralO
</UiBaseButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const slug = route.params.slug as string
const [{ data: content }, { data: homeData }] = await Promise.all([
usePageContent(`numerique/${slug}`),
usePageContent('home'),
])
const appConfig = useAppConfig()
const sejeteral0Url = (appConfig.sejeteral0 as { url: string })?.url ?? '#'
// Sommaire — titres des sections ayant un titre
interface Section { type: string; title?: string }
const sommaire = computed(() =>
(content.value?.sections as Section[] ?? [])
.map((s, i) => ({ title: s.title, id: `s${i}` }))
.filter(s => !!s.title),
)
// Prev / next dans la section numérique
interface AxisItem { label: string; to?: string; icon?: string }
const sectionItems = computed<AxisItem[]>(
() => (homeData.value as Record<string, unknown> | null)?.axes?.numerique?.items as AxisItem[] ?? [],
)
const currentPath = `/numerique/${slug}`
const currentIdx = computed(() => sectionItems.value.findIndex(i => i.to === currentPath))
const prevItem = computed(() => currentIdx.value > 0 ? sectionItems.value[currentIdx.value - 1] : null)
const nextItem = computed(() => currentIdx.value < sectionItems.value.length - 1 ? sectionItems.value[currentIdx.value + 1] : null)
useHead({
title: content.value?.meta?.title ?? slug,
})
</script>
<style scoped>
.section-icon {
display: flex;
align-items: center;
justify-content: center;
width: 5rem;
height: 5rem;
border-radius: 1rem;
background: hsl(var(--color-primary) / 0.1);
border: 1px solid hsl(var(--color-primary) / 0.2);
color: hsl(var(--color-primary));
}
.project-card {
padding: 1.5rem;
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-primary) / 0.15);
background: hsl(var(--color-surface));
}
.project-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
margin-bottom: 1rem;
}
.gestation-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
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);
}
.prose-block {
padding: 1.5rem;
border-radius: 0.75rem;
background: hsl(var(--color-surface));
}
/* scroll-margin-top pour compenser le header fixe */
.section-anchor {
scroll-margin-top: 5.5rem;
}
/* ── Navigation prev/next (en haut) ── */
.ctx-nav {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 0.5rem;
align-items: center;
}
.ctx-nav-spacer { min-width: 0; }
.ctx-nav-btn {
display: flex;
align-items: center;
gap: 0.45rem;
padding: 0.5rem 0.875rem;
border-radius: 20px;
text-decoration: none;
font-size: 0.8rem;
font-weight: 500;
color: hsl(var(--color-text-muted));
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-text) / 0.08);
transition: all 0.15s;
min-width: 0;
}
.ctx-nav-btn:hover {
color: hsl(var(--color-primary));
border-color: hsl(var(--color-primary) / 0.25);
background: hsl(var(--color-primary) / 0.05);
}
.ctx-nav-prev { justify-content: flex-start; }
.ctx-nav-next { justify-content: flex-end; }
.ctx-nav-prev span,
.ctx-nav-next span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ctx-nav-index {
justify-content: center;
background: hsl(var(--color-primary) / 0.08);
border-color: hsl(var(--color-primary) / 0.15);
color: hsl(var(--color-primary));
font-size: 0.75rem;
padding: 0.45rem 0.75rem;
white-space: nowrap;
flex-shrink: 0;
}
.ctx-nav-index:hover { background: hsl(var(--color-primary) / 0.15); }
@media (max-width: 480px) {
.ctx-nav { grid-template-columns: auto 1fr auto; }
.ctx-nav-prev span, .ctx-nav-next span { display: none; }
.ctx-nav-prev, .ctx-nav-next { padding: 0.5rem; }
}
/* ── Zone sections — relative pour ancrer le sidebar ── */
.sections-area {
position: relative;
}
/* ── Sidebar sommaire — position absolue, ne décale pas le contenu ── */
.page-sidebar {
display: none; /* caché par défaut (mobile) */
position: absolute;
top: 3.5rem;
right: 0;
width: 10.5rem;
height: 100%;
}
/* Activé uniquement quand il y a assez de place (>= largeur max-w-4xl + sidebar) */
@media (min-width: 1300px) {
.page-sidebar {
display: block;
}
}
.sommaire-sidebar {
position: sticky;
top: 5.5rem;
padding: 0.875rem;
border-radius: 14px;
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-text) / 0.07);
}
.sommaire-sidebar-title {
font-family: var(--font-mono);
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: hsl(var(--color-text-muted));
margin: 0 0 0.6rem;
}
.sommaire-sidebar-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.sommaire-sidebar-link {
display: flex;
align-items: baseline;
gap: 0.45rem;
padding: 0.28rem 0.4rem;
border-radius: 8px;
text-decoration: none;
font-size: 0.75rem;
color: hsl(var(--color-text-muted));
line-height: 1.4;
transition: color 0.12s, background 0.12s;
}
.sommaire-sidebar-link:hover {
color: hsl(var(--color-primary));
background: hsl(var(--color-primary) / 0.06);
}
.sommaire-n {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.2rem;
height: 1.2rem;
border-radius: 50%;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
font-family: var(--font-mono);
font-size: 0.6rem;
font-weight: 700;
flex-shrink: 0;
}
/* ── Section titles ── */
.section-title {
font-family: var(--font-display);
font-size: clamp(1.1rem, 2.5vw, 1.35rem);
font-weight: 700;
color: hsl(var(--color-text));
margin: 0 0 1.25rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid hsl(var(--color-primary) / 0.15);
}
/* ── Arguments ── */
.args-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 0.875rem;
}
.arg-card {
padding: 1rem 1.1rem;
border-radius: 14px;
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-primary) / 0.08);
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.arg-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 24px hsl(var(--color-primary) / 0.1);
}
.arg-icon {
width: 2rem;
height: 2rem;
border-radius: 8px;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.6rem;
}
.arg-title {
font-family: var(--font-display);
font-size: 0.9rem;
font-weight: 600;
color: hsl(var(--color-text));
margin: 0 0 0.4rem;
}
.arg-text {
font-size: 0.82rem;
line-height: 1.55;
color: hsl(var(--color-text-muted));
margin: 0;
}
/* ── Fiche steps ── */
.fiche-steps {
display: flex;
flex-direction: column;
gap: 0;
}
.fiche-step {
display: flex;
gap: 1rem;
padding: 1rem 0;
border-bottom: 1px solid hsl(var(--color-text) / 0.06);
}
.fiche-step:last-child {
border-bottom: none;
}
.step-num {
width: 2rem;
height: 2rem;
border-radius: 50%;
background: hsl(var(--color-primary) / 0.12);
color: hsl(var(--color-primary));
font-family: var(--font-mono);
font-size: 0.8rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 0.1rem;
}
.step-body {
flex: 1;
}
.step-title {
font-family: var(--font-display);
font-size: 0.95rem;
font-weight: 600;
color: hsl(var(--color-text));
margin: 0 0 0.3rem;
}
.step-text {
font-size: 0.85rem;
line-height: 1.55;
color: hsl(var(--color-text-muted));
margin: 0;
}
.step-tip {
margin: 0.5rem 0 0;
font-size: 0.78rem;
color: hsl(var(--color-accent));
font-style: italic;
}
/* ── Equivalents ── */
.equiv-categories {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.equiv-cat-label {
font-family: var(--font-mono);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: hsl(var(--color-primary));
margin: 0 0 0.5rem;
}
.equiv-rows {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.equiv-row {
display: grid;
grid-template-columns: 1fr auto 1fr 1.5fr auto;
align-items: center;
gap: 0.5rem 0.75rem;
padding: 0.5rem 0.75rem;
border-radius: 10px;
background: hsl(var(--color-surface));
border: 1px solid transparent;
text-decoration: none;
transition: border-color 0.12s, background 0.12s;
}
.equiv-row:hover {
border-color: hsl(var(--color-primary) / 0.2);
background: hsl(var(--color-primary) / 0.04);
}
.equiv-from {
font-size: 0.82rem;
color: hsl(var(--color-text-muted));
text-decoration: line-through;
text-decoration-color: hsl(var(--color-text) / 0.2);
}
.equiv-arrow {
font-size: 0.75rem;
color: hsl(var(--color-primary) / 0.4);
}
.equiv-to {
font-size: 0.85rem;
font-weight: 600;
color: hsl(var(--color-primary));
}
.equiv-note {
font-size: 0.78rem;
color: hsl(var(--color-text-muted));
}
.equiv-ext {
color: hsl(var(--color-primary) / 0.4);
flex-shrink: 0;
}
@media (max-width: 600px) {
.equiv-row {
grid-template-columns: 1fr auto 1fr;
grid-template-rows: auto auto;
}
.equiv-note { grid-column: 1 / -1; font-size: 0.74rem; }
.equiv-ext { display: none; }
}
/* ── LLM ── */
.llm-intro {
font-size: 0.9rem;
line-height: 1.6;
color: hsl(var(--color-text-muted));
margin: 0 0 1rem;
}
.llm-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.llm-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.25rem 0.7rem;
border-radius: 20px;
font-size: 0.78rem;
font-weight: 500;
font-family: var(--font-mono);
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
}
.llm-commands {
display: flex;
flex-direction: column;
gap: 0.35rem;
margin-bottom: 1rem;
}
.llm-cmd {
display: block;
padding: 0.5rem 0.875rem;
border-radius: 10px;
background: hsl(var(--color-bg));
border: 1px solid hsl(var(--color-surface-light));
font-family: var(--font-mono);
font-size: 0.78rem;
color: hsl(var(--color-text));
white-space: pre;
overflow-x: auto;
}
.llm-rules {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.llm-rule {
display: flex;
align-items: flex-start;
gap: 0.5rem;
font-size: 0.82rem;
color: hsl(var(--color-text-muted));
line-height: 1.5;
}
.llm-rule .i-lucide-shield-check {
color: hsl(var(--color-accent));
}
/* ── Atelier ── */
.atelier-text {
font-size: 0.9rem;
line-height: 1.6;
color: hsl(var(--color-text-muted));
margin: 0 0 0.75rem;
}
.atelier-format {
font-size: 0.82rem;
font-family: var(--font-mono);
color: hsl(var(--color-primary));
margin: 0 0 1rem;
}
.atelier-programme {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0;
}
.atelier-item {
display: flex;
align-items: flex-start;
gap: 0.625rem;
padding: 0.625rem 0;
border-bottom: 1px solid hsl(var(--color-text) / 0.06);
font-size: 0.85rem;
color: hsl(var(--color-text));
}
.atelier-item:last-child { border-bottom: none; }
/* ── Links ── */
.links-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.link-row {
display: grid;
grid-template-columns: 9rem 1fr auto;
align-items: center;
gap: 0.75rem;
padding: 0.625rem 0.875rem;
border-radius: 10px;
text-decoration: none;
background: hsl(var(--color-surface));
border: 1px solid transparent;
transition: border-color 0.12s, background 0.12s;
}
.link-row:hover {
border-color: hsl(var(--color-primary) / 0.2);
background: hsl(var(--color-primary) / 0.04);
}
.link-label {
font-size: 0.85rem;
font-weight: 600;
color: hsl(var(--color-primary));
}
.link-desc {
font-size: 0.8rem;
color: hsl(var(--color-text-muted));
}
@media (max-width: 480px) {
.link-row { grid-template-columns: 1fr auto; grid-template-rows: auto auto; }
.link-desc { grid-column: 1; font-size: 0.74rem; }
}
/* ── Insight ── */
.insight-box {
padding: 1.25rem 1.5rem;
border-radius: 14px;
background: hsl(var(--color-surface));
border-left: 3px solid hsl(var(--color-primary) / 0.5);
}
.insight-box--warning {
border-left-color: hsl(var(--color-accent) / 0.6);
background: hsl(var(--color-accent) / 0.05);
}
.insight-header {
display: flex;
align-items: flex-start;
gap: 0.6rem;
margin-bottom: 0.75rem;
color: hsl(var(--color-primary));
}
.insight-box--warning .insight-header {
color: hsl(var(--color-accent));
}
.insight-title {
font-family: var(--font-display);
font-size: 1rem;
font-weight: 700;
color: inherit;
margin: 0;
line-height: 1.3;
}
.insight-text {
font-size: 0.875rem;
line-height: 1.6;
color: hsl(var(--color-text-muted));
margin: 0 0 0.75rem;
}
.insight-points {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.insight-points li {
font-size: 0.82rem;
color: hsl(var(--color-text-muted));
padding-left: 1rem;
position: relative;
line-height: 1.5;
}
.insight-points li::before {
content: '·';
position: absolute;
left: 0;
color: hsl(var(--color-primary) / 0.5);
font-weight: 700;
}
/* ── Tiers ── */
.tiers-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
@media (max-width: 768px) {
.tiers-grid { grid-template-columns: 1fr; }
}
.tier-card {
padding: 1.25rem;
border-radius: 14px;
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-primary) / 0.08);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.tier-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.25rem;
}
.tier-level {
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 600;
color: hsl(var(--color-text-muted));
text-transform: uppercase;
letter-spacing: 0.06em;
}
.tier-badge {
font-size: 0.7rem;
font-weight: 500;
padding: 0.15rem 0.5rem;
border-radius: 20px;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
}
.tier-icon {
width: 2rem;
height: 2rem;
border-radius: 8px;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
display: flex;
align-items: center;
justify-content: center;
}
.tier-title {
font-family: var(--font-display);
font-size: 0.95rem;
font-weight: 700;
color: hsl(var(--color-text));
margin: 0;
}
.tier-text {
font-size: 0.8rem;
line-height: 1.55;
color: hsl(var(--color-text-muted));
margin: 0;
flex: 1;
}
.tier-tools {
list-style: none;
padding: 0;
margin: 0.25rem 0 0;
display: flex;
flex-direction: column;
gap: 0.2rem;
border-top: 1px solid hsl(var(--color-text) / 0.06);
padding-top: 0.5rem;
}
.tier-tools li {
font-size: 0.76rem;
color: hsl(var(--color-text-muted));
padding-left: 0.75rem;
position: relative;
}
.tier-tools li::before {
content: '';
position: absolute;
left: 0;
color: hsl(var(--color-primary) / 0.4);
font-size: 0.65rem;
}
/* ── Projet (gestation) ── */
.projet-card {
padding: 1.75rem;
border-radius: 16px;
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-accent) / 0.2);
box-shadow: 0 0 32px hsl(var(--color-accent) / 0.05);
}
.projet-header {
display: flex;
align-items: flex-start;
gap: 1rem;
margin-bottom: 1rem;
}
.projet-icon {
width: 3rem;
height: 3rem;
border-radius: 12px;
background: hsl(var(--color-accent) / 0.12);
color: hsl(var(--color-accent));
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.projet-header-text {
flex: 1;
}
.projet-kicker {
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: hsl(var(--color-accent));
margin: 0 0 0.2rem;
}
.projet-title {
font-family: var(--font-display);
font-size: clamp(1.25rem, 3vw, 1.6rem);
font-weight: 800;
color: hsl(var(--color-text));
margin: 0;
}
.projet-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.2rem 0.6rem;
border-radius: 20px;
background: hsl(var(--color-accent) / 0.12);
color: hsl(var(--color-accent));
font-size: 0.7rem;
font-weight: 500;
font-family: var(--font-mono);
white-space: nowrap;
flex-shrink: 0;
}
.projet-text {
font-size: 0.875rem;
line-height: 1.65;
color: hsl(var(--color-text-muted));
margin: 0 0 1.25rem;
}
.projet-features {
list-style: none;
padding: 0;
margin: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 0.5rem;
border-top: 1px solid hsl(var(--color-text) / 0.06);
padding-top: 1rem;
}
.projet-feature {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.82rem;
color: hsl(var(--color-text-muted));
padding: 0.4rem 0.6rem;
border-radius: 8px;
background: hsl(var(--color-bg) / 0.5);
}
.projet-feature .i-lucide-hard-drive,
.projet-feature .i-lucide-video,
.projet-feature .i-lucide-message-circle,
.projet-feature .i-lucide-bot,
.projet-feature .i-lucide-globe-2,
.projet-feature .i-lucide-share-2 {
color: hsl(var(--color-accent));
}
/* ── Territoire ── */
.territoire-card {
border-radius: 16px;
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-primary) / 0.12);
overflow: hidden;
}
.territoire-header {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1.75rem 1.75rem 1.25rem;
}
.territoire-icon {
width: 3rem;
height: 3rem;
border-radius: 12px;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 0.1rem;
}
.territoire-title {
font-family: var(--font-display);
font-size: clamp(1.15rem, 2.5vw, 1.4rem);
font-weight: 800;
color: hsl(var(--color-text));
margin: 0 0 0.5rem;
}
.territoire-text {
font-size: 0.875rem;
line-height: 1.65;
color: hsl(var(--color-text-muted));
margin: 0;
}
/* Modèles économiques */
.territoire-economies {
display: flex;
gap: 0.75rem;
padding: 0 1.75rem 1.5rem;
flex-wrap: wrap;
}
.territoire-eco {
flex: 1;
min-width: 220px;
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.875rem 1rem;
border-radius: 12px;
background: hsl(var(--color-bg) / 0.6);
border: 1px solid hsl(var(--color-primary) / 0.1);
}
.territoire-eco-icon {
width: 2rem;
height: 2rem;
border-radius: 8px;
background: hsl(var(--color-primary) / 0.12);
color: hsl(var(--color-primary));
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.territoire-g1 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1rem;
}
.territoire-eco-label {
font-size: 0.82rem;
font-weight: 700;
color: hsl(var(--color-text));
margin: 0 0 0.2rem;
}
.territoire-eco-desc {
font-size: 0.78rem;
line-height: 1.5;
color: hsl(var(--color-text-muted));
margin: 0;
}
/* Bouquet */
.territoire-bouquet {
padding: 1.25rem 1.75rem;
border-top: 1px solid hsl(var(--color-text) / 0.06);
}
.territoire-bouquet-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-family: var(--font-mono);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: hsl(var(--color-primary));
margin: 0 0 1rem;
}
.territoire-bouquet-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 0.75rem;
}
.territoire-bouquet-cat {
background: hsl(var(--color-bg) / 0.5);
border-radius: 10px;
padding: 0.75rem;
}
.territoire-bouquet-cat-header {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.75rem;
font-weight: 700;
color: hsl(var(--color-accent));
margin-bottom: 0.5rem;
}
.territoire-bouquet-items {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.territoire-bouquet-items li {
font-size: 0.76rem;
color: hsl(var(--color-text-muted));
line-height: 1.4;
padding-left: 0.6rem;
position: relative;
}
.territoire-bouquet-items li::before {
content: '';
position: absolute;
left: 0;
color: hsl(var(--color-primary) / 0.5);
}
/* Estimation toggled */
.territoire-estimate {
border-top: 1px solid hsl(var(--color-text) / 0.06);
}
.territoire-estimate-toggle {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 1rem 1.75rem;
cursor: pointer;
list-style: none;
font-size: 0.85rem;
font-weight: 600;
color: hsl(var(--color-text) / 0.7);
transition: color 0.15s;
user-select: none;
}
.territoire-estimate-toggle:hover {
color: hsl(var(--color-primary));
}
.territoire-estimate-toggle::-webkit-details-marker { display: none; }
.territoire-chevron {
margin-left: auto;
transition: transform 0.2s;
}
details[open] .territoire-chevron {
transform: rotate(180deg);
}
.territoire-estimate-body {
padding: 0 1.75rem 1.75rem;
}
.territoire-estimate-note {
font-size: 0.8rem;
color: hsl(var(--color-text-muted));
font-style: italic;
margin: 0 0 1.25rem;
line-height: 1.55;
}
/* HW table */
.territoire-hw-table {
display: flex;
flex-direction: column;
gap: 0;
border-radius: 12px;
overflow: hidden;
border: 1px solid hsl(var(--color-text) / 0.06);
margin-bottom: 1.25rem;
}
.territoire-hw-header {
display: grid;
grid-template-columns: 1.5fr 3fr 1.2fr;
gap: 0.75rem;
padding: 0.5rem 1rem;
background: hsl(var(--color-bg) / 0.8);
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: hsl(var(--color-text-muted));
border-bottom: 1px solid hsl(var(--color-text) / 0.06);
}
.territoire-hw-row {
display: grid;
grid-template-columns: 1.5fr 3fr 1.2fr;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid hsl(var(--color-text) / 0.04);
align-items: start;
}
.territoire-hw-row:last-child { border-bottom: none; }
.territoire-hw-row:nth-child(even) { background: hsl(var(--color-bg) / 0.3); }
.territoire-hw-role {
font-size: 0.82rem;
font-weight: 600;
color: hsl(var(--color-text));
line-height: 1.4;
}
.territoire-hw-detail {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.territoire-hw-detail span:first-child {
font-size: 0.82rem;
color: hsl(var(--color-text) / 0.85);
}
.territoire-hw-usage {
font-size: 0.75rem;
color: hsl(var(--color-text-muted));
line-height: 1.4;
}
.territoire-hw-note {
font-size: 0.72rem;
color: hsl(var(--color-accent) / 0.8);
font-style: italic;
line-height: 1.4;
}
.territoire-hw-cost {
font-family: var(--font-mono);
font-size: 0.8rem;
font-weight: 600;
color: hsl(var(--color-primary));
white-space: nowrap;
}
/* Totaux */
.territoire-totals {
border-radius: 12px;
overflow: hidden;
border: 1px solid hsl(var(--color-primary) / 0.15);
}
.territoire-total-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.6rem 1rem;
border-bottom: 1px solid hsl(var(--color-primary) / 0.08);
}
.territoire-total-row:last-of-type { border-bottom: none; }
.territoire-total-row--highlight {
background: hsl(var(--color-primary) / 0.06);
}
.territoire-total-label {
font-size: 0.82rem;
color: hsl(var(--color-text-muted));
}
.territoire-total-value {
font-family: var(--font-mono);
font-size: 0.88rem;
font-weight: 700;
color: hsl(var(--color-primary));
}
.territoire-totals-note {
padding: 0.6rem 1rem;
font-size: 0.8rem;
color: hsl(var(--color-text-muted));
font-style: italic;
border-top: 1px solid hsl(var(--color-primary) / 0.08);
margin: 0;
background: hsl(var(--color-primary) / 0.03);
}
@media (max-width: 640px) {
.territoire-header { flex-direction: column; gap: 0.75rem; }
.territoire-hw-header { display: none; }
.territoire-hw-row { grid-template-columns: 1fr; gap: 0.25rem; }
.territoire-hw-cost { font-size: 0.85rem; }
.territoire-bouquet-grid { grid-template-columns: 1fr; }
}
/* overflow:clip coupe sans créer de scroll container — préserve position:sticky */
.page-outer {
position: relative;
overflow: clip;
}
/* ── Shadoks décoratifs ── */
.shadok-dl {
position: absolute;
pointer-events: none;
color: hsl(var(--color-primary));
}
.shadok-dl-pinguin {
left: 1%;
top: 18%;
width: clamp(65px, 9vw, 130px);
opacity: 0.22;
animation: shadokfloat-a 11s ease-in-out infinite;
}
.shadok-dl-toile {
right: 1%;
top: 22%;
width: clamp(65px, 9vw, 140px);
opacity: 0.2;
color: hsl(var(--color-accent));
animation: shadokfloat-b 13s ease-in-out infinite;
}
.shadok-dl-nas {
left: 2%;
bottom: 6%;
width: clamp(65px, 9vw, 135px);
opacity: 0.22;
color: hsl(var(--color-accent));
animation: shadokfloat-c 10s ease-in-out infinite;
}
@keyframes shadokfloat-a {
0%, 100% { transform: translateY(0) rotate(-2deg); }
50% { transform: translateY(-8px) rotate(2deg); }
}
@keyframes shadokfloat-b {
0%, 100% { transform: translateY(0) rotate(3deg); }
50% { transform: translateY(-10px) rotate(-2deg); }
}
@keyframes shadokfloat-c {
0%, 100% { transform: translateY(0) rotate(-1deg); }
50% { transform: translateY(-7px) rotate(3deg); }
}
@keyframes shadokfloat-d {
0%, 100% { transform: translateY(0) rotate(2deg); }
50% { transform: translateY(-9px) rotate(-3deg); }
}
@keyframes shadokfloat-e {
0%, 100% { transform: translateY(0) rotate(-3deg); }
50% { transform: translateY(-6px) rotate(1deg); }
}
@keyframes shadokfloat-f {
0%, 100% { transform: translateY(0) rotate(1deg); }
50% { transform: translateY(-11px) rotate(-2deg); }
}
.shadok-dl-debugger {
right: 1%;
bottom: 14%;
width: clamp(65px, 9vw, 140px);
opacity: 0.2;
color: hsl(var(--color-primary));
animation: shadokfloat-d 12s ease-in-out infinite;
}
.shadok-dl-duck {
left: 38%;
top: 6%;
width: clamp(65px, 9vw, 145px);
opacity: 0.2;
color: hsl(var(--color-accent));
animation: shadokfloat-e 14s ease-in-out infinite;
}
.shadok-dl-cafeine {
right: 2%;
top: 52%;
width: clamp(65px, 9vw, 132px);
opacity: 0.22;
color: hsl(var(--color-primary));
animation: shadokfloat-f 9s ease-in-out infinite;
}
@media (max-width: 768px) {
.shadok-dl { display: none; }
}
/* equiv-row without link */
.equiv-row--no-link {
cursor: default;
opacity: 0.75;
}
.equiv-row--no-link:hover {
border-color: transparent;
background: hsl(var(--color-surface));
}
</style>