Files
photobooth-qr/src/e/style.css
T
KevinLecouandClaude Sonnet 5 e06f8afd3e Corrige le lien vers la galerie inatteignable derrière la modale + renomme "mur" en "fête"
La modale de bienvenue (position: fixed, inset: 0) recouvre tout
l'écran y compris l'en-tête où vivait le seul lien vers la galerie :
au premier chargement (cas le plus fréquent, un seul passage par
invité et par événement), le lien était visuellement présent mais
totalement inatteignable au clic. Ajout d'un lien dupliqué dans la
carte de la modale elle-même. Au passage, "mur" remplacé par "fête"
partout (jugé pas assez fun) pour rester cohérent avec le texte déjà
utilisé sur l'écran de succès.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 09:54:09 +02:00

892 lines
20 KiB
CSS

/* Page upload invité — direction artistique "Polaroid Néon"
(.claude/skills/photobooth-da/SKILL.md). Remplace les 3 passes DA
précédentes (palette corail/prune/doré + Fraunces, voir historique
git) : cet écran suit désormais src/galerie/style.css comme référence
de cohérence (premier écran construit dans cette DA, validé par
Kévin — "c'est vraiment top comme ça !").
Choix pris pendant cette passe, à signaler à Kévin :
- Thème unique sombre (color-scheme: dark), comme src/galerie/style.css,
plutôt que le double thème clair/sombre (prefers-color-scheme) de
l'ancienne DA. Le skill ne demande nulle part de garder un thème
clair, et le premier écran validé n'en a pas — cohérence retenue
plutôt qu'une redondance non demandée. À confirmer si un thème clair
est en fait souhaité pour cet écran précis.
- Titre et éléments de "chrome" en texte plein (pas de dégradé
corail→doré comme l'ancienne DA) : le skill met en garde contre le
"glow/neon CSS artificiel" et l'écran de référence (galerie) n'utilise
aucun dégradé de texte ni halo radial en fond — même logique
appliquée ici pour rester cohérent plutôt que de réintroduire cet
effet.
Contrastes vérifiés par calcul WCAG AA (même méthode que
src/galerie/style.css) : texte clair (#FAF6F0/#E8DFD3) sur fond sombre
(#12100E) est très largement au-dessus du seuil ; texte sombre (#12100E)
sur les 3 couleurs d'accent (corail/jaune/vert) passe confortablement
(6.3:1 à 13.2:1) — utilisé systématiquement pour le texte des CTA en
couleur pleine. Les fonds teintés à faible opacité (états erreur/limite/
succès) restent très proches du noir une fois mélangés à #12100E ; le
texte de ces blocs reste donc en couleur claire standard (--color-text/
--color-muted), jamais en couleur d'accent pleine, par prudence. */
@font-face {
font-family: "General Sans";
src: url("/vendor/fonts/generalsans-700.woff2") format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Caveat";
src: url("/vendor/fonts/caveat-latin-variable.woff2") format("woff2");
font-weight: 400 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter";
src: url("/vendor/fonts/inter-latin-variable.woff2") format("woff2");
font-weight: 400 700;
font-style: normal;
font-display: swap;
}
:root {
color-scheme: dark;
/* Mêmes noms/valeurs que src/galerie/style.css pour la cohérence entre
écrans (voir consigne de la passe). */
--color-bg: #12100e;
--color-ink: #12100e; /* texte sombre sur surfaces claires/accents */
--color-text: #faf6f0;
--color-muted: #e8dfd3;
--color-muted-2: #a89e90;
--color-coral: #ff5e5b;
--color-yellow: #ffd166;
--color-mint: #06d6a0;
--color-border: #e8dfd3;
--radius: 4px; /* réservé aux éléments façon polaroid, non utilisé ici */
--radius-ui: 14px;
--radius-pill: 999px;
--shadow-soft: 0 10px 24px rgba(0, 0, 0, 0.35);
--shadow-card: 0 6px 16px rgba(0, 0, 0, 0.4);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-standard: cubic-bezier(0.22, 1, 0.36, 1);
--font-display: "General Sans", -apple-system, BlinkMacSystemFont, sans-serif;
--font-hand: "Caveat", "Segoe Script", cursive;
--font-ui: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
/* Tokens spécifiques à cet écran (formulaire, états), pas repris de
src/galerie/style.css qui n'en a pas besoin. */
--color-surface: #1e1b18;
--color-surface-muted: #241f1a;
--color-border-ui: rgba(250, 246, 240, 0.16);
--color-border-ui-strong: rgba(250, 246, 240, 0.28);
--color-error-bg: rgba(255, 94, 91, 0.14);
--color-limit-bg: rgba(255, 209, 102, 0.16);
--color-success-bg: rgba(6, 214, 160, 0.12);
--shadow-sm: 0 2px 10px rgba(0, 0, 0, 0.3);
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: var(--font-ui);
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;
animation: fade-rise 0.5s var(--ease-standard) both;
}
@keyframes fade-rise {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.brand {
display: flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
margin-bottom: 0.7rem;
color: var(--color-muted-2);
}
.brand__mark {
width: 16px;
height: 16px;
flex-shrink: 0;
color: var(--color-coral);
}
.brand__name {
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.13em;
text-transform: uppercase;
font-family: var(--font-ui);
}
.page__header h1 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.9rem;
letter-spacing: -0.01em;
line-height: 1.15;
margin: 0;
}
.wall-link {
display: inline-flex;
margin-top: 0.7rem;
font-family: var(--font-ui);
font-weight: 700;
font-size: 0.85rem;
color: var(--color-coral);
text-decoration: none;
}
.wall-link:active {
opacity: 0.7;
}
.page__kicker {
color: var(--color-muted);
font-size: 0.95rem;
margin: 0.4rem 0 0;
}
.state-block {
text-align: center;
padding: 1.5rem 1.2rem;
border-radius: var(--radius-ui);
margin-bottom: 1rem;
}
.state-block--loading {
color: var(--color-muted);
}
.state-block--error {
background: var(--color-error-bg);
color: var(--color-text);
}
.state-block--error.state-block--limit {
background: var(--color-limit-bg);
}
.state-block--success {
position: relative;
background: linear-gradient(180deg, var(--color-success-bg), transparent 75%);
color: var(--color-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. */
.spinner {
display: inline-block;
width: 26px;
height: 26px;
margin-bottom: 0.6rem;
border: 3px solid rgba(255, 94, 91, 0.22);
border-top-color: var(--color-coral);
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). Aucune logique déplacée
dans le CSS, uniquement le rendu visuel. */
.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(255, 94, 91, 0.16);
color: var(--color-coral);
}
.state-block--error.state-block--limit .state-icon--error {
display: none;
}
.state-block--limit .state-icon--limit {
display: flex;
background: rgba(255, 209, 102, 0.18);
color: var(--color-yellow);
animation: pop-in 0.5s var(--ease-spring) both;
}
.state-icon svg {
width: 24px;
height: 24px;
}
.upload-form {
display: flex;
flex-direction: column;
gap: 1.15rem;
animation: fade-rise 0.5s var(--ease-standard) 0.08s both;
}
/* Bug réel corrigé (2026-09-16, conservé lors du passage à la DA "Polaroid
Néon") : une déclaration `display` en CSS auteur passe toujours devant
`[hidden] { display: none }` du navigateur. Sans cette règle explicite,
`form.hidden = true` (JS) ne masquait jamais réellement le formulaire. */
.upload-form[hidden] {
display: none;
}
.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-2);
font-size: 0.85rem;
}
/* Regroupe prénom + petit mot dans une même carte : second niveau de
hiérarchie visuelle (la photo reste l'action principale au-dessus). */
.field-group {
display: flex;
flex-direction: column;
gap: 1rem;
background: var(--color-surface);
border: 1px solid var(--color-border-ui);
border-radius: var(--radius-ui);
padding: 1.1rem;
}
input[type="text"],
textarea {
font: inherit;
padding: 0.7rem 0.8rem;
border: 1.5px solid var(--color-border-ui);
border-radius: 10px;
background: var(--color-bg);
color: inherit;
width: 100%;
transition: border-color 0.18s var(--ease-standard), box-shadow 0.18s var(--ease-standard);
}
input[type="text"]:focus,
textarea:focus {
border-color: var(--color-coral);
box-shadow: 0 0 0 4px rgba(255, 94, 91, 0.22);
}
.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" — la capture directe
est l'action attendue pour la quasi-totalité des invités pendant
l'événement, donc traitée en primaire (corail plein, texte sombre) ;
la galerie reste secondaire mais tout aussi accessible en un tap. */
.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-spring), box-shadow 0.12s ease, background 0.12s ease;
}
.btn-source--primary {
border: none;
background: var(--color-coral);
color: var(--color-ink);
box-shadow: var(--shadow-sm);
}
.btn-source--secondary {
border: 1.5px solid var(--color-border-ui-strong);
background: transparent;
color: var(--color-text);
}
.btn-source:active {
transform: scale(0.96);
}
.btn-source--primary:active {
background: #ff7673;
}
.btn-source--secondary:active {
background: var(--color-surface);
}
/* États hover — uniquement pour les pointeurs qui en ont un (souris/trackpad
d'un prospect qui consulte la page sur desktop lors d'une démo) : jamais
sur mobile tactile. */
@media (hover: hover) {
.btn-source--primary:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-soft);
}
.btn-source--secondary:hover {
transform: translateY(-1px);
background: var(--color-surface);
border-color: var(--color-coral);
}
}
/* Icônes SVG inline (aucune requête réseau), héritent de currentColor. */
.btn-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.file-chosen-label {
font-size: 0.85rem;
color: var(--color-muted-2);
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-coral);
outline-offset: 2px;
}
.preview {
width: 100%;
max-height: 320px;
object-fit: contain;
border-radius: var(--radius-ui);
background: var(--color-surface-muted);
animation: pop-in 0.35s var(--ease-spring) 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-ui);
padding: 0.85rem 0.9rem;
margin: 0;
}
.legal-notice__icon {
width: 18px;
height: 18px;
flex-shrink: 0;
margin-top: 0.1rem;
color: var(--color-muted-2);
}
.legal-link {
display: flex;
align-items: center;
gap: 0.5rem;
background: none;
border: none;
box-shadow: none;
padding: 0.4rem 0.1rem;
margin: 0;
font: inherit;
font-size: 0.8rem;
font-weight: 600;
color: var(--color-muted-2);
text-decoration: underline;
text-underline-offset: 2px;
cursor: pointer;
min-height: 0;
}
.legal-link .legal-notice__icon {
color: var(--color-muted-2);
}
@media (hover: hover) {
.legal-link:hover {
color: var(--color-text);
}
.legal-link:hover .legal-notice__icon {
color: var(--color-text);
}
#welcome-modal-dismiss:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-soft);
}
}
/* Mot de bienvenue + RGPD — overlay affiché une fois par événement,
au-dessus du formulaire déjà présent en arrière-plan (cf. upload.js). */
.welcome-modal {
position: fixed;
inset: 0;
z-index: 100;
display: flex;
align-items: flex-end;
justify-content: center;
padding: 0;
background: rgba(0, 0, 0, 0.6);
animation: fade-in 0.2s ease both;
}
/* Même bug que .upload-form (voir commentaire plus haut), présent depuis le
tout premier commit : sans cette règle, welcomeModal.hidden = true (JS)
ne masquait jamais visuellement la modale. */
.welcome-modal[hidden] {
display: none;
}
.welcome-modal__card {
position: relative;
width: 100%;
max-width: 480px;
max-height: 88vh;
overflow-y: auto;
background: var(--color-surface);
border-radius: var(--radius-ui) var(--radius-ui) 0 0;
padding: 1.5rem 1.25rem calc(1.5rem + env(safe-area-inset-bottom));
box-shadow: var(--shadow-soft);
animation: slide-up 0.35s var(--ease-spring) both;
display: flex;
flex-direction: column;
gap: 0.9rem;
}
/* Liseré corail en haut de la carte — même traitement de "chrome" que la
galerie admin (src/admin/gallery/style.css), touche de couleur cohérente
entre écrans sur un élément de bordure, pas de contenu. */
.welcome-modal__card::before {
content: "";
display: block;
height: 4px;
flex-shrink: 0;
margin: -1.5rem -1.25rem 1.1rem;
background: var(--color-coral);
}
@media (min-width: 520px) {
.welcome-modal {
align-items: center;
}
.welcome-modal__card {
border-radius: var(--radius-ui);
}
}
.welcome-modal .brand {
margin-bottom: 0;
}
.welcome-modal .brand__mark {
width: 30px;
height: 30px;
padding: 7px;
border-radius: 50%;
background: rgba(255, 94, 91, 0.16);
color: var(--color-coral);
}
.welcome-modal__title {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.6rem;
text-align: center;
margin: 0;
color: var(--color-text);
}
.welcome-modal__text {
text-align: center;
margin: 0;
color: var(--color-muted);
}
#welcome-modal-dismiss {
background: var(--color-coral);
color: var(--color-ink);
box-shadow: var(--shadow-sm);
}
#welcome-modal-dismiss:active {
background: #ff7673;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-up {
from { transform: translateY(24px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.progress-text {
min-height: 1.2em;
font-size: 0.9rem;
font-weight: 600;
color: var(--color-yellow);
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-spring), 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: var(--color-coral);
color: var(--color-ink);
box-shadow: var(--shadow-soft);
font-size: 1rem;
}
#submit-btn:active:not(:disabled),
#add-another-btn:active {
transform: scale(0.97);
background: #ff7673;
}
.success-actions {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.7rem;
}
.btn-secondary-link {
display: block;
text-align: center;
font-family: var(--font-ui);
font-weight: 700;
font-size: 0.9rem;
color: var(--color-muted);
text-decoration: none;
}
.btn-secondary-link:active {
opacity: 0.7;
}
#submit-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
box-shadow: none;
}
@media (hover: hover) {
#submit-btn:hover:not(:disabled),
#add-another-btn:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-soft);
}
/* Micro-nudge de l'icône avion au survol : desktop uniquement. */
#submit-btn:hover:not(:disabled) .btn-icon {
transform: translate(2px, -2px);
}
}
#submit-btn .btn-icon {
transition: transform 0.18s var(--ease-standard);
}
#retry-btn {
background: var(--color-coral);
color: var(--color-ink);
}
#retry-btn:active {
background: #ff7673;
}
/* ---------------------------------------------------------------------
É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 dans les 3 couleurs d'accent de la DA (pur CSS).
------------------------------------------------------------------ */
.success-check {
position: relative;
width: 76px;
height: 76px;
margin: 0 auto 0.9rem;
animation: pop-in 0.55s var(--ease-spring) both;
}
.success-check svg {
width: 100%;
height: 100%;
overflow: visible;
}
.success-check__circle {
stroke: var(--color-mint);
fill: rgba(6, 214, 160, 0.14);
stroke-dasharray: 145;
stroke-dashoffset: 145;
animation: draw 0.5s ease-out 0.1s forwards;
}
.success-check__mark {
stroke: var(--color-mint);
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-coral);
border-radius: 2px;
animation-delay: 0.05s;
}
.confetti span:nth-child(2) {
left: 32%;
top: 0.2rem;
background: var(--color-yellow);
border-radius: 50%;
animation-delay: 0.15s;
}
.confetti span:nth-child(3) {
left: 48%;
background: var(--color-mint);
border-radius: 2px;
animation-delay: 0.02s;
}
.confetti span:nth-child(4) {
left: 62%;
background: var(--color-coral);
border-radius: 50%;
animation-delay: 0.22s;
}
.confetti span:nth-child(5) {
left: 74%;
background: var(--color-yellow);
border-radius: 2px;
animation-delay: 0.1s;
}
.confetti span:nth-child(6) {
left: 86%;
background: var(--color-mint);
border-radius: 50%;
animation-delay: 0.28s;
}
.confetti span:nth-child(7) {
left: 10%;
top: 0.3rem;
background: var(--color-yellow);
border-radius: 50%;
animation-delay: 0.18s;
}
.confetti span:nth-child(8) {
left: 94%;
background: var(--color-coral);
border-radius: 2px;
animation-delay: 0.08s;
}
@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-weight: 700;
font-size: 1.75rem;
color: var(--color-text);
margin: 0 0 0.3rem;
}
/* Touche manuscrite ponctuelle (Caveat) sur le petit mot de succès — ton
"friendly" demandé, sans en abuser (seul élément du texte à l'utiliser
sur cet écran, comme sur les légendes de la galerie invité). */
.success-text {
font-family: var(--font-hand);
font-weight: 600;
font-size: 1.35rem;
line-height: 1.3;
color: var(--color-muted);
margin: 0 0 1.4rem;
}
@media (prefers-reduced-motion: reduce) {
.spinner,
.success-check,
.success-check__circle,
.success-check__mark,
.confetti span,
.preview,
.state-icon--limit,
button,
.btn-source,
.page__header,
.upload-form,
.welcome-modal,
.welcome-modal__card,
input[type="text"],
textarea,
.btn-icon {
animation: none !important;
transition: none !important;
}
}