Étend la DA "Polaroid Néon" aux 4 derniers écrans

Upload invité (priorité haute, full traitement : fond #12100E, CTA
corail, General Sans/Inter, touche Caveat sur le message de succès),
galerie admin (exception admin du skill : sobre, sombre, cartes #1E1B18,
pas de rotation/confettis, corail réservé aux CTA), générateur QR (même
exception admin, traitement minimal), carton imprimable (reste clair —
un support papier n'a pas vocation à avoir un fond noir — mais adopte les
polices et l'accent corail de la nouvelle DA, contraste recalculé pour
l'impression).

Double thème clair/sombre de l'ancienne DA retiré au profit d'un thème
unique sombre, cohérent avec src/galerie/ (premier écran de cette DA,
déjà validé). Dégradés de texte et halo de fond de l'ancienne DA retirés
(skill : jamais de glow/neon artificiel).

Contrastes vérifiés par calcul WCAG AA pour toutes les nouvelles
combinaisons. Chemins d'assets absolus et pattern [hidden]/display
vérifiés sur les 4 écrans (bugs déjà rencontrés deux fois en prod).
This commit is contained in:
2026-09-18 09:21:40 +02:00
parent f4322850d6
commit ea3e49cc04
6 changed files with 432 additions and 627 deletions
+201 -356
View File
@@ -1,200 +1,101 @@
/* 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).
/* 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 !").
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).
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.
Passe 3, 2026-09-16 — retour Kévin : "top ! rajoute des animations, de la
couleur [...] brainstorm sur le choix des couleurs et de la typo, ajoute
un mode sombre [...] fait moi quelque chose de vendeur et de friendly
(type iPhone)." Palette/typo déjà validées passe 2 : le brainstorm de
cette passe (voir rapport de passe pour le détail des 2 directions
alternatives écartées) a confirmé la famille corail/prune/doré + Fraunces
plutôt que d'ajouter de nouvelles teintes ou une police supplémentaire —
"plus de couleur" est obtenu en réutilisant plus largement cette même
palette (halo radial en fond, focus ring teinté, confettis étoffés)
plutôt qu'en important une 4e couleur qui romprait l'identité perso de
Kévin. Traduction concrète de cette passe :
- Mode sombre réel via prefers-color-scheme (pas d'inversion brute) :
fond brun quasi-noir (jamais bleu-nuit, pour rester dans la famille
chaude de la marque), texte/accents reclaircis et vérifiés au ratio de
contraste (WCAG AA, cf. rapport de passe pour le détail des calculs).
- Niveau de finition "type iPhone" au sens exécution, pas esthétique iOS
: pas de glassmorphism/blur ni de police système Apple. Concrètement :
easing dédié aux transitions d'état (--ease-standard, plus posé que le
--ease-bounce déjà en place et réservé aux pops/rebonds), halo de
couleur subtil en arrière-plan de page, focus ring teinté sur les
champs, micro-interactions de hover en plus du :active existant
(uniquement sous @media (hover: hover), donc jamais sur mobile tactile
— l'invité ne voit que les états focus/active déjà en place), entrées
en fondu/translation des sections principales au chargement. */
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: "Fraunces";
src: url("/vendor/fonts/fraunces-latin-600-normal.woff2") format("woff2");
font-weight: 600;
font-family: "General Sans";
src: url("/vendor/fonts/generalsans-700.woff2") format("woff2");
font-weight: 700;
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-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;
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 {
/* "light dark" (et non "light" seul) : indique au navigateur que les deux
thèmes sont pris en charge, pour que les contrôles natifs (scrollbar,
éventuels <select>) s'adaptent aussi, pas seulement nos propres
couleurs. Les valeurs ci-dessous sont le thème clair ; le thème sombre
est redéfini intégralement dans le bloc @media (prefers-color-scheme:
dark) plus bas — jamais un simple filtre d'inversion. */
color-scheme: light dark;
color-scheme: dark;
/* 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;
/* 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-accent: #e35f2b;
--color-accent-hover: #c74c1e;
--color-accent-light: #ff8a52;
--color-accent-soft: #ffe3d0;
--color-accent-contrast: #fffaf6;
--color-coral: #ff5e5b;
--color-yellow: #ffd166;
--color-mint: #06d6a0;
--color-plum: #7c2f56;
--color-plum-soft: #f8e3ee;
--color-border: #e8dfd3;
--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;
/* Bouton "danger" (Réessayer) : variable dédiée plutôt que de réutiliser
--color-error-text tel quel comme fond de bouton — nécessaire en thème
sombre où le texte d'erreur s'éclaircit (pour rester lisible sur fond
sombre) et ne fonctionne plus comme couleur de fond de bouton avec du
texte blanc (contraste vérifié, voir rapport de passe). */
--color-danger: #a3341c;
--color-danger-contrast: #fff;
--radius-sm: 12px;
--radius: 20px;
--radius: 4px; /* réservé aux éléments façon polaroid, non utilisé ici */
--radius-ui: 14px;
--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);
--shadow-soft: 0 10px 24px rgba(0, 0, 0, 0.35);
--shadow-card: 0 6px 16px rgba(0, 0, 0, 0.4);
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
/* Easing "posé" pour les transitions d'état/hover (fondus, lifts) — le
rebond de --ease-bounce reste réservé aux moments ludiques (pop-in,
coche de succès) pour ne pas le diluer. */
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-standard: cubic-bezier(0.22, 1, 0.36, 1);
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
--font-display: "Fraunces", Georgia, "Times New Roman", serif;
}
--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;
/* -----------------------------------------------------------------------
Thème sombre — palette pensée pour l'obscurité (pas une inversion des
couleurs claires) : fond brun quasi-noir pour rester dans la famille
chaude de la marque plutôt qu'un noir bleuté générique, texte et accents
reclaircis et vérifiés au ratio de contraste WCAG AA (>= 4.5:1 pour le
texte courant, >= 3:1 pour le grand texte/les éléments d'UI ; calculs
détaillés dans le rapport de passe). Répond aux deux demandes de Kévin à
la fois : un vrai mode sombre ET un rendu "moins agressif" en usage réel
(salle tamisée en soirée). ----------------------------------------- */
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #1b120b;
--color-surface: #26190f;
--color-surface-muted: #32211a;
--color-text: #f7ead9;
--color-muted: #b39d84;
--color-border: #40291a;
/* 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-accent: #ff7a42;
--color-accent-hover: #ffa06e;
--color-accent-light: #ffab7a;
--color-accent-soft: rgba(255, 122, 66, 0.18);
--color-accent-contrast: #241206;
--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);
--color-plum: #f0b8d8;
--color-plum-soft: rgba(240, 184, 216, 0.14);
--color-gold: #ffcf6b;
--color-gold-soft: #3a2a12;
--color-error-bg: #3a1712;
--color-error-text: #ff9b86;
--color-success-bg: #2e2110;
--color-success-text: #f2d9b8;
--color-limit-bg: #3a2a12;
--color-limit-text: #ffd98a;
--color-danger: #ff6b52;
--color-danger-contrast: #2b0f08;
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.45);
--shadow-md: 0 14px 36px rgba(0, 0, 0, 0.55);
}
/* Fonds d'icônes état erreur/limite : calculés à partir des couleurs
claires d'origine, non pilotables via une seule variable — surchargés
ici pour rester cohérents en sombre plutôt que de laisser un rouge/or
"clair" plaqué sur fond sombre. */
.state-block--error .state-icon--error {
background: rgba(255, 155, 134, 0.16);
}
.state-block--limit .state-icon--limit {
background: rgba(255, 217, 138, 0.18);
}
.welcome-modal {
background: rgba(0, 0, 0, 0.6);
}
body::before {
opacity: 0.4;
}
--shadow-sm: 0 2px 10px rgba(0, 0, 0, 0.3);
}
* {
@@ -206,32 +107,13 @@ html {
}
body {
position: relative;
margin: 0;
font-family: var(--font-sans);
font-family: var(--font-ui);
background: var(--color-bg);
color: var(--color-text);
line-height: 1.45;
}
/* Halo de couleur en arrière-plan — "plus de couleur" sans ajouter de
teinte à la palette : deux dégradés radiaux réutilisant les couleurs
d'accent déjà en place (corail + doré), très doux, purement décoratifs.
Objectif "type iPhone" : une page peut être vivante sans être plate,
sans pour autant recourir à un vrai fond image (poids nul, CSS pur).
z-index négatif + fixed : reste derrière le contenu, ne capte aucun
événement (pointer-events: none), ne bouge pas au scroll. */
body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background: radial-gradient(60% 38% at 50% -8%, var(--color-accent-soft) 0%, transparent 72%),
radial-gradient(46% 30% at 100% 100%, var(--color-gold-soft) 0%, transparent 70%);
opacity: 0.7;
}
.page {
max-width: 480px;
margin: 0 auto;
@@ -242,10 +124,6 @@ body::before {
.page__header {
text-align: center;
margin-bottom: 1.6rem;
/* Entrée en fondu/translation au chargement — discrète, une seule fois
par affichage (rejoue si la page est masquée/réaffichée via l'attribut
hidden, ce qui reste cohérent : chaque "nouvel écran" mérite sa petite
entrée). */
animation: fade-rise 0.5s var(--ease-standard) both;
}
@@ -260,24 +138,20 @@ body::before {
}
}
/* 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);
color: var(--color-muted-2);
}
.brand__mark {
width: 16px;
height: 16px;
flex-shrink: 0;
color: var(--color-accent);
color: var(--color-coral);
}
.brand__name {
@@ -285,39 +159,28 @@ body::before {
font-weight: 700;
letter-spacing: 0.13em;
text-transform: uppercase;
font-family: var(--font-ui);
}
.page__header h1 {
font-family: var(--font-display);
font-size: 2rem;
font-weight: 600;
font-weight: 700;
font-size: 1.9rem;
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;
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);
border-radius: var(--radius-ui);
margin-bottom: 1rem;
}
@@ -327,18 +190,17 @@ body::before {
.state-block--error {
background: var(--color-error-bg);
color: var(--color-error-text);
color: var(--color-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);
background: linear-gradient(180deg, var(--color-success-bg), transparent 75%);
color: var(--color-text);
overflow: visible;
padding-top: 1.75rem;
}
@@ -351,16 +213,14 @@ body::before {
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 de chargement — anneau CSS pur, aucune dépendance. */
.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: 3px solid rgba(255, 94, 91, 0.22);
border-top-color: var(--color-coral);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@@ -373,9 +233,8 @@ body::before {
/* 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-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;
@@ -388,8 +247,8 @@ body::before {
.state-block--error .state-icon--error {
display: flex;
background: rgba(163, 52, 28, 0.12);
color: var(--color-error-text);
background: rgba(255, 94, 91, 0.16);
color: var(--color-coral);
}
.state-block--error.state-block--limit .state-icon--error {
@@ -398,9 +257,9 @@ body::before {
.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;
background: rgba(255, 209, 102, 0.18);
color: var(--color-yellow);
animation: pop-in 0.5s var(--ease-spring) both;
}
.state-icon svg {
@@ -415,10 +274,10 @@ body::before {
animation: fade-rise 0.5s var(--ease-standard) 0.08s both;
}
/* Même bug que .welcome-modal (voir commentaire plus haut) : présent depuis
le tout premier commit de ce fichier. form.hidden = true (JS) ne masquait
jamais réellement le formulaire, qui restait affiché en display:flex en
permanence, superposé aux états chargement/erreur/succès. */
/* 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;
}
@@ -436,20 +295,19 @@ body::before {
.field .optional {
font-weight: 400;
color: var(--color-muted);
color: var(--color-muted-2);
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). */
/* 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);
border-radius: var(--radius);
border: 1px solid var(--color-border-ui);
border-radius: var(--radius-ui);
padding: 1.1rem;
}
@@ -457,21 +315,18 @@ 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);
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);
}
/* Anneau de focus teinté (en plus du changement de bordure) : détail de
finition "type iPhone" — un champ actif doit se voir sans ambiguïté,
avec une transition douce plutôt qu'un changement sec. */
input[type="text"]:focus,
textarea:focus {
border-color: var(--color-accent);
box-shadow: 0 0 0 4px var(--color-accent-soft);
border-color: var(--color-coral);
box-shadow: 0 0 0 4px rgba(255, 94, 91, 0.22);
}
.visually-hidden {
@@ -491,12 +346,10 @@ textarea:focus {
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). */
/* 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;
@@ -510,20 +363,20 @@ textarea:focus {
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;
transition: transform 0.12s var(--ease-spring), 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);
background: var(--color-coral);
color: var(--color-ink);
box-shadow: var(--shadow-sm);
}
.btn-source--secondary {
border: 1.5px solid var(--color-plum);
background: var(--color-surface);
color: var(--color-plum);
border: 1.5px solid var(--color-border-ui-strong);
background: transparent;
color: var(--color-text);
}
.btn-source:active {
@@ -531,34 +384,30 @@ textarea:focus {
}
.btn-source--primary:active {
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-hover));
background: #ff7673;
}
.btn-source--secondary:active {
background: var(--color-plum-soft);
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, où seuls :active/:focus-visible s'appliquent déjà.
Lift discret + easing posé (pas de rebond ici, réservé aux pops). */
sur mobile tactile. */
@media (hover: hover) {
.btn-source--primary:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-md);
box-shadow: var(--shadow-soft);
}
.btn-source--secondary:hover {
transform: translateY(-1px);
background: var(--color-plum-soft);
background: var(--color-surface);
border-color: var(--color-coral);
}
}
/* 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. */
/* Icônes SVG inline (aucune requête réseau), héritent de currentColor. */
.btn-icon {
width: 20px;
height: 20px;
@@ -567,7 +416,7 @@ textarea:focus {
.file-chosen-label {
font-size: 0.85rem;
color: var(--color-muted);
color: var(--color-muted-2);
margin: 0.5rem 0 0;
text-align: center;
}
@@ -580,7 +429,7 @@ textarea {
input:focus-visible,
textarea:focus-visible,
button:focus-visible {
outline: 3px solid var(--color-plum);
outline: 3px solid var(--color-coral);
outline-offset: 2px;
}
@@ -588,9 +437,9 @@ button:focus-visible {
width: 100%;
max-height: 320px;
object-fit: contain;
border-radius: var(--radius);
border-radius: var(--radius-ui);
background: var(--color-surface-muted);
animation: pop-in 0.35s var(--ease-bounce) both;
animation: pop-in 0.35s var(--ease-spring) both;
}
.legal-notice {
@@ -600,7 +449,7 @@ button:focus-visible {
font-size: 0.78rem;
color: var(--color-muted);
background: var(--color-surface-muted);
border-radius: var(--radius);
border-radius: var(--radius-ui);
padding: 0.85rem 0.9rem;
margin: 0;
}
@@ -610,7 +459,7 @@ button:focus-visible {
height: 18px;
flex-shrink: 0;
margin-top: 0.1rem;
color: var(--color-plum);
color: var(--color-muted-2);
}
.legal-link {
@@ -625,7 +474,7 @@ button:focus-visible {
font: inherit;
font-size: 0.8rem;
font-weight: 600;
color: var(--color-muted);
color: var(--color-muted-2);
text-decoration: underline;
text-underline-offset: 2px;
cursor: pointer;
@@ -633,21 +482,21 @@ button:focus-visible {
}
.legal-link .legal-notice__icon {
color: var(--color-muted);
color: var(--color-muted-2);
}
@media (hover: hover) {
.legal-link:hover {
color: var(--color-plum);
color: var(--color-text);
}
.legal-link:hover .legal-notice__icon {
color: var(--color-plum);
color: var(--color-text);
}
#welcome-modal-dismiss:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-md);
box-shadow: var(--shadow-soft);
}
}
@@ -661,17 +510,13 @@ button:focus-visible {
align-items: flex-end;
justify-content: center;
padding: 0;
background: rgba(43, 32, 22, 0.45);
background: rgba(0, 0, 0, 0.6);
animation: fade-in 0.2s ease both;
}
/* Bug réel corrigé (2026-09-16) : une déclaration `display` en CSS auteur
passe toujours devant la règle `[hidden] { display: none }` du navigateur
(l'origine "auteur" l'emporte sur l'origine "user-agent" dans la
cascade, indépendamment de la spécificité). Sans cette règle explicite,
`welcomeModal.hidden = true` en JS ne masquait jamais visuellement la
modale : le bouton "C'est parti" fonctionnait bien (l'état se mettait à
jour) mais rien ne changeait à l'écran. */
/* 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;
}
@@ -683,26 +528,25 @@ button:focus-visible {
max-height: 88vh;
overflow-y: auto;
background: var(--color-surface);
border-radius: var(--radius) var(--radius) 0 0;
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-md);
animation: slide-up 0.35s var(--ease-bounce) both;
box-shadow: var(--shadow-soft);
animation: slide-up 0.35s var(--ease-spring) both;
display: flex;
flex-direction: column;
gap: 0.9rem;
}
/* Liseré dégradé en haut de la carte — même traitement que les cartes de la
galerie admin (src/admin/gallery/style.css), pour une petite touche de
couleur cohérente entre les deux écrans sur un élément de chrome (pas de
contenu), plutôt qu'un ajout ponctuel isolé. */
/* 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: linear-gradient(90deg, var(--color-accent), var(--color-gold));
background: var(--color-coral);
}
@media (min-width: 520px) {
@@ -710,7 +554,7 @@ button:focus-visible {
align-items: center;
}
.welcome-modal__card {
border-radius: var(--radius);
border-radius: var(--radius-ui);
}
}
@@ -718,46 +562,38 @@ button:focus-visible {
margin-bottom: 0;
}
/* Peaufinage passe 3 (la modale était fonctionnelle mais minimale) : la
marque prend la forme d'un badge rond coloré plutôt qu'une petite icône
nue — cohérent avec le ton "app premium" recherché, sans nouvel élément
HTML (juste du style appliqué au SVG existant). */
.welcome-modal .brand__mark {
width: 30px;
height: 30px;
padding: 7px;
border-radius: 50%;
background: var(--color-accent-soft);
color: var(--color-accent);
background: rgba(255, 94, 91, 0.16);
color: var(--color-coral);
}
.welcome-modal__title {
font-family: var(--font-display);
font-weight: 600;
font-weight: 700;
font-size: 1.6rem;
text-align: center;
margin: 0;
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;
}
.welcome-modal__text {
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: linear-gradient(135deg, var(--color-accent-light), var(--color-accent));
color: var(--color-accent-contrast);
background: var(--color-coral);
color: var(--color-ink);
box-shadow: var(--shadow-sm);
}
#welcome-modal-dismiss:active {
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-hover));
background: #ff7673;
}
@keyframes fade-in {
@@ -774,7 +610,7 @@ button:focus-visible {
min-height: 1.2em;
font-size: 0.9rem;
font-weight: 600;
color: var(--color-plum);
color: var(--color-yellow);
margin: 0;
text-align: center;
}
@@ -787,7 +623,7 @@ button {
padding: 0.9rem 1rem;
min-height: 50px;
cursor: pointer;
transition: transform 0.12s var(--ease-bounce), background 0.12s ease, opacity 0.12s ease;
transition: transform 0.12s var(--ease-spring), background 0.12s ease, opacity 0.12s ease;
}
#submit-btn,
@@ -796,20 +632,20 @@ button {
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);
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: linear-gradient(135deg, var(--color-accent), var(--color-accent-hover));
background: #ff7673;
}
#submit-btn:disabled {
opacity: 0.6;
opacity: 0.5;
cursor: not-allowed;
box-shadow: none;
}
@@ -818,12 +654,10 @@ button {
#submit-btn:hover:not(:disabled),
#add-another-btn:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-md);
box-shadow: var(--shadow-soft);
}
/* Micro-nudge de l'icône avion au survol : détail qui n'existe que pour
les visiteurs desktop (donc surtout les prospects en démo), sans
dépendance JS supplémentaire. */
/* Micro-nudge de l'icône avion au survol : desktop uniquement. */
#submit-btn:hover:not(:disabled) .btn-icon {
transform: translate(2px, -2px);
}
@@ -834,14 +668,18 @@ button {
}
#retry-btn {
background: var(--color-danger);
color: var(--color-danger-contrast);
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 colorés (pur CSS, aucune dépendance JS supplémentaire).
confettis dans les 3 couleurs d'accent de la DA (pur CSS).
------------------------------------------------------------------ */
.success-check {
@@ -849,7 +687,7 @@ button {
width: 76px;
height: 76px;
margin: 0 auto 0.9rem;
animation: pop-in 0.55s var(--ease-bounce) both;
animation: pop-in 0.55s var(--ease-spring) both;
}
.success-check svg {
@@ -859,15 +697,15 @@ button {
}
.success-check__circle {
stroke: var(--color-accent);
fill: var(--color-gold-soft);
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-accent);
stroke: var(--color-mint);
fill: none;
stroke-dasharray: 32;
stroke-dashoffset: 32;
@@ -912,51 +750,51 @@ button {
.confetti span:nth-child(1) {
left: 18%;
background: var(--color-accent);
background: var(--color-coral);
border-radius: 2px;
animation-delay: 0.05s;
}
.confetti span:nth-child(2) {
left: 32%;
top: 0.2rem;
background: var(--color-gold);
background: var(--color-yellow);
border-radius: 50%;
animation-delay: 0.15s;
}
.confetti span:nth-child(3) {
left: 48%;
background: var(--color-plum);
background: var(--color-mint);
border-radius: 2px;
animation-delay: 0.02s;
}
.confetti span:nth-child(4) {
left: 62%;
background: var(--color-accent-light);
background: var(--color-coral);
border-radius: 50%;
animation-delay: 0.22s;
}
.confetti span:nth-child(5) {
left: 74%;
background: var(--color-gold);
background: var(--color-yellow);
border-radius: 2px;
animation-delay: 0.1s;
}
.confetti span:nth-child(6) {
left: 86%;
background: var(--color-plum);
background: var(--color-mint);
border-radius: 50%;
animation-delay: 0.28s;
}
.confetti span:nth-child(7) {
left: 10%;
top: 0.3rem;
background: var(--color-gold);
background: var(--color-yellow);
border-radius: 50%;
animation-delay: 0.18s;
}
.confetti span:nth-child(8) {
left: 94%;
background: var(--color-accent);
background: var(--color-coral);
border-radius: 2px;
animation-delay: 0.08s;
}
@@ -977,15 +815,22 @@ button {
.success-title {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.75rem;
font-weight: 600;
color: var(--color-accent-hover);
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-size: 0.95rem;
margin: 0 0 1.3rem;
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) {