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.
644 lines
15 KiB
CSS
644 lines
15 KiB
CSS
/* 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).
|
|
|
|
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 — 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: #fdecdd;
|
|
--color-text: #2b2016;
|
|
--color-muted: #8a7869;
|
|
--color-border: #f1ddc6;
|
|
|
|
--color-accent: #e35f2b;
|
|
--color-accent-hover: #c74c1e;
|
|
--color-accent-light: #ff8a52;
|
|
--color-accent-soft: #ffe3d0;
|
|
--color-accent-contrast: #fffaf6;
|
|
|
|
--color-plum: #7c2f56;
|
|
--color-plum-soft: #f8e3ee;
|
|
|
|
--color-gold: #f0a91e;
|
|
--color-gold-soft: #fef0d3;
|
|
|
|
--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;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html {
|
|
-webkit-text-size-adjust: 100%;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
font-family: var(--font-sans);
|
|
background: var(--color-bg);
|
|
color: var(--color-text);
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.page {
|
|
max-width: 480px;
|
|
margin: 0 auto;
|
|
padding: 1.5rem 1.1rem 3rem;
|
|
min-height: 100dvh;
|
|
}
|
|
|
|
.page__header {
|
|
text-align: center;
|
|
margin-bottom: 1.6rem;
|
|
}
|
|
|
|
/* Bloc de marque — discret, rassure l'invité (outil "pro") et signe le
|
|
produit pour les organisateurs/prestataires qui verront la page.
|
|
Volontairement minuscule : ce n'est pas l'écran de l'invité qui doit
|
|
porter la marque de Kévin, c'est un clin d'œil en filigrane. */
|
|
.brand {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.4rem;
|
|
margin-bottom: 0.7rem;
|
|
color: var(--color-muted);
|
|
}
|
|
|
|
.brand__mark {
|
|
width: 16px;
|
|
height: 16px;
|
|
flex-shrink: 0;
|
|
color: var(--color-accent);
|
|
}
|
|
|
|
.brand__name {
|
|
font-size: 0.7rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.13em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.page__header h1 {
|
|
font-family: var(--font-display);
|
|
font-size: 2rem;
|
|
font-weight: 600;
|
|
letter-spacing: -0.01em;
|
|
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.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 {
|
|
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 {
|
|
margin: 0 0 0.75rem;
|
|
}
|
|
|
|
.state-block p:last-child {
|
|
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.15rem;
|
|
}
|
|
|
|
.field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.4rem;
|
|
}
|
|
|
|
.field label {
|
|
font-weight: 700;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.field .optional {
|
|
font-weight: 400;
|
|
color: var(--color-muted);
|
|
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"],
|
|
textarea {
|
|
font: inherit;
|
|
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="text"]:focus,
|
|
textarea:focus {
|
|
border-color: var(--color-accent);
|
|
}
|
|
|
|
.visually-hidden {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
padding: 0;
|
|
margin: -1px;
|
|
overflow: hidden;
|
|
clip: rect(0, 0, 0, 0);
|
|
white-space: nowrap;
|
|
border: 0;
|
|
}
|
|
|
|
.photo-source-buttons {
|
|
display: flex;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
/* 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: 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-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
|
|
boutons "Prendre une photo" / "Depuis la galerie". Le rendu des emoji
|
|
varie fortement selon l'OS (Android/iOS/Windows) — pas assez maîtrisé
|
|
pour une pièce de portfolio. Les icônes héritent de currentColor, donc
|
|
suivent l'état du bouton sans CSS supplémentaire. */
|
|
.btn-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.file-chosen-label {
|
|
font-size: 0.85rem;
|
|
color: var(--color-muted);
|
|
margin: 0.5rem 0 0;
|
|
text-align: center;
|
|
}
|
|
|
|
textarea {
|
|
resize: vertical;
|
|
min-height: 4.5rem;
|
|
}
|
|
|
|
input:focus-visible,
|
|
textarea:focus-visible,
|
|
button:focus-visible {
|
|
outline: 3px solid var(--color-plum);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.preview {
|
|
width: 100%;
|
|
max-height: 320px;
|
|
object-fit: contain;
|
|
border-radius: var(--radius);
|
|
background: var(--color-surface-muted);
|
|
animation: pop-in 0.35s var(--ease-bounce) both;
|
|
}
|
|
|
|
.legal-notice {
|
|
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.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;
|
|
font-weight: 600;
|
|
color: var(--color-plum);
|
|
margin: 0;
|
|
text-align: center;
|
|
}
|
|
|
|
button {
|
|
font: inherit;
|
|
font-weight: 700;
|
|
border: none;
|
|
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 {
|
|
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-md);
|
|
font-size: 1rem;
|
|
}
|
|
|
|
#submit-btn:active:not(:disabled),
|
|
#add-another-btn:active {
|
|
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 {
|
|
background: var(--color-error-text);
|
|
color: #fff;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
É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;
|
|
}
|
|
}
|