Files
photobooth-qr/src/e/style.css
T
KevinLecou 3afd475a55 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.
2026-09-16 15:48:29 +02:00

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;
}
}