Remplace la silhouette SVG du hero par une vraie vidéo de joueur

L'illustration SVG derrière les stats de l'accueil ne rendait pas
professionnel. Remplacée par une vraie vidéo (Pexels, licence libre
d'usage commercial) d'un joueur en dribble continu, passée en noir et
blanc via CSS et masquée en dégradé pour préserver le contraste du
texte, comme pour la version précédente.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
This commit is contained in:
Claude committed 2026-07-25 23:30:56 +00:00
1 parent f5b2e26ee4
commit e03a5009c4
6 files changed
+44 -114

No files matched your search

+1
View File
@@ -50,6 +50,7 @@ app.use(
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
fontSrc: ['https://fonts.gstatic.com'],
imgSrc: ["'self'", 'data:', 'blob:'],
mediaSrc: ["'self'"],
connectSrc: ["'self'"],
frameSrc: ["'self'", 'https://www.google.com', 'https://maps.google.com'],
frameAncestors: ["'none'"],
Binary file not shown.
@@ -1,60 +0,0 @@
// 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 (
<svg
className={`hero-stats-silhouette ${className}`.trim()}
viewBox="0 0 360 300"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
role="presentation"
focusable="false"
>
<g className="hspl-figure" fill="var(--ink)">
<g fill="none" stroke="var(--ink)" strokeLinecap="round">
{/* trailing arm, swung back for balance */}
<line x1="222" y1="88" x2="192" y2="64" strokeWidth="15" />
<line x1="192" y1="64" x2="164" y2="50" strokeWidth="10" />
{/* dribbling arm, reaching down to the ball */}
<line x1="230" y1="96" x2="262" y2="136" strokeWidth="16" />
<line x1="262" y1="136" x2="290" y2="186" strokeWidth="10" />
{/* torso */}
<line x1="240" y1="80" x2="215" y2="130" strokeWidth="34" />
<line x1="215" y1="130" x2="198" y2="168" strokeWidth="24" />
{/* planted / front leg */}
<line x1="205" y1="168" x2="224" y2="220" strokeWidth="22" />
<line x1="224" y1="220" x2="218" y2="272" strokeWidth="14" />
<line x1="218" y1="272" x2="240" y2="280" strokeWidth="12" />
{/* trailing leg, extended back */}
<line x1="196" y1="170" x2="150" y2="196" strokeWidth="22" />
<line x1="150" y1="196" x2="108" y2="204" strokeWidth="13" />
<line x1="108" y1="204" x2="84" y2="212" strokeWidth="11" />
</g>
{/* joint fillers smoothing the width change between segments */}
<circle cx="192" cy="64" r="7.5" />
<circle cx="262" cy="136" r="8" />
<circle cx="215" cy="130" r="17" />
<circle cx="201" cy="169" r="13" />
<circle cx="224" cy="220" r="11" />
<circle cx="150" cy="196" r="11" />
<circle cx="248" cy="54" r="23" />
</g>
<g className="hspl-ball" fill="var(--ink)">
<circle cx="300" cy="208" r="22" />
<path d="M280,196 C293,203 307,203 320,196" fill="none" stroke="var(--bg)" strokeWidth="2" />
<path d="M300,187 C293,197 293,220 300,231" fill="none" stroke="var(--bg)" strokeWidth="2" />
</g>
<g className="hspl-speedlines" stroke="var(--ink)" strokeWidth="3" strokeLinecap="round" fill="none">
<path className="hspl-speedline" d="M42,196 L78,201" />
<path className="hspl-speedline" d="M34,214 L72,214" />
<path className="hspl-speedline" d="M40,232 L76,226" />
</g>
</svg>
);
}
+24
View File
@@ -0,0 +1,24 @@
// Purely decorative real video of a basketball player dribbling, animated
// behind the hero stats row. Desaturated to black & white at runtime via
// CSS (no source video processing available in this environment). Source:
// Pexels stock footage (free commercial license, no attribution required,
// anonymous player) — see web/public/assets/hero-player.mp4.
export default function HeroPlayerVideo({ className = '' }) {
const reduceMotion =
typeof window !== 'undefined' &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
return (
<video
className={`hero-stats-video ${className}`.trim()}
src="/assets/hero-player.mp4"
autoPlay={!reduceMotion}
loop={!reduceMotion}
muted
playsInline
preload="auto"
aria-hidden="true"
tabIndex={-1}
/>
);
}
+2 -2
View File
@@ -7,7 +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';
import HeroPlayerVideo from '../components/HeroPlayerVideo.jsx';
export function InscriptionModal({ rencontre, onClose, onDone }) {
const { user, login } = useAuth();
@@ -267,7 +267,7 @@ export default function Home() {
</Link>
</div>
<div className="hero-stats-wrap" style={{ position: 'relative', marginTop: 54, paddingTop: 34, borderTop: '1px solid var(--border)' }}>
<HeroPlayerSilhouette />
<HeroPlayerVideo />
<div style={{ display: 'flex', gap: 42, position: 'relative', zIndex: 1 }}>
{[[String(members.length), 'entreprises membres'], [String(passees.length), 'rencontres organisées'], ['30 ans', 'de partenariat']].map(([n, l]) => (
<div key={l}>
+17 -52
View File
@@ -43,58 +43,23 @@ 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 background: a real video of a basketball player dribbling,
behind the "entreprises membres / rencontres organisées / X ans" row.
Pexels stock footage (free commercial license, no attribution required,
anonymous player), desaturated at runtime via CSS. */
.hero-stats-wrap { overflow: visible; }
.hero-stats-silhouette {
position: absolute; right: -20px; bottom: -34px; width: 300px; height: auto;
.hero-stats-video {
position: absolute; right: -20px; bottom: -34px;
width: 420px; height: 130px;
object-fit: cover; object-position: 60% 35%;
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; }
filter: grayscale(1) contrast(1.05) brightness(1.05);
opacity: .22;
/* Fades the video out over the stat text (left) and lets it show only
over the clear whitespace (right), so it never degrades the existing
text contrast. */
mask-image: linear-gradient(100deg, transparent 0%, transparent 26%, black 52%, black 100%);
-webkit-mask-image: linear-gradient(100deg, transparent 0%, transparent 26%, black 52%, black 100%);
}
.container { max-width: 1180px; margin: 0 auto; padding-left: 32px; padding-right: 32px; }
@@ -302,7 +267,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; }
.hero-stats-video { width: 260px; height: 100px; 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; }
@@ -329,7 +294,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; }
.hero-stats-video { display: none; }
.admin-nav { grid-template-columns: 1fr; gap: 20px; }
.admin-sidebar-footer { align-items: stretch; flex-direction: column; }
.admin-sidebar-action { width: 100%; }