identité : le kit du studio DA intégré, les 61 oiseaux posés sur la page

Copie du kit kit-site (empreinte vérifiée) : logotype dans l'en-tête, signe §,
favicons et icônes, og-default (?v=3), frise du pied de page, 61 oiseaux.
Les shadoks positionnés à la main (HomeShadoks et les règles des 10 pages)
cèdent la place à UiOiseauPose : chaque oiseau se pose sur une arête réelle
(perché, marge gauche ou droite, derrière une carte, libre), currentColor,
largeur 72 px au moins, opacité 0,18-0,28 près du texte et 0,10-0,14 derrière,
jamais deux fois sur une page, calligraphe et typographe jamais en miroir.
Sous 768 px, un oiseau par page au plus ; seuls ceux derrière une carte
dérivent un peu, et plus du tout en mouvement réduit.

Consignes d'Yvv du 03/11 inscrites dans docs/consignes-yvv.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 06:39:34 +01:00
co-authored by Claude Opus 5.5
parent cdd3fa57de
commit d2bc0d9e46
99 changed files with 889 additions and 3413 deletions
+6
View File
@@ -8,6 +8,8 @@
<div class="format-grid">
<!-- Éditorial : le livre et les chansons -->
<article v-if="book" class="format-card format-card--book">
<!-- The reader peeks over the book card: only her head and book show -->
<UiOiseauPose nom="lectrice" pose="derriere" left="76%" bottom="calc(100% - 48px)" :w="104" />
<div
class="format-cover"
role="img"
@@ -41,6 +43,8 @@
<!-- Événementiel -->
<NuxtLink v-if="evenement" :to="evenement.to" class="format-card format-card--event">
<!-- The accordion player stands on the event card -->
<UiOiseauPose nom="accordeoniste" pose="perche" right="66px" :w="84" retourne ton="accent" />
<p class="format-kicker">
{{ evenement.teaser }}
<span v-if="evenement.gestation" class="format-state">en gestation</span>
@@ -123,6 +127,7 @@ defineEmits<{
}
.format-grid {
isolation: isolate;
display: grid;
gap: 1.25rem;
}
@@ -132,6 +137,7 @@ defineEmits<{
}
.format-card {
position: relative;
display: flex;
flex-direction: column;
gap: 0.5rem;
+16 -2
View File
@@ -2,7 +2,6 @@
<div class="va">
<!-- Hero : the headline, and the three cursors to push -->
<section class="va-hero">
<HomeShadoks />
<div class="container-content va-hero-grid">
<div class="va-intro">
<h1 class="va-h1">
@@ -20,7 +19,11 @@
</div>
<div class="va-console">
<p class="va-console-title">{{ heading[2] }}</p>
<p class="va-console-title">
{{ heading[2] }}
<!-- The DJ stands on the console board, looking at the faders -->
<UiOiseauPose nom="dj" pose="perche" right="2rem" bottom="-0.75rem" :w="80" retourne ton="accent" mobile />
</p>
<HomeConsole
v-model="active"
:axes="axes"
@@ -61,6 +64,7 @@
<div class="container-content">
<h2 class="va-h2">{{ toolboxTitle }}</h2>
<HomeToolbox
:oiseaux="toolboxOiseaux"
:axes="axes"
:lpk="lpk"
:bouquet="bouquet"
@@ -76,6 +80,7 @@
<script setup lang="ts">
import type { AxisKey } from '~/composables/useHomeData'
import type { OiseauPlace } from '~/utils/oiseauPose'
defineEmits<{
'open-player': []
@@ -97,6 +102,14 @@ const axesLine = computed(() =>
.filter(Boolean)
.map(text => ({ text, hue: AXIS_WORDS[text] })))
const active = ref<AxisKey>('numerique')
// The governance crew stands in the empty space under the citoyenne column of the toolbox
const toolboxOiseaux: Partial<Record<AxisKey, OiseauPlace[]>> = {
politique: [
{ nom: 'capitaine', pose: 'libre', left: 'calc(34% - 75px)', bottom: '-190px', w: 150, op: 0.24 },
{ nom: 'matelot', pose: 'libre', left: 'calc(80% - 40px)', bottom: '-300px', w: 80, op: 0.26, retourne: true },
],
}
</script>
<style scoped>
@@ -184,6 +197,7 @@ const active = ref<AxisKey>('numerique')
}
.va-console-title {
position: relative;
margin: 0 0 0.75rem;
font-family: var(--font-display);
font-weight: 700;
+3
View File
@@ -5,6 +5,8 @@
<!-- Formulaire -->
<UiScrollReveal>
<div class="message-form-card">
<!-- The postwoman stands on the message form -->
<UiOiseauPose nom="factrice" pose="perche" right="30px" :w="88" retourne />
<h3 class="font-display text-lg font-bold form-title mb-4">Laisser un message</h3>
<form v-if="!submitted" class="space-y-3" @submit.prevent="send">
@@ -167,6 +169,7 @@ function formatDate(iso: string) {
/* ── Carte formulaire ── */
.message-form-card {
position: relative;
background: hsl(var(--color-surface));
border-radius: 16px;
padding: 1.5rem;
-83
View File
@@ -1,83 +0,0 @@
<template>
<div class="shadoks" aria-hidden="true">
<!-- 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>
</div>
</template>
<style scoped>
.shadoks {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
}
.shadok-bird {
position: absolute;
right: 3%;
top: 6%;
width: clamp(90px, 10vw, 150px);
opacity: 0.22;
color: hsl(var(--axis-num));
animation: shadok-float 8s ease-in-out infinite;
}
.shadok-boulanger {
position: absolute;
left: 1.5%;
bottom: 4%;
width: clamp(80px, 9vw, 140px);
opacity: 0.2;
color: hsl(var(--axis-eco));
animation: shadok-float 9s ease-in-out infinite reverse;
}
@keyframes shadok-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@media (max-width: 768px) {
.shadoks { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.shadok-bird, .shadok-boulanger { animation: none; }
}
</style>
+11 -1
View File
@@ -9,6 +9,7 @@
:style="{ '--hue': `var(${axis.hue})` }"
:aria-labelledby="`tb-${uid}-${axis.key}`"
>
<UiOiseauPose v-for="o in oiseaux?.[axis.key] ?? []" :key="`${o.nom}-${o.pose}`" v-bind="o" class="toolbox-oiseau" />
<header class="toolbox-head">
<p class="toolbox-lever">{{ axis.lever }}</p>
<h3 :id="`tb-${uid}-${axis.key}`" class="toolbox-title">
@@ -61,7 +62,8 @@
</template>
<script setup lang="ts">
import type { HomeAxis } from '~/composables/useHomeData'
import type { AxisKey, HomeAxis } from '~/composables/useHomeData'
import type { OiseauPlace } from '~/utils/oiseauPose'
withDefaults(defineProps<{
axes: HomeAxis[]
@@ -70,6 +72,8 @@ withDefaults(defineProps<{
gratewizard?: { title: string, subtitle: string }
showPromise?: boolean
anchors?: boolean
/** Birds standing by an axis column (home page) */
oiseaux?: Partial<Record<AxisKey, OiseauPlace[]>>
}>(), {
showPromise: true,
anchors: false,
@@ -106,7 +110,13 @@ function onAction(id: string) {
}
}
/* The birds under a column only make sense with the three columns side by side */
@container (max-width: 59.99rem) {
.toolbox-oiseau { display: none; }
}
.toolbox-axis {
position: relative;
display: flex;
flex-direction: column;
gap: 0.75rem;
+6 -42
View File
@@ -1,44 +1,7 @@
<template>
<footer class="footer-wrap border-t border-[hsl(var(--color-text)/0.1)] bg-[hsl(var(--color-surface))]">
<!-- Shadok pattern -->
<svg class="footer-shadok-pattern" viewBox="0 0 400 80" fill="none" aria-hidden="true">
<g transform="translate(20,10)">
<ellipse cx="15" cy="25" rx="12" ry="14" fill="currentColor" opacity="0.2"/>
<circle cx="15" cy="10" r="7" fill="currentColor" opacity="0.16"/>
<line x1="10" y1="38" x2="8" y2="55" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
<line x1="20" y1="38" x2="22" y2="55" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
</g>
<g transform="translate(80,15)">
<ellipse cx="15" cy="22" rx="10" ry="12" fill="currentColor" opacity="0.16"/>
<circle cx="15" cy="8" r="6" fill="currentColor" opacity="0.13"/>
<line x1="10" y1="33" x2="8" y2="48" stroke="currentColor" stroke-width="1.5" opacity="0.12"/>
<line x1="20" y1="33" x2="22" y2="48" stroke="currentColor" stroke-width="1.5" opacity="0.12"/>
</g>
<g transform="translate(140,8)">
<ellipse cx="15" cy="25" rx="11" ry="13" fill="currentColor" opacity="0.18"/>
<circle cx="15" cy="10" r="6.5" fill="currentColor" opacity="0.15"/>
<line x1="10" y1="37" x2="7" y2="54" stroke="currentColor" stroke-width="1.5" opacity="0.14"/>
<line x1="20" y1="37" x2="23" y2="54" stroke="currentColor" stroke-width="1.5" opacity="0.14"/>
</g>
<g transform="translate(210,18)">
<ellipse cx="15" cy="20" rx="10" ry="11" fill="currentColor" opacity="0.14"/>
<circle cx="15" cy="7" r="5.5" fill="currentColor" opacity="0.11"/>
<line x1="10" y1="30" x2="9" y2="44" stroke="currentColor" stroke-width="1.5" opacity="0.1"/>
<line x1="20" y1="30" x2="21" y2="44" stroke="currentColor" stroke-width="1.5" opacity="0.1"/>
</g>
<g transform="translate(270,12)">
<ellipse cx="15" cy="24" rx="12" ry="14" fill="currentColor" opacity="0.18"/>
<circle cx="15" cy="9" r="7" fill="currentColor" opacity="0.15"/>
<line x1="10" y1="37" x2="7" y2="55" stroke="currentColor" stroke-width="1.5" opacity="0.14"/>
<line x1="20" y1="37" x2="23" y2="55" stroke="currentColor" stroke-width="1.5" opacity="0.14"/>
</g>
<g transform="translate(340,16)">
<ellipse cx="15" cy="22" rx="10" ry="12" fill="currentColor" opacity="0.16"/>
<circle cx="15" cy="8" r="6" fill="currentColor" opacity="0.13"/>
<line x1="10" y1="33" x2="8" y2="48" stroke="currentColor" stroke-width="1.5" opacity="0.12"/>
<line x1="20" y1="33" x2="22" y2="48" stroke="currentColor" stroke-width="1.5" opacity="0.12"/>
</g>
</svg>
<!-- The frieze of the birds (studio DA), standing on the bottom edge -->
<UiOiseau nom="frise" class="footer-shadok-pattern" />
<div class="container-content px-4 py-8 relative z-1">
<div class="flex flex-col items-center gap-4 md:flex-row md:justify-between">
<!-- Credits -->
@@ -72,13 +35,14 @@ const { data: site } = await useSiteContent()
overflow: hidden;
}
/* The whole frieze fits in the footer (mask: contain), never cropped; pale under the links */
.footer-shadok-pattern {
position: absolute;
bottom: 0;
top: 0.75rem;
left: 0;
width: 100%;
height: auto;
opacity: 1;
height: calc(100% - 0.75rem);
opacity: 0.12;
pointer-events: none;
color: hsl(var(--color-primary));
}
+8 -20
View File
@@ -2,9 +2,8 @@
<header class="sticky top-0 z-40 border-b border-[hsl(var(--color-text)/0.08)] bg-[hsl(var(--color-bg)/0.85)] backdrop-blur-xl">
<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">
<UiSectionSign class="logo-icon" />
<span class="logo-text">{{ site?.identity.name }}</span>
<NuxtLink to="/" class="logo-link flex items-center" :aria-label="site?.identity.name || 'Le librodrome'">
<UiLogotype class="logo-lockup" />
</NuxtLink>
<!-- Desktop navigation -->
@@ -72,31 +71,20 @@ const AXIS_HUES = ['--axis-num', '--axis-eco', '--axis-cit']
opacity: 0.8;
}
.logo-icon {
/* The validated lockup (studio DA, 27/10): the Geste already leans 25°, no rotation on hover */
.logo-lockup {
height: 2.3rem;
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);
.logo-link:hover .logo-lockup {
transform: scale(1.04);
}
@media (prefers-reduced-motion: reduce) {
.logo-icon { transition: none; }
}
.logo-text {
font-family: var(--font-logo);
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;
.logo-lockup { transition: none; }
}
.nav-prefix {
File diff suppressed because one or more lines are too long
+27
View File
@@ -0,0 +1,27 @@
<script setup lang="ts">
// One of the 61 birds (studio DA, validated 2026-10-28), drawn from /images/oiseaux/<nom>.svg as a
// mask filled with currentColor : it takes the colour the page gives it, like the shadoks did.
// The classes of the figure it replaces keep its place, size, opacity and animation.
import { OISEAUX } from '~/utils/oiseaux'
const props = defineProps<{ nom: string }>()
const o = computed(() => OISEAUX[props.nom])
</script>
<template>
<span
v-if="o"
class="oiseau"
aria-hidden="true"
:style="{ aspectRatio: o.ratio, '--oiseau': `url(/images/oiseaux/${props.nom}.svg)` }"
/>
</template>
<style scoped>
.oiseau {
display: block;
background-color: currentColor;
-webkit-mask: var(--oiseau) center / contain no-repeat;
mask: var(--oiseau) center / contain no-repeat;
}
</style>
+142
View File
@@ -0,0 +1,142 @@
<script setup lang="ts">
// A bird of the librodrome placed on its block (composition of 03/11, docs/consignes-yvv.md):
// it stands on a real edge of the page, next to the content its trade is about.
// The host block is the positioning context: position relative, overflow visible.
//
// Poses
// perche on the block's top edge (bottom: 100%), x with left / right
// marge-g beside the block, on its left; feet at the block's bottom, raised by `bottom`
// marge-d the same, on its right
// derriere large and pale, behind the block (z-index -1): only what sticks out shows.
// Needs a stacking context above the block (isolation: isolate) and a block
// that creates none itself (no z-index, transform, opacity, filter).
// libre placed only by left / right / top / bottom (an empty area next to its block)
//
// Birds in the margin, behind a block or free need room around the column: they show from
// 1200 px wide, the perched ones from 768 px (`seuil` overrides); under 768 px, only a bird
// marked `mobile`.
//
// Drawing rules of the studio DA (kit README, §2.5): one colour (currentColor), proportions
// intact and never cropped, 72 px wide at least, opacity 0.18-0.28 next to text and 0.10-0.14
// large behind a card, mirror allowed except for the calligraphe and the typographe.
// Birds standing on an edge do not move; only the one behind a card drifts a little (Yvv, 03/11).
import { OISEAUX } from '~/utils/oiseaux'
type Pose = 'perche' | 'marge-g' | 'marge-d' | 'derriere' | 'libre'
const props = withDefaults(defineProps<{
nom: string
pose?: Pose
/** Width in px: S 72-90, M 108-156, L 200-250 */
w?: number
left?: string
right?: string
top?: string
bottom?: string
/** Gap between the block and a bird in the margin */
ecart?: string
retourne?: boolean
/** Shown under 768 px (one bird per page at most) */
mobile?: boolean
/** Shown under 768 px only: the phone place of a bird placed elsewhere on wider screens */
mobileSeul?: boolean
/** Narrowest window (px) where the bird shows: 1200 by default for the margin, behind
* and free poses, 768 for a perched bird */
seuil?: 768 | 1200
ton?: 'primary' | 'accent'
op?: number
}>(), { pose: 'perche', w: 80, ecart: '2.5rem', ton: 'primary' })
/** These two write and compose from left to right: never mirrored */
const NO_MIRROR = new Set(['calligraphe', 'typographe'])
const sol = computed(() => OISEAUX[props.nom]?.sol ?? 5)
const style = computed(() => {
const s: Record<string, string> = { 'width': `${Math.max(72, props.w)}px`, '--sol': `${sol.value}%` }
if (props.pose === 'perche') s.bottom = '100%'
if (props.pose === 'marge-g') s.right = `calc(100% + ${props.ecart})`
if (props.pose === 'marge-d') s.left = `calc(100% + ${props.ecart})`
if (props.pose === 'marge-g' || props.pose === 'marge-d') s.bottom = '0px'
for (const k of ['left', 'right', 'top', 'bottom'] as const) {
if (props[k] !== undefined) s[k] = props[k]!
}
if (props.op !== undefined) s.opacity = String(props.op)
return s
})
const classes = computed(() => [
'oiseau-pose',
`oiseau-pose--${props.pose}`,
{
'oiseau-pose--retourne': props.retourne && !NO_MIRROR.has(props.nom),
'oiseau-pose--mobile': props.mobile || props.mobileSeul,
'oiseau-pose--mobile-seul': props.mobileSeul,
'oiseau-pose--large': (props.seuil ?? (props.pose === 'perche' ? 768 : 1200)) >= 1200,
'oiseau-pose--accent': props.ton === 'accent',
},
])
</script>
<template>
<UiOiseau :nom="nom" :class="classes" :style="style" :data-oiseau="nom" :data-pose="pose" />
</template>
<style scoped>
.oiseau-pose {
position: absolute;
z-index: 1;
pointer-events: none;
color: hsl(var(--color-primary));
opacity: 0.24;
/* The drawing's ground line lands on the edge */
transform: translateY(var(--sol));
}
.oiseau-pose--accent {
color: hsl(var(--color-accent));
}
.oiseau-pose--perche {
opacity: 0.28;
}
.oiseau-pose--derriere {
z-index: -1;
opacity: 0.12;
animation: oiseau-derive 12s ease-in-out infinite alternate;
}
.oiseau-pose--retourne {
scale: -1 1;
}
@keyframes oiseau-derive {
from { translate: 0 0; }
to { translate: 0 -6px; }
}
@media (max-width: 1199px) {
.oiseau-pose--large {
display: none;
}
}
@media (min-width: 768px) {
.oiseau-pose--mobile-seul {
display: none;
}
}
@media (max-width: 767px) {
.oiseau-pose:not(.oiseau-pose--mobile) {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.oiseau-pose--derriere {
animation: none;
}
}
</style>
File diff suppressed because one or more lines are too long