Deuxième passe DA : direction "punchy, friendly, donne envie de fun"

Retour direct de Kévin après la 1ère passe (trop sage) : palette poussée
en saturation (corail vif en primaire, prune + doré en accents), formes
plus rondes (boutons pilule, rayons 20px), micro-animations avec du peps
(easing overshoot, coche qui se dessine + confettis CSS purs à l'envoi,
spinner animé). Police d'accent Fraunces ajoutée pour les titres (2
fichiers .woff2 auto-hébergés, ~44 Ko, SIL OFL, jamais de CDN).

Ton de voix retravaillé : micro-copies plus souriantes, nouvel état
"limite de photos atteinte" traité chaleureusement (pas comme une erreur).

Cohérence maintenue sur galerie admin, carton imprimable, et générateur
QR (sync légère, priorité basse). Testé en conditions réelles sur
photobooth.assistantaikev.eu.
This commit is contained in:
2026-09-16 15:48:29 +02:00
parent 66435b0705
commit 3afd475a55
10 changed files with 716 additions and 175 deletions
+18 -2
View File
@@ -13,6 +13,8 @@
fichier front, à mettre en place lors du déploiement.
-->
<meta name="robots" content="noindex, nofollow">
<meta name="theme-color" content="#faf6f0">
<link rel="preload" href="/vendor/fonts/fraunces-latin-600-normal.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/admin/gallery/style.css">
</head>
<body>
@@ -30,6 +32,7 @@
</header>
<section id="status-loading" class="state-block" role="status">
<span class="spinner" aria-hidden="true"></span>
<p>Chargement de la galerie…</p>
</section>
@@ -42,8 +45,21 @@
<p id="status-banner" class="status-banner" hidden></p>
<div class="toolbar">
<button type="button" id="refresh-btn" class="btn btn--secondary">Actualiser</button>
<button type="button" id="download-zip-btn" class="btn">Télécharger tout (ZIP)</button>
<button type="button" id="refresh-btn" class="btn btn--secondary">
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 11A8 8 0 1 0 18.5 16"/>
<path d="M20 5v6h-6"/>
</svg>
<span>Actualiser</span>
</button>
<button type="button" id="download-zip-btn" class="btn">
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M12 4v11"/>
<path d="M7 10.5 12 15.5 17 10.5"/>
<path d="M5 19.5h14"/>
</svg>
<span>Télécharger tout (ZIP)</span>
</button>
</div>
<p id="zip-status" class="zip-status" aria-live="polite" hidden></p>
+98 -35
View File
@@ -1,38 +1,58 @@
/* Page galerie admin — mobile-first mais utilisable au clavier/souris sur
desktop (l'admin peut consulter depuis un ordinateur), sans dépendance
externe. Mêmes tokens de couleur/rayon/ombre que la page invité
externe. Mêmes tokens de couleur/rayon/ombre/police que la page invité
(src/e/style.css) pour rester cohérent visuellement — c'est aussi l'écran
que verront les prospects (photographes, wedding planners) lors d'une
démo, donc soigné au même niveau que la page invité. */
démo, donc soigné au même niveau que la page invité. Palette et police
Fraunces alignées sur la passe 2 (2026-09-16) : voir src/e/style.css pour
la justification complète de la direction créative. */
@font-face {
font-family: "Fraunces";
src: url("/vendor/fonts/fraunces-latin-600-normal.woff2") format("woff2");
font-weight: 600;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F,
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
:root {
color-scheme: light;
--color-bg: #faf8f5;
--color-bg: #fff8f0;
--color-surface: #ffffff;
--color-surface-muted: #f2ede4;
--color-text: #262420;
--color-muted: #6f6a62;
--color-border: #e3ddd2;
--color-surface-muted: #fdecdd;
--color-text: #2b2016;
--color-muted: #8a7869;
--color-border: #f1ddc6;
--color-accent: #8a5a44;
--color-accent-hover: #6e4735;
--color-accent-soft: #f1e3d8;
--color-accent-contrast: #ffffff;
--color-accent: #e35f2b;
--color-accent-hover: #c74c1e;
--color-accent-light: #ff8a52;
--color-accent-soft: #ffe3d0;
--color-accent-contrast: #fffaf6;
--color-error-bg: #fbeceb;
--color-error-text: #7a1f14;
--color-warning-bg: #fbf1de;
--color-warning-text: #7a5a14;
--color-plum: #7c2f56;
--color-plum-soft: #f8e3ee;
--radius-sm: 8px;
--radius: 14px;
--color-error-bg: #fdeae6;
--color-error-text: #a3341c;
--color-warning-bg: #fbe9d8;
--color-warning-text: #8a4a10;
--shadow-sm: 0 1px 2px rgba(38, 36, 32, 0.06);
--shadow-md: 0 6px 20px rgba(38, 36, 32, 0.10);
--radius-sm: 12px;
--radius: 20px;
--radius-pill: 999px;
--shadow-sm: 0 2px 6px rgba(163, 78, 30, 0.1);
--shadow-md: 0 10px 28px rgba(163, 78, 30, 0.16);
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
--font-display: "Fraunces", Georgia, "Times New Roman", serif;
}
* {
@@ -87,10 +107,16 @@ body {
}
.page__header h1 {
font-size: 1.5rem;
font-weight: 700;
font-family: var(--font-display);
font-size: 1.7rem;
font-weight: 600;
letter-spacing: -0.01em;
margin: 0.15rem 0 0;
color: var(--color-accent);
background: linear-gradient(100deg, var(--color-accent) 20%, var(--color-plum) 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.event-meta {
@@ -119,6 +145,23 @@ body {
margin-bottom: 0;
}
.spinner {
display: inline-block;
width: 26px;
height: 26px;
margin-bottom: 0.6rem;
border: 3px solid var(--color-accent-soft);
border-top-color: var(--color-accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.status-banner {
background: var(--color-warning-bg);
color: var(--color-warning-text);
@@ -136,22 +179,33 @@ body {
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
font: inherit;
font-weight: 600;
font-weight: 700;
border: none;
border-radius: var(--radius-sm);
padding: 0.75rem 1rem;
min-height: 44px;
border-radius: var(--radius-pill);
padding: 0.75rem 1.15rem;
min-height: 46px;
cursor: pointer;
background: var(--color-accent);
background: linear-gradient(135deg, var(--color-accent-light), var(--color-accent));
color: var(--color-accent-contrast);
box-shadow: var(--shadow-sm);
transition: transform 0.12s var(--ease-bounce), background 0.12s ease;
}
.btn-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.btn--secondary {
background: var(--color-surface);
color: var(--color-accent);
border: 1px solid var(--color-accent);
color: var(--color-plum);
border: 1.5px solid var(--color-plum);
box-shadow: none;
}
@@ -161,18 +215,18 @@ body {
}
.btn:active:not(:disabled) {
background: var(--color-accent-hover);
transform: translateY(1px);
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-hover));
transform: scale(0.97);
}
.btn--secondary:active:not(:disabled) {
background: var(--color-accent-soft);
background: var(--color-plum-soft);
}
.btn:focus-visible,
a:focus-visible {
outline: 3px solid var(--color-accent);
outline-offset: 1px;
outline: 3px solid var(--color-plum);
outline-offset: 2px;
}
.zip-status {
@@ -201,13 +255,13 @@ a:focus-visible {
display: flex;
flex-direction: column;
box-shadow: var(--shadow-sm);
transition: box-shadow 0.15s ease, transform 0.15s ease;
transition: box-shadow 0.15s var(--ease-bounce), transform 0.15s var(--ease-bounce);
}
@media (hover: hover) {
.photo-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
transform: translateY(-3px) rotate(-0.6deg);
}
}
@@ -284,3 +338,12 @@ a:focus-visible {
.photo-card__download:hover {
background: var(--color-accent-hover);
}
@media (prefers-reduced-motion: reduce) {
.spinner,
.photo-card,
.btn {
animation: none !important;
transition: none !important;
}
}
+62 -23
View File
@@ -17,21 +17,51 @@
tables lors de l'événement.
-->
<style>
/* Palette/police alignées sur la passe 2 (2026-09-16) du reste du produit
— voir src/e/style.css pour la justification complète de la direction
créative (punchy/friendly plutôt que sobre). Le carton imprimé est un
livrable physique posé sur les tables : mêmes contraintes qu'ailleurs
côté fond (pas de dépendance CDN), mais on évite ici le texte en
dégradé (background-clip:text) sur les éléments imprimés — beaucoup
d'utilisateurs impriment sans l'option "Imprimer les arrière-plans",
ce qui ferait disparaître un titre en dégradé. La personnalité vient
donc de la police Fraunces + d'une couleur pleine, pas d'un effet qui
dépend d'un réglage d'impression. */
@font-face {
font-family: "Fraunces";
src: url("/vendor/fonts/fraunces-latin-600-normal.woff2") format("woff2");
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Fraunces";
src: url("/vendor/fonts/fraunces-latin-500-italic.woff2") format("woff2");
font-weight: 500;
font-style: italic;
font-display: swap;
}
:root {
color-scheme: light;
--color-bg: #faf8f5;
--color-bg: #fff8f0;
--color-surface: #ffffff;
--color-surface-muted: #f2ede4;
--color-text: #262420;
--color-muted: #6f6a62;
--color-border: #e3ddd2;
--color-accent: #8a5a44;
--color-accent-hover: #6e4735;
--color-accent-contrast: #ffffff;
--radius-sm: 8px;
--radius: 14px;
--color-surface-muted: #fdecdd;
--color-text: #2b2016;
--color-muted: #8a7869;
--color-border: #f1ddc6;
--color-accent: #e35f2b;
--color-accent-hover: #c74c1e;
--color-accent-light: #ff8a52;
--color-accent-contrast: #fffaf6;
--color-plum: #7c2f56;
--radius-sm: 12px;
--radius: 20px;
--radius-pill: 999px;
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
--font-display: "Fraunces", Georgia, "Times New Roman", serif;
}
* {
@@ -126,23 +156,27 @@
width: 100%;
margin-top: 1.1rem;
font: inherit;
font-weight: 600;
font-weight: 700;
border: none;
border-radius: var(--radius-sm);
border-radius: var(--radius-pill);
padding: 0.8rem 1rem;
min-height: 46px;
cursor: pointer;
background: var(--color-accent);
background: linear-gradient(135deg, var(--color-accent-light), var(--color-accent));
color: var(--color-accent-contrast);
box-shadow: 0 6px 16px rgba(195, 76, 30, 0.22);
transition: transform 0.12s var(--ease-bounce), background 0.12s ease;
}
#printBtn:disabled {
opacity: 0.55;
cursor: not-allowed;
box-shadow: none;
}
#printBtn:active:not(:disabled) {
background: var(--color-accent-hover);
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-hover));
transform: scale(0.98);
}
/* --- Zone d'aperçu (écran uniquement) --- */
@@ -158,8 +192,8 @@
width: 148mm;
height: 210mm;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 4mm;
border: 1.5mm solid var(--color-accent);
border-radius: 6mm;
box-shadow: 0 10px 30px rgba(38, 36, 32, 0.18);
padding: 14mm 12mm;
display: flex;
@@ -189,17 +223,22 @@
}
.card__title {
font-size: 8mm;
font-weight: 700;
font-family: var(--font-display);
font-size: 9mm;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--color-accent-hover);
margin: 6mm 0 2mm;
line-height: 1.2;
word-break: break-word;
}
.card__prompt {
font-size: 4.2mm;
color: var(--color-muted);
font-family: var(--font-display);
font-style: italic;
font-weight: 500;
font-size: 4.6mm;
color: var(--color-plum);
margin: 0 0 8mm;
}
@@ -210,7 +249,7 @@
align-items: center;
justify-content: center;
background: var(--color-surface-muted);
border-radius: 3mm;
border-radius: 5mm;
padding: 5mm;
}
@@ -326,7 +365,7 @@
<span class="card__brand-name">photobooth</span>
</div>
<h2 class="card__title" id="cardTitle">Votre événement</h2>
<p class="card__prompt">Scannez pour déposer vos photos</p>
<p class="card__prompt">Scanne, souris, envoie ta photo&nbsp;!</p>
<div class="card__qr" id="qrContainer">
<span class="card__qr-placeholder">Saisissez un slug pour générer le QR</span>
</div>
@@ -348,7 +387,7 @@
<span class="card__brand-name">photobooth</span>
</div>
<h2 class="card__title" id="cardTitlePrint">Votre événement</h2>
<p class="card__prompt">Scannez pour déposer vos photos</p>
<p class="card__prompt">Scanne, souris, envoie ta photo&nbsp;!</p>
<div class="card__qr" id="qrContainerPrint"></div>
<p class="card__url" id="cardUrlPrint"></p>
<p class="card__footnote">Vos photos sont supprimées automatiquement 7 jours après l'événement.</p>
+17 -14
View File
@@ -8,23 +8,26 @@
<style>
/* Mêmes tokens que src/e/style.css et src/admin/gallery/style.css —
cohérence visuelle même sur cet outil interne que les clients ne
voient pas (cf. retour Kévin 2026-09-16). */
voient pas (cf. retour Kévin 2026-09-16). Palette/rayons resynchronisés
avec la passe 2 (2026-09-16, direction "punchy/friendly") ; outil
interne, priorité basse donc pas de police d'accent ni d'animation
ajoutées ici, juste la cohérence de base (couleurs, rayons). */
:root {
color-scheme: light;
--color-bg: #faf8f5;
--color-bg: #fff8f0;
--color-surface: #ffffff;
--color-surface-muted: #f2ede4;
--color-text: #262420;
--color-muted: #6f6a62;
--color-border: #e3ddd2;
--color-accent: #8a5a44;
--color-accent-hover: #6e4735;
--color-accent-soft: #f1e3d8;
--color-accent-contrast: #ffffff;
--color-error-text: #7a1f14;
--radius-sm: 8px;
--radius: 14px;
--shadow-sm: 0 1px 2px rgba(38, 36, 32, 0.06);
--color-surface-muted: #fdecdd;
--color-text: #2b2016;
--color-muted: #8a7869;
--color-border: #f1ddc6;
--color-accent: #e35f2b;
--color-accent-hover: #c74c1e;
--color-accent-soft: #ffe3d0;
--color-accent-contrast: #fffaf6;
--color-error-text: #a3341c;
--radius-sm: 12px;
--radius: 20px;
--shadow-sm: 0 2px 6px rgba(163, 78, 30, 0.1);
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
}
+50 -6
View File
@@ -12,6 +12,8 @@
lors du déploiement, hors scope de ce fichier front.
-->
<meta name="robots" content="noindex, nofollow">
<meta name="theme-color" content="#faf6f0">
<link rel="preload" href="/vendor/fonts/fraunces-latin-600-normal.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/e/style.css">
</head>
<body>
@@ -25,19 +27,43 @@
<span class="brand__name">photobooth</span>
</p>
<h1 id="event-name">Ajouter une photo</h1>
<p class="page__kicker">Une photo, un petit mot, et hop dans l'album&nbsp;!</p>
</header>
<section id="status-loading" class="state-block" role="status">
<section id="status-loading" class="state-block state-block--loading" role="status">
<span class="spinner" aria-hidden="true"></span>
<p>Chargement de l'événement…</p>
</section>
<section id="status-error" class="state-block state-block--error" hidden role="alert">
<div class="state-icon state-icon--error" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" focusable="false">
<circle cx="12" cy="12" r="9"/>
<path d="M12 8v5"/>
<path d="M12 16.2v.1"/>
</svg>
</div>
<div class="state-icon state-icon--limit" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" focusable="false">
<path d="M12 20.2s-7.5-4.6-7.5-10.1a4.4 4.4 0 0 1 7.5-3.1 4.4 4.4 0 0 1 7.5 3.1c0 5.5-7.5 10.1-7.5 10.1Z"/>
</svg>
</div>
<p id="status-error-message"></p>
<button type="button" id="retry-btn" hidden>Réessayer</button>
</section>
<section id="success-block" class="state-block state-block--success" hidden role="status">
<p>Merci&nbsp;! Ta photo est bien envoyée 🎉</p>
<div class="confetti" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span><span></span>
</div>
<div class="success-check" aria-hidden="true">
<svg viewBox="0 0 52 52" fill="none" focusable="false">
<circle class="success-check__circle" cx="26" cy="26" r="23" stroke-width="2.5"/>
<path class="success-check__mark" d="M15 27.5 22.5 35 38 18" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<p class="success-title">C'est envoyé&nbsp;!</p>
<p class="success-text">Ta photo vient de rejoindre la fête. Merci d'avoir été là&nbsp;!</p>
<button type="button" id="add-another-btn">Ajouter une autre photo</button>
</section>
@@ -47,16 +73,20 @@
<!-- Deux entrées séparées : "photo-input" (capture="environment")
ouvre directement l'appareil photo, "photo-input-gallery" ouvre
le sélecteur de fichiers classique. Décision actée avec Kévin le
2026-09-16 : les deux doivent être accessibles en un tap. -->
2026-09-16 : les deux doivent être accessibles en un tap.
Hiérarchie visuelle ajoutée : "Prendre une photo" est l'action
attendue pour l'immense majorité des invités pendant l'événement
(capture sur le moment), donc traitée en primaire ; "Depuis la
galerie" reste secondaire mais tout aussi accessible. -->
<div class="photo-source-buttons">
<label for="photo-input" class="btn-secondary">
<label for="photo-input" class="btn-source btn-source--primary">
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M4 8h3l1.5-2h7L17 8h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1Z"/>
<circle cx="12" cy="13" r="3.5"/>
</svg>
<span>Prendre une photo</span>
</label>
<label for="photo-input-gallery" class="btn-secondary">
<label for="photo-input-gallery" class="btn-source btn-source--secondary">
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<rect x="3" y="4" width="18" height="16" rx="2.5"/>
<circle cx="8.5" cy="9.5" r="1.5"/>
@@ -87,6 +117,7 @@
<img id="preview" class="preview" alt="Aperçu de la photo choisie" hidden>
<div class="field-group">
<div class="field">
<label for="nom-input">Ton prénom <span class="optional">(facultatif)</span></label>
<input
@@ -109,8 +140,14 @@
placeholder="Ton petit mot pour l'occasion…"
></textarea>
</div>
</div>
<p class="legal-notice">
<svg class="legal-notice__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<rect x="4.5" y="10.5" width="15" height="9.5" rx="2"/>
<path d="M8 10.5V7.5a4 4 0 0 1 8 0v3"/>
</svg>
<span>
En déposant une photo, vous acceptez qu'elle soit stockée temporairement
dans le cadre de cet événement et mise à disposition de l'organisateur.
Vos photos sont automatiquement supprimées 7 jours après la date de
@@ -118,11 +155,18 @@
d'autres fins, ni transmise à des tiers. Vous pouvez demander la
suppression anticipée d'une photo en contactant l'organisateur de
l'événement.
</span>
</p>
<p id="progress-text" class="progress-text" aria-live="polite"></p>
<button type="submit" id="submit-btn">Envoyer ma photo</button>
<button type="submit" id="submit-btn">
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M21 4 3 11.5l7 2.5"/>
<path d="M10 14l2.5 7L21 4"/>
</svg>
<span id="submit-btn-label">Envoyer ma photo</span>
</button>
</form>
</main>
+413 -66
View File
@@ -1,42 +1,92 @@
/* Page upload invité — mobile-first, sobre, sans dépendance externe.
/* Page upload invité — mobile-first, sans dépendance externe.
Système de design partagé avec la galerie admin (src/admin/gallery/style.css) :
mêmes tokens de couleur/rayon/ombre, pour une identité cohérente entre les
deux écrans (voir docs/decisions.md pour le contexte produit). */
deux écrans (voir docs/decisions.md pour le contexte produit).
Direction créative (passe 2, 2026-09-16) — voir rapport de la passe pour
le détail complet, résumé ici :
Retour direct de Kévin sur la 1ère passe : trop "démo sage", pas assez
"envie de participer". Consigne resserrée en cours de route : viser
PUNCHY / FRIENDLY / FUN, pas un premium feutré silencieux. Traduction :
- Palette plus affirmée : on garde la famille chaude (cohérente avec
l'identité perso de Kévin / Rêves Liquides) mais on la sature — un
corail vif en primaire plutôt que le brun terracotta sourd de la V1,
avec un prune et un doré en accents secondaires (confettis, badges).
- Formes plus rondes/organiques (rayons agrandis, boutons pilule).
- Micro-animations qui ont du peps : coche qui "pop" avec rebond,
confettis colorés au succès, spinner de chargement, easing "overshoot"
sur les interactions plutôt que du linear plat.
- Ton de voix plus souriant dans les micro-copies (succès, limite
atteinte) — cf. src/e/upload.js pour les textes.
- Police d'accent Fraunces (serif à forte personnalité, chaleureuse et
un peu "pop") en plus de la pile système pour les titres/moments clés
— auto-hébergée en .woff2, 2 graisses (~41 Ko au total), voir
src/vendor/fonts/ et src/vendor/NOTICE.md. Le corps de texte reste
100% système (poids nul). */
@font-face {
font-family: "Fraunces";
src: url("/vendor/fonts/fraunces-latin-600-normal.woff2") format("woff2");
font-weight: 600;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F,
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Fraunces";
src: url("/vendor/fonts/fraunces-latin-500-italic.woff2") format("woff2");
font-weight: 500;
font-style: italic;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F,
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
:root {
color-scheme: light;
/* Couleurs — palette chaude et sobre, cohérente avec l'identité perso de
Kévin Lecou (Rêves Liquides). L'accent brun/terracotta est inchangé par
rapport à la V1 déployée : on affine la palette autour, pas de rupture. */
--color-bg: #faf8f5;
/* Couleurs — famille chaude toujours ancrée dans l'identité de Kévin,
mais poussée en saturation pour un rendu festif plutôt que sobre. */
--color-bg: #fff8f0;
--color-surface: #ffffff;
--color-surface-muted: #f2ede4;
--color-text: #262420;
--color-muted: #6f6a62;
--color-border: #e3ddd2;
--color-surface-muted: #fdecdd;
--color-text: #2b2016;
--color-muted: #8a7869;
--color-border: #f1ddc6;
--color-accent: #8a5a44;
--color-accent-hover: #6e4735;
--color-accent-soft: #f1e3d8;
--color-accent-contrast: #ffffff;
--color-accent: #e35f2b;
--color-accent-hover: #c74c1e;
--color-accent-light: #ff8a52;
--color-accent-soft: #ffe3d0;
--color-accent-contrast: #fffaf6;
--color-error-bg: #fbeceb;
--color-error-text: #7a1f14;
--color-success-bg: #eaf3ec;
--color-success-text: #1e5c2d;
--color-plum: #7c2f56;
--color-plum-soft: #f8e3ee;
/* Rayon à deux niveaux : sm pour les petits éléments (inputs, chips),
base pour les blocs/cards — donne une identité "arrondie/artisanale"
un peu plus marquée que le 10px uniforme de la V1. */
--radius-sm: 8px;
--radius: 14px;
--color-gold: #f0a91e;
--color-gold-soft: #fef0d3;
--shadow-sm: 0 1px 2px rgba(38, 36, 32, 0.06);
--shadow-md: 0 6px 20px rgba(38, 36, 32, 0.10);
--color-error-bg: #fdeae6;
--color-error-text: #a3341c;
--color-success-bg: #fff1de;
--color-success-text: #8a4a10;
--color-limit-bg: #fbe9d8;
--color-limit-text: #8a4a10;
--radius-sm: 12px;
--radius: 20px;
--radius-pill: 999px;
--shadow-sm: 0 2px 6px rgba(163, 78, 30, 0.1);
--shadow-md: 0 10px 28px rgba(163, 78, 30, 0.16);
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
--font-display: "Fraunces", Georgia, "Times New Roman", serif;
}
* {
@@ -64,7 +114,7 @@ body {
.page__header {
text-align: center;
margin-bottom: 1.5rem;
margin-bottom: 1.6rem;
}
/* Bloc de marque — discret, rassure l'invité (outil "pro") et signe le
@@ -76,7 +126,7 @@ body {
align-items: center;
justify-content: center;
gap: 0.4rem;
margin-bottom: 0.6rem;
margin-bottom: 0.7rem;
color: var(--color-muted);
}
@@ -95,27 +145,59 @@ body {
}
.page__header h1 {
font-size: 1.5rem;
font-weight: 700;
font-family: var(--font-display);
font-size: 2rem;
font-weight: 600;
letter-spacing: -0.01em;
margin: 0.15rem 0 0;
line-height: 1.15;
margin: 0;
/* Titre en dégradé corail -> doré : la touche "pop" la plus visible de la
page, réservée à ce seul élément pour ne pas diluer son effet. Fallback
: couleur pleine (--color-accent) si background-clip:text n'est pas
supporté (support large sur mobile Safari/Chrome en pratique). */
color: var(--color-accent);
background: linear-gradient(100deg, var(--color-accent) 20%, var(--color-gold) 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.page__kicker {
font-family: var(--font-display);
font-style: italic;
font-weight: 500;
font-size: 1.05rem;
color: var(--color-plum);
margin: 0.35rem 0 0;
}
.state-block {
text-align: center;
padding: 1.25rem 1rem;
padding: 1.5rem 1.2rem;
border-radius: var(--radius);
margin-bottom: 1rem;
}
.state-block--loading {
color: var(--color-muted);
}
.state-block--error {
background: var(--color-error-bg);
color: var(--color-error-text);
}
.state-block--error.state-block--limit {
background: var(--color-limit-bg);
color: var(--color-limit-text);
}
.state-block--success {
background: var(--color-success-bg);
position: relative;
background: linear-gradient(180deg, var(--color-success-bg), var(--color-surface) 75%);
color: var(--color-success-text);
overflow: visible;
padding-top: 1.75rem;
}
.state-block p {
@@ -126,10 +208,67 @@ body {
margin-bottom: 0;
}
/* Spinner de chargement — anneau CSS pur, aucune dépendance. Remplace le
simple texte "Chargement…" par un repère visuel qui bouge : la première
impression de la page ne doit jamais sembler figée/en panne. */
.spinner {
display: inline-block;
width: 26px;
height: 26px;
margin-bottom: 0.6rem;
border: 3px solid var(--color-accent-soft);
border-top-color: var(--color-accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Icônes des états erreur / limite atteinte — les deux existent toujours
dans le DOM, affichées/masquées via la classe portée par le bloc parent
(state-block--limit vs par défaut = erreur). Le choix entre les deux se
fait côté JS uniquement en togglant cette classe (aucune autre logique
déplacée dans le CSS). */
.state-icon {
display: none;
width: 44px;
height: 44px;
margin: 0 auto 0.6rem;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.state-block--error .state-icon--error {
display: flex;
background: rgba(163, 52, 28, 0.12);
color: var(--color-error-text);
}
.state-block--error.state-block--limit .state-icon--error {
display: none;
}
.state-block--limit .state-icon--limit {
display: flex;
background: rgba(138, 74, 16, 0.14);
color: var(--color-gold);
animation: pop-in 0.5s var(--ease-bounce) both;
}
.state-icon svg {
width: 24px;
height: 24px;
}
.upload-form {
display: flex;
flex-direction: column;
gap: 1.1rem;
gap: 1.15rem;
}
.field {
@@ -139,7 +278,7 @@ body {
}
.field label {
font-weight: 600;
font-weight: 700;
font-size: 0.95rem;
}
@@ -149,20 +288,34 @@ body {
font-size: 0.85rem;
}
/* Regroupe prénom + petit mot dans une même carte : crée un second niveau
de hiérarchie visuelle (la photo reste l'action principale au-dessus,
ces champs facultatifs sont secondaires et "posés" ensemble). */
.field-group {
display: flex;
flex-direction: column;
gap: 1rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: 1.1rem;
}
input[type="text"],
input[type="file"],
textarea {
font: inherit;
padding: 0.7rem 0.75rem;
border: 1px solid var(--color-border);
padding: 0.7rem 0.8rem;
border: 1.5px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-surface);
color: inherit;
width: 100%;
transition: border-color 0.15s ease;
}
input[type="file"] {
padding: 0.55rem 0.5rem;
input[type="text"]:focus,
textarea:focus {
border-color: var(--color-accent);
}
.visually-hidden {
@@ -182,27 +335,51 @@ input[type="file"] {
gap: 0.6rem;
}
.btn-secondary {
/* Boutons "Prendre une photo" / "Depuis la galerie" — forme pilule (identité
plus ronde/organique que la V1) avec une hiérarchie claire : la capture
directe est l'action attendue pour la quasi-totalité des invités pendant
l'événement (le moment se vit là, tout de suite), donc traitée en
primaire pleine couleur ; la galerie reste secondaire mais tout aussi
accessible en un tap (contrainte actée le 2026-09-16). */
.btn-source {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
text-align: center;
font-weight: 600;
font-size: 0.9rem;
padding: 0.85rem 0.6rem;
min-height: 48px;
border: 1px solid var(--color-accent);
border-radius: var(--radius-sm);
color: var(--color-accent);
background: var(--color-surface);
font-weight: 700;
font-size: 0.88rem;
padding: 0.9rem 0.6rem;
min-height: 50px;
border-radius: var(--radius-pill);
cursor: pointer;
transition: transform 0.12s var(--ease-bounce), box-shadow 0.12s ease, background 0.12s ease;
}
.btn-secondary:active {
background: var(--color-accent-soft);
transform: translateY(1px);
.btn-source--primary {
border: none;
background: linear-gradient(135deg, var(--color-accent-light), var(--color-accent));
color: var(--color-accent-contrast);
box-shadow: var(--shadow-sm);
}
.btn-source--secondary {
border: 1.5px solid var(--color-plum);
background: var(--color-surface);
color: var(--color-plum);
}
.btn-source:active {
transform: scale(0.96);
}
.btn-source--primary:active {
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-hover));
}
.btn-source--secondary:active {
background: var(--color-plum-soft);
}
/* Icônes SVG inline (aucune requête réseau) : remplacent les emoji des
@@ -219,7 +396,8 @@ input[type="file"] {
.file-chosen-label {
font-size: 0.85rem;
color: var(--color-muted);
margin: 0;
margin: 0.5rem 0 0;
text-align: center;
}
textarea {
@@ -230,8 +408,8 @@ textarea {
input:focus-visible,
textarea:focus-visible,
button:focus-visible {
outline: 3px solid var(--color-accent);
outline-offset: 1px;
outline: 3px solid var(--color-plum);
outline-offset: 2px;
}
.preview {
@@ -240,50 +418,71 @@ button:focus-visible {
object-fit: contain;
border-radius: var(--radius);
background: var(--color-surface-muted);
animation: pop-in 0.35s var(--ease-bounce) both;
}
.legal-notice {
font-size: 0.8rem;
display: flex;
align-items: flex-start;
gap: 0.6rem;
font-size: 0.78rem;
color: var(--color-muted);
background: var(--color-surface-muted);
border-radius: var(--radius);
padding: 0.8rem 0.9rem;
padding: 0.85rem 0.9rem;
margin: 0;
}
.legal-notice__icon {
width: 18px;
height: 18px;
flex-shrink: 0;
margin-top: 0.1rem;
color: var(--color-plum);
}
.progress-text {
min-height: 1.2em;
font-size: 0.9rem;
color: var(--color-muted);
font-weight: 600;
color: var(--color-plum);
margin: 0;
text-align: center;
}
button {
font: inherit;
font-weight: 600;
font-weight: 700;
border: none;
border-radius: var(--radius-sm);
padding: 0.85rem 1rem;
min-height: 48px;
border-radius: var(--radius-pill);
padding: 0.9rem 1rem;
min-height: 50px;
cursor: pointer;
transition: transform 0.12s var(--ease-bounce), background 0.12s ease, opacity 0.12s ease;
}
#submit-btn,
#add-another-btn {
background: var(--color-accent);
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
background: linear-gradient(135deg, var(--color-accent-light), var(--color-accent));
color: var(--color-accent-contrast);
box-shadow: var(--shadow-sm);
box-shadow: var(--shadow-md);
font-size: 1rem;
}
#submit-btn:active:not(:disabled),
#add-another-btn:active {
background: var(--color-accent-hover);
transform: scale(0.97);
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-hover));
}
#submit-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
box-shadow: none;
}
#retry-btn {
@@ -291,6 +490,154 @@ button {
color: #fff;
}
button:active {
transform: translateY(1px);
/* ---------------------------------------------------------------------
Écran de succès — LE moment à soigner : c'est la dernière impression
que garde l'invité. Coche qui "pop" avec rebond + petite volée de
confettis colorés (pur CSS, aucune dépendance JS supplémentaire).
------------------------------------------------------------------ */
.success-check {
position: relative;
width: 76px;
height: 76px;
margin: 0 auto 0.9rem;
animation: pop-in 0.55s var(--ease-bounce) both;
}
.success-check svg {
width: 100%;
height: 100%;
overflow: visible;
}
.success-check__circle {
stroke: var(--color-accent);
fill: var(--color-gold-soft);
stroke-dasharray: 145;
stroke-dashoffset: 145;
animation: draw 0.5s ease-out 0.1s forwards;
}
.success-check__mark {
stroke: var(--color-accent);
fill: none;
stroke-dasharray: 32;
stroke-dashoffset: 32;
animation: draw 0.35s ease-out 0.5s forwards;
}
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
@keyframes pop-in {
0% {
transform: scale(0.3);
opacity: 0;
}
65% {
transform: scale(1.12);
opacity: 1;
}
100% {
transform: scale(1);
}
}
.confetti {
position: absolute;
inset: 0;
pointer-events: none;
overflow: visible;
}
.confetti span {
position: absolute;
top: 0.5rem;
width: 9px;
height: 9px;
opacity: 0;
animation: confetti-fall 1.1s ease-out forwards;
}
.confetti span:nth-child(1) {
left: 18%;
background: var(--color-accent);
border-radius: 2px;
animation-delay: 0.05s;
}
.confetti span:nth-child(2) {
left: 32%;
top: 0.2rem;
background: var(--color-gold);
border-radius: 50%;
animation-delay: 0.15s;
}
.confetti span:nth-child(3) {
left: 48%;
background: var(--color-plum);
border-radius: 2px;
animation-delay: 0.02s;
}
.confetti span:nth-child(4) {
left: 62%;
background: var(--color-accent-light);
border-radius: 50%;
animation-delay: 0.22s;
}
.confetti span:nth-child(5) {
left: 74%;
background: var(--color-gold);
border-radius: 2px;
animation-delay: 0.1s;
}
.confetti span:nth-child(6) {
left: 86%;
background: var(--color-plum);
border-radius: 50%;
animation-delay: 0.28s;
}
@keyframes confetti-fall {
0% {
transform: translateY(-6px) rotate(0deg) scale(0.6);
opacity: 0;
}
15% {
opacity: 1;
}
100% {
transform: translateY(92px) rotate(300deg) scale(1);
opacity: 0;
}
}
.success-title {
font-family: var(--font-display);
font-size: 1.75rem;
font-weight: 600;
color: var(--color-accent-hover);
margin: 0 0 0.3rem;
}
.success-text {
font-size: 0.95rem;
margin: 0 0 1.3rem;
}
@media (prefers-reduced-motion: reduce) {
.spinner,
.success-check,
.success-check__circle,
.success-check__mark,
.confetti span,
.preview,
.state-icon--limit,
button,
.btn-source {
animation: none !important;
transition: none !important;
}
}
+11 -2
View File
@@ -47,6 +47,7 @@ const nomInput = document.getElementById("nom-input");
const messageInput = document.getElementById("message-input");
const progressText = document.getElementById("progress-text");
const submitBtn = document.getElementById("submit-btn");
const submitBtnLabel = document.getElementById("submit-btn-label");
// ---------------------------------------------------------------------------
// État
@@ -72,7 +73,7 @@ const ERROR_MESSAGES = {
"event-closed":
"Cet événement n'accepte plus de photos (il est clos). Merci quand même d'avoir essayé !",
"guest-limit-reached":
"Tu as déjà envoyé le nombre maximum de photos autorisé pour cet événement. Merci pour ta participation !",
"Dis donc, on ne t'arrête plus ! Tu as atteint le nombre de photos autorisé pour cet événement. Merci d'avoir aussi bien joué le jeu !",
"file-missing": "Choisis d'abord une photo avant d'envoyer.",
"file-not-image":
"Ce fichier n'est pas une photo. Choisis une image (JPEG, PNG, HEIC…).",
@@ -239,6 +240,11 @@ function showError(rawErr, retryAction) {
successBlock.hidden = true;
errorBlock.hidden = false;
errorMessageEl.textContent = ERROR_MESSAGES[err.code] || ERROR_MESSAGES.unknown;
// Traitement visuel dédié (chaleureux, pas rouge "erreur") pour la limite
// de photos atteinte : ce n'est pas un échec pour l'invité, juste une
// limite après participation. Pur toggle de classe CSS, cf. style.css
// (.state-block--limit) — aucun changement de logique ici.
errorBlock.classList.toggle("state-block--limit", err.code === "guest-limit-reached");
state.lastRetryAction = retryAction || null;
const canRetry = Boolean(retryAction) && RETRYABLE_CODES.has(err.code);
@@ -290,7 +296,10 @@ function setProgress(text) {
function setSubmitting(isSubmitting) {
state.isSubmitting = isSubmitting;
submitBtn.disabled = isSubmitting;
submitBtn.textContent = isSubmitting ? "Envoi en cours…" : "Envoyer ma photo";
// On cible le <span> du libellé (et non submitBtn.textContent) pour ne pas
// effacer l'icône SVG du bouton à chaque bascule — changement purement
// visuel, la logique de soumission elle-même est inchangée.
submitBtnLabel.textContent = isSubmitting ? "Envoi en cours…" : "Envoyer ma photo";
}
// ---------------------------------------------------------------------------
+20
View File
@@ -25,3 +25,23 @@ depuis la source indiquée.
`src/e/upload.js`. La majorité des invités (Android, ou iPhone dont Safari
a déjà transcodé le fichier en JPEG lors de la sélection) ne téléchargeront
jamais ce fichier.
## Fraunces (fonts/)
- Fichiers : `fonts/fraunces-latin-600-normal.woff2` (18 Ko),
`fonts/fraunces-latin-500-italic.woff2` (22,9 Ko).
Sous-ensemble « latin » uniquement (couvre les accents français : la
plage Google Fonts « latin » inclut Latin-1 Supplement + œ/Œ, suffisant
pour le français — pas besoin de « latin-ext », plus lourd).
- Licence : SIL Open Font License 1.1 (police Fraunces par Undercase Type /
Oh no Type Co, distribuée via Google Fonts).
- Source : régénérée par le projet **Fontsource**
(`https://cdn.jsdelivr.net/npm/@fontsource/fraunces@5/files/`), qui
convertit Google Fonts en fichiers statiques par graisse — évite de servir
la version variable complète (beaucoup plus lourde) alors qu'on n'utilise
que 2 graisses fixes.
- Usage : police d'accent (titres, moment de succès) chargée en
`@font-face` avec `font-display: swap`, jamais via lien Google Fonts en
direct (règle du projet). Voir `src/e/style.css` (`--font-display`).
Le corps de texte reste sur la pile système (`--font-sans`), aucun poids
supplémentaire pour le texte courant.
Binary file not shown.
Binary file not shown.