From f5b2e26ee48d553e367ec57977fd590af66481a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 22:43:58 +0000 Subject: [PATCH] =?UTF-8?q?Silhouette=20anim=C3=A9e=20de=20joueur=20de=20b?= =?UTF-8?q?asket=20derri=C3=A8re=20les=20stats=20du=20hero?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute une illustration originale (SVG, pas une photo) d'un joueur en plein dribble, en aplat noir/blanc avec liseré rouge de la marque, animée derrière la ligne de statistiques du hero (rebond du ballon, léger balancement du corps, traits de vitesse). Silhouette générique — aucune personne réelle identifiable, donc aucune question de droit à l'image. - Nouveau composant HeroPlayerSilhouette.jsx : figure construite à partir de segments à traits épais et jointures rondes (même logique que les pictogrammes sportifs plats type NBA/Jordan), plus fiable à main levée qu'un tracé bezier continu. - Positionnement en arrière-plan avec un masque en dégradé qui estompe la silhouette exactement derrière le texte des statistiques (déjà proche du seuil de contraste WCAG) et ne la laisse apparaître que dans l'espace vide à droite — le contraste du texte reste inchangé. - prefers-reduced-motion : figure et ballon figés, traits de vitesse masqués (première utilisation de cette media query dans le projet). aria-hidden + pointer-events:none (purement décoratif). - Réduite à 960px, masquée sous 640px (pas de place pour coexister avec la ligne de stats sur petit écran). - Aucun changement serveur, CSP, Docker ni nouvelle dépendance — CSS/SVG pur, dans l'esprit de l'animation marquee déjà existante. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o --- web/src/components/HeroPlayerSilhouette.jsx | 60 +++++++++++++++++++++ web/src/pages/Home.jsx | 18 ++++--- web/src/styles.css | 56 +++++++++++++++++++ 3 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 web/src/components/HeroPlayerSilhouette.jsx diff --git a/web/src/components/HeroPlayerSilhouette.jsx b/web/src/components/HeroPlayerSilhouette.jsx new file mode 100644 index 0000000..6ada6f6 --- /dev/null +++ b/web/src/components/HeroPlayerSilhouette.jsx @@ -0,0 +1,60 @@ +// Purely decorative basketball-player silhouette animated behind the hero +// stats row. An original flat-illustration silhouette (not a photo/video), +// built from rounded-capsule limb segments — the same construction as +// classic flat sports pictograms (NBA/Jordan-style marks) — in a duotone +// matching the site theme. Avoids any droit-à-l'image concern since it +// does not depict a real identifiable person. +export default function HeroPlayerSilhouette({ className = '' }) { + return ( + + ); +} diff --git a/web/src/pages/Home.jsx b/web/src/pages/Home.jsx index c9abb92..9ea7819 100644 --- a/web/src/pages/Home.jsx +++ b/web/src/pages/Home.jsx @@ -7,6 +7,7 @@ import Modal from '../components/Modal.jsx'; import PastEventCard from '../components/PastEventCard.jsx'; import { AdhesionForm, AdhesionModal } from '../components/AdhesionForm.jsx'; import { associationSettings } from '../lib/siteSettings.js'; +import HeroPlayerSilhouette from '../components/HeroPlayerSilhouette.jsx'; export function InscriptionModal({ rencontre, onClose, onDone }) { const { user, login } = useAuth(); @@ -265,13 +266,16 @@ export default function Home() { -
- {[[String(members.length), 'entreprises membres'], [String(passees.length), 'rencontres organisées'], ['30 ans', 'de partenariat']].map(([n, l]) => ( -
-
{n}
-
{l}
-
- ))} +
+ +
+ {[[String(members.length), 'entreprises membres'], [String(passees.length), 'rencontres organisées'], ['30 ans', 'de partenariat']].map(([n, l]) => ( +
+
{n}
+
{l}
+
+ ))} +
diff --git a/web/src/styles.css b/web/src/styles.css index c7c3c9e..d02f9ca 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -43,6 +43,60 @@ input:focus, textarea:focus, select:focus { outline: none; border-color: var(--r .marquee-fade { width: 40px; } } +/* Hero stats background: an animated basketball-player silhouette behind + the "entreprises membres / rencontres organisées / X ans" row. Original + illustration (not a photo), decorative only — see .hspl-* below. */ +.hero-stats-wrap { overflow: visible; } +.hero-stats-silhouette { + position: absolute; right: -20px; bottom: -34px; width: 300px; height: auto; + pointer-events: none; + /* Fades the silhouette out over the stat text (left) and lets it show at + full (still very low) opacity only over the clear whitespace (right), + so it never degrades the existing text contrast. */ + mask-image: linear-gradient(100deg, transparent 0%, transparent 24%, black 48%, black 100%); + -webkit-mask-image: linear-gradient(100deg, transparent 0%, transparent 24%, black 48%, black 100%); +} +.hspl-figure { + opacity: .08; transform-origin: 50% 100%; + animation: hspl-drift 7s ease-in-out infinite alternate, hspl-glow 5s ease-in-out infinite; +} +.hspl-ball { + opacity: .14; transform-box: fill-box; transform-origin: 50% 100%; + animation: hspl-ball-bounce 1.4s infinite; +} +.hspl-speedline { + opacity: .3; + animation: hspl-speedline 2.4s ease-out infinite; +} +.hspl-speedline:nth-child(2) { animation-delay: .3s; } +.hspl-speedline:nth-child(3) { animation-delay: .6s; } + +@keyframes hspl-drift { + from { transform: translateY(0) rotate(0deg); } + to { transform: translateY(-10px) rotate(-1.4deg); } +} +@keyframes hspl-glow { + 0%, 100% { filter: drop-shadow(2px 0 0 rgba(193, 39, 45, 0)); } + 50% { filter: drop-shadow(2px 0 0 rgba(193, 39, 45, .18)); } +} +@keyframes hspl-ball-bounce { + 0% { transform: translateY(0); animation-timing-function: cubic-bezier(.55, 0, 1, .45); } + 18% { transform: translateY(38px) scaleY(.82) scaleX(1.1); animation-timing-function: cubic-bezier(0, .55, .45, 1); } + 50% { transform: translateY(0); animation-timing-function: cubic-bezier(.55, 0, 1, .45); } + 68% { transform: translateY(38px) scaleY(.82) scaleX(1.1); animation-timing-function: cubic-bezier(0, .55, .45, 1); } + 100% { transform: translateY(0); } +} +@keyframes hspl-speedline { + 0% { opacity: 0; transform: translateX(6px); } + 30% { opacity: 1; transform: translateX(0); } + 70% { opacity: 1; transform: translateX(-4px); } + 100% { opacity: 0; transform: translateX(-10px); } +} +@media (prefers-reduced-motion: reduce) { + .hspl-figure, .hspl-ball { animation: none !important; } + .hspl-speedline { display: none !important; } +} + .container { max-width: 1180px; margin: 0 auto; padding-left: 32px; padding-right: 32px; } .serif { font-family: var(--serif); } @@ -248,6 +302,7 @@ input:focus, textarea:focus, select:focus { outline: none; border-color: var(--r @media (max-width: 960px) { .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr !important; } .hero-grid { grid-template-columns: 1fr !important; } + .hero-stats-silhouette { width: 190px; right: -10px; bottom: -10px; } } .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; } .grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; } @@ -274,6 +329,7 @@ input:focus, textarea:focus, select:focus { outline: none; border-color: var(--r @media (max-width: 640px) { .container { padding-left: 20px; padding-right: 20px; } + .hero-stats-silhouette { display: none; } .admin-nav { grid-template-columns: 1fr; gap: 20px; } .admin-sidebar-footer { align-items: stretch; flex-direction: column; } .admin-sidebar-action { width: 100%; }