feat(mobile): refonte design DSFR complète + partie admin

- écrans auth/dashboard/journal/info/urgence/profil refondus sur le design system
- AppBar institutionnel République Française sur tous les écrans
- partie admin (utilisateurs, contenus, émotions) avec AdminGuard
- entrée Espace administration dans le profil (admins uniquement)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Azmog
2026-06-19 15:22:15 +02:00
parent 2e96e07c7e
commit 7fd27d8dc2
22 changed files with 2102 additions and 910 deletions
+68 -59
View File
@@ -1,8 +1,10 @@
import { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import { Link, useRouter } from 'expo-router';
import { View, Text, StyleSheet, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme';
import { AppBar, Button, Card, TextField } from '@/components/ui';
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -28,20 +30,27 @@ export default function ForgotPasswordScreen() {
if (submitted) {
return (
<View style={styles.container}>
<View style={styles.success}>
<View style={styles.successIcon}>
<AppBar title="Demande enregistrée" />
<View style={styles.successWrap}>
<View style={styles.iconCircle}>
<Ionicons name="mail-open" size={32} color={Colors.primary} />
</View>
<Text style={styles.successTitle}>Demande enregistrée</Text>
<Text style={styles.successTitle}>Vérifiez votre boîte mail</Text>
<Text style={styles.successText}>
Si l'adresse <Text style={styles.bold}>{email}</Text> est associée à un compte CESIZen, un email contenant les instructions de réinitialisation sera envoyé.
Si l'adresse <Text style={styles.bold}>{email}</Text> est associée à un compte CESIZen,
un email contenant les instructions de réinitialisation sera envoyé.
</Text>
<Text style={styles.successText}>
Si vous ne recevez rien sous 24h, contactez votre administrateur.
</Text>
<TouchableOpacity style={styles.button} onPress={() => router.replace('/auth/login')}>
<Text style={styles.buttonText}>Retour à la connexion</Text>
</TouchableOpacity>
<View style={styles.successAction}>
<Button
title="Retour à la connexion"
icon="arrow-back-outline"
onPress={() => router.replace('/auth/login')}
fullWidth
/>
</View>
</View>
</View>
);
@@ -49,64 +58,64 @@ export default function ForgotPasswordScreen() {
return (
<KeyboardAvoidingView style={styles.container} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
<AppBar title="Mot de passe oublié" subtitle="Réinitialisez votre accès" />
<ScrollView contentContainerStyle={styles.scrollContent} keyboardShouldPersistTaps="handled">
<View style={styles.header}>
<Text style={styles.logo}>Mot de passe oublié ?</Text>
<Text style={styles.subtitle}>
Entrez votre adresse email. Si elle est associée à un compte, vous recevrez les instructions pour réinitialiser votre mot de passe.
</Text>
</View>
<Text style={styles.intro}>
Entrez votre adresse email. Si elle est associée à un compte, vous recevrez les
instructions pour réinitialiser votre mot de passe.
</Text>
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.label}>Email</Text>
<TextInput
style={styles.input}
value={email}
onChangeText={(v) => { setEmail(v); setError(null); }}
placeholder="votre@email.com"
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
<Card padding={Spacing.xl} elevated>
<TextField
label="Email"
value={email}
onChangeText={(v) => { setEmail(v); setError(null); }}
placeholder="votre@email.com"
keyboardType="email-address"
autoCapitalize="none"
autoComplete="email"
error={error ?? undefined}
required
/>
<Button
title="Envoyer les instructions"
onPress={handleSubmit}
icon="paper-plane-outline"
fullWidth
/>
<View style={styles.backRow}>
<Button
title="Retour à la connexion"
variant="tertiary"
size="sm"
onPress={() => router.replace('/auth/login')}
/>
{error && <Text style={styles.errorText}>{error}</Text>}
</View>
<TouchableOpacity style={styles.button} onPress={handleSubmit}>
<Text style={styles.buttonText}>Envoyer les instructions</Text>
</TouchableOpacity>
<View style={styles.footer}>
<Link href="/auth/login" asChild>
<TouchableOpacity>
<Text style={styles.link}>Retour à la connexion</Text>
</TouchableOpacity>
</Link>
</View>
</View>
</Card>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
scrollContent: { flexGrow: 1, justifyContent: 'center', padding: 24 },
header: { alignItems: 'center', marginBottom: 32 },
logo: { fontSize: 26, fontWeight: '800', color: Colors.black, textAlign: 'center' },
subtitle: { fontSize: 14, color: Colors.gray[500], marginTop: 10, textAlign: 'center', lineHeight: 20 },
form: { backgroundColor: Colors.gray[50], borderRadius: 4, padding: 24, borderWidth: 1, borderColor: Colors.gray[200] },
inputGroup: { marginBottom: 16 },
label: { fontSize: 14, fontWeight: '600', color: Colors.black, marginBottom: 6 },
input: { backgroundColor: Colors.gray[100], borderTopLeftRadius: 4, borderTopRightRadius: 4, borderBottomWidth: 2, borderBottomColor: Colors.black, paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, color: Colors.black },
errorText: { color: Colors.error, marginTop: 6, fontSize: 12 },
button: { backgroundColor: Colors.primary, borderRadius: 4, paddingVertical: 16, alignItems: 'center', marginTop: 8 },
buttonText: { fontSize: 16, fontWeight: '700', color: Colors.white },
footer: { alignItems: 'center', marginTop: 20 },
link: { color: Colors.primary, fontSize: 14, fontWeight: '600', textDecorationLine: 'underline' },
success: { padding: 24, alignItems: 'center', flex: 1, justifyContent: 'center' },
successIcon: { width: 72, height: 72, borderRadius: 999, backgroundColor: `${Colors.primary}15`, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
successTitle: { fontSize: 22, fontWeight: '800', color: Colors.black, marginBottom: 12 },
successText: { fontSize: 14, color: Colors.gray[600], textAlign: 'center', lineHeight: 20, marginBottom: 12 },
bold: { fontWeight: '700', color: Colors.black },
container: { flex: 1, backgroundColor: Colors.gray[50] },
scrollContent: { flexGrow: 1, justifyContent: 'center', padding: Spacing.xl },
intro: { fontSize: FontSize.sm, color: Colors.gray[600], lineHeight: 20, marginBottom: Spacing.xl, textAlign: 'center' },
backRow: { alignItems: 'center', marginTop: Spacing.sm },
successWrap: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: Spacing.xl },
iconCircle: {
width: 72,
height: 72,
borderRadius: Radius.pill,
backgroundColor: '#E3E3FD',
alignItems: 'center',
justifyContent: 'center',
marginBottom: Spacing.lg,
},
successTitle: { fontSize: FontSize.xl, fontWeight: FontWeight.heavy, color: Colors.black, marginBottom: Spacing.md, textAlign: 'center' },
successText: { fontSize: FontSize.sm, color: Colors.gray[600], textAlign: 'center', lineHeight: 20, marginBottom: Spacing.md },
bold: { fontWeight: FontWeight.bold, color: Colors.black },
successAction: { alignSelf: 'stretch', marginTop: Spacing.lg },
});
+84 -84
View File
@@ -1,10 +1,12 @@
import { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import { Link, useRouter } from 'expo-router';
import { View, Text, StyleSheet, Alert, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useAuthStore } from '@/lib/auth-store';
import { getApiError } from '@/lib/api';
import { Colors } from '@/lib/colors';
import RepubliqueHeader from '@/components/RepubliqueHeader';
import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme';
import { AppBar, Button, Card, TextField } from '@/components/ui';
export default function LoginScreen() {
const router = useRouter();
@@ -12,18 +14,22 @@ export default function LoginScreen() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleLogin = async () => {
setError(null);
if (!email || !password) {
Alert.alert('Erreur', 'Veuillez remplir tous les champs');
setError('Veuillez remplir tous les champs.');
return;
}
setLoading(true);
try {
await login(email, password);
router.replace('/(tabs)');
} catch (error) {
Alert.alert('Erreur', getApiError(error).message || 'Identifiants incorrects');
} catch (err) {
const message = getApiError(err).message || 'Identifiants incorrects';
setError(message);
Alert.alert('Erreur', message);
} finally {
setLoading(false);
}
@@ -31,62 +37,69 @@ export default function LoginScreen() {
return (
<KeyboardAvoidingView style={styles.container} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
<RepubliqueHeader />
<AppBar title="Connexion" subtitle="Accédez à votre espace personnel" />
<ScrollView contentContainerStyle={styles.scrollContent} keyboardShouldPersistTaps="handled">
<View style={styles.header}>
<Text style={styles.logo}>CESIZen</Text>
<Text style={styles.subtitle}>Votre compagnon de santé mentale</Text>
<View style={styles.intro}>
<Text style={styles.welcome}>Bienvenue sur CESIZen</Text>
<Text style={styles.welcomeSub}>Votre compagnon de santé mentale.</Text>
</View>
<View style={styles.form}>
<Text style={styles.title}>Connexion</Text>
<Card padding={Spacing.xl} elevated>
{error ? (
<View style={styles.errorBanner}>
<Ionicons name="alert-circle" size={18} color={Colors.error} />
<Text style={styles.errorBannerText}>{error}</Text>
</View>
) : null}
<View style={styles.inputGroup}>
<Text style={styles.label}>Email</Text>
<TextInput
style={styles.input}
value={email}
onChangeText={setEmail}
placeholder="votre@email.com"
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
</View>
<TextField
label="Email"
value={email}
onChangeText={(v) => { setEmail(v); setError(null); }}
placeholder="votre@email.com"
keyboardType="email-address"
autoCapitalize="none"
autoComplete="email"
required
/>
<View style={styles.inputGroup}>
<Text style={styles.label}>Mot de passe</Text>
<TextInput
style={styles.input}
value={password}
onChangeText={setPassword}
placeholder="Votre mot de passe"
secureTextEntry
/>
</View>
<TextField
label="Mot de passe"
value={password}
onChangeText={(v) => { setPassword(v); setError(null); }}
placeholder="Votre mot de passe"
secureTextEntry
autoComplete="password"
required
/>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
<Button
title="Se connecter"
onPress={handleLogin}
disabled={loading}
>
<Text style={styles.buttonText}>{loading ? 'Connexion...' : 'Se connecter'}</Text>
</TouchableOpacity>
loading={loading}
icon="log-in-outline"
fullWidth
/>
<Link href="/auth/forgot-password" asChild>
<TouchableOpacity style={styles.forgotLink}>
<Text style={styles.link}>Mot de passe oublié ?</Text>
</TouchableOpacity>
</Link>
<View style={styles.footer}>
<Text style={styles.footerText}>Pas encore de compte ? </Text>
<Link href="/auth/register" asChild>
<TouchableOpacity>
<Text style={styles.link}>S'inscrire</Text>
</TouchableOpacity>
</Link>
<View style={styles.forgotRow}>
<Button
title="Mot de passe oublié ?"
variant="tertiary"
size="sm"
onPress={() => router.push('/auth/forgot-password')}
/>
</View>
</Card>
<View style={styles.footer}>
<Text style={styles.footerText}>Pas encore de compte ?</Text>
<Button
title="Créer un compte"
variant="secondary"
onPress={() => router.push('/auth/register')}
icon="person-add-outline"
fullWidth
/>
</View>
</ScrollView>
</KeyboardAvoidingView>
@@ -94,37 +107,24 @@ export default function LoginScreen() {
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
scrollContent: { flexGrow: 1, justifyContent: 'center', padding: 24 },
header: { alignItems: 'center', marginBottom: 40 },
logo: { fontSize: 36, fontWeight: '800', color: Colors.primary },
subtitle: { fontSize: 16, color: Colors.gray[500], marginTop: 8 },
form: { backgroundColor: Colors.gray[50], borderRadius: 4, padding: 24, borderWidth: 1, borderColor: Colors.gray[200] },
title: { fontSize: 24, fontWeight: '700', color: Colors.black, marginBottom: 24 },
inputGroup: { marginBottom: 16 },
label: { fontSize: 14, fontWeight: '600', color: Colors.black, marginBottom: 6 },
input: {
backgroundColor: Colors.gray[100],
borderTopLeftRadius: 4,
borderTopRightRadius: 4,
borderBottomWidth: 2,
borderBottomColor: Colors.black,
paddingHorizontal: 16,
paddingVertical: 14,
fontSize: 16,
color: Colors.black,
},
button: {
backgroundColor: Colors.primary,
borderRadius: 4,
paddingVertical: 16,
container: { flex: 1, backgroundColor: Colors.gray[50] },
scrollContent: { flexGrow: 1, justifyContent: 'center', padding: Spacing.xl },
intro: { alignItems: 'center', marginBottom: Spacing.xl },
welcome: { fontSize: FontSize.xxl, fontWeight: FontWeight.heavy, color: Colors.black, textAlign: 'center' },
welcomeSub: { fontSize: FontSize.sm, color: Colors.gray[500], marginTop: Spacing.xs, textAlign: 'center' },
errorBanner: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 8,
gap: Spacing.sm,
backgroundColor: Colors.errorBg,
borderRadius: Radius.sm,
borderLeftWidth: 3,
borderLeftColor: Colors.error,
padding: Spacing.md,
marginBottom: Spacing.lg,
},
buttonDisabled: { opacity: 0.6 },
buttonText: { fontSize: 16, fontWeight: '700', color: Colors.white },
forgotLink: { alignItems: 'center', marginTop: 12 },
footer: { flexDirection: 'row', justifyContent: 'center', marginTop: 16 },
footerText: { color: Colors.gray[500], fontSize: 14 },
link: { color: Colors.primary, fontSize: 14, fontWeight: '600', textDecorationLine: 'underline' },
errorBannerText: { flex: 1, fontSize: FontSize.xs, color: Colors.error, fontWeight: FontWeight.medium },
forgotRow: { alignItems: 'center', marginTop: Spacing.sm },
footer: { marginTop: Spacing.xl, alignItems: 'center' },
footerText: { fontSize: FontSize.sm, color: Colors.gray[500], marginBottom: Spacing.sm },
});
+117 -78
View File
@@ -1,11 +1,12 @@
import { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import { Link, useRouter } from 'expo-router';
import { View, Text, StyleSheet, Alert, KeyboardAvoidingView, Platform, ScrollView, Pressable } from 'react-native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useAuthStore } from '@/lib/auth-store';
import { getApiError } from '@/lib/api';
import { Colors } from '@/lib/colors';
import RepubliqueHeader from '@/components/RepubliqueHeader';
import { FontSize, FontWeight, MIN_TOUCH, Radius, Spacing } from '@/lib/theme';
import { AppBar, Button, Card, TextField } from '@/components/ui';
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -60,70 +61,112 @@ export default function RegisterScreen() {
}
};
const consentError = errors.consentement || errors.consentement_rgpd;
return (
<KeyboardAvoidingView style={styles.container} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
<RepubliqueHeader />
<AppBar title="Inscription" subtitle="Créez votre compte CESIZen" />
<ScrollView contentContainerStyle={styles.scrollContent} keyboardShouldPersistTaps="handled">
<View style={styles.header}>
<Text style={styles.logo}>CESIZen</Text>
<Text style={styles.subtitle}>Créez votre compte</Text>
</View>
<View style={styles.form}>
<Text style={styles.title}>Inscription</Text>
<View style={styles.row}>
<View style={[styles.inputGroup, { flex: 1, marginRight: 8 }]}>
<Text style={styles.label}>Nom</Text>
<TextInput style={styles.input} value={nom} onChangeText={setNom} placeholder="Dupont" />
{errors.nom && <Text style={styles.errorText}>{errors.nom}</Text>}
</View>
<View style={[styles.inputGroup, { flex: 1, marginLeft: 8 }]}>
<Text style={styles.label}>Prénom</Text>
<TextInput style={styles.input} value={prenom} onChangeText={setPrenom} placeholder="Jean" />
{errors.prenom && <Text style={styles.errorText}>{errors.prenom}</Text>}
</View>
<Card padding={Spacing.xl} elevated>
<View style={styles.nameRow}>
<TextField
label="Nom"
value={nom}
onChangeText={setNom}
placeholder="Dupont"
autoCapitalize="words"
error={errors.nom}
required
style={styles.nameField}
/>
<TextField
label="Prénom"
value={prenom}
onChangeText={setPrenom}
placeholder="Jean"
autoCapitalize="words"
error={errors.prenom}
required
style={styles.nameField}
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Email</Text>
<TextInput style={styles.input} value={email} onChangeText={setEmail} placeholder="votre@email.com" keyboardType="email-address" autoCapitalize="none" autoCorrect={false} />
{errors.email && <Text style={styles.errorText}>{errors.email}</Text>}
</View>
<TextField
label="Email"
value={email}
onChangeText={setEmail}
placeholder="votre@email.com"
keyboardType="email-address"
autoCapitalize="none"
autoComplete="email"
error={errors.email}
required
/>
<View style={styles.inputGroup}>
<Text style={styles.label}>Mot de passe (min. 8 caractères)</Text>
<TextInput style={styles.input} value={password} onChangeText={setPassword} placeholder="Mot de passe" secureTextEntry />
{errors.password && <Text style={styles.errorText}>{errors.password}</Text>}
</View>
<TextField
label="Mot de passe"
value={password}
onChangeText={setPassword}
placeholder="Mot de passe"
secureTextEntry
error={errors.password}
helper="Au moins 8 caractères"
required
/>
<View style={styles.inputGroup}>
<Text style={styles.label}>Confirmer le mot de passe</Text>
<TextInput style={styles.input} value={passwordConfirmation} onChangeText={setPasswordConfirmation} placeholder="Confirmer" secureTextEntry />
{errors.passwordConfirmation && <Text style={styles.errorText}>{errors.passwordConfirmation}</Text>}
</View>
<TextField
label="Confirmer le mot de passe"
value={passwordConfirmation}
onChangeText={setPasswordConfirmation}
placeholder="Confirmer"
secureTextEntry
error={errors.passwordConfirmation}
required
/>
<TouchableOpacity style={styles.consentRow} onPress={() => setConsentement(!consentement)} activeOpacity={0.7}>
<View style={[styles.checkbox, consentement && styles.checkboxChecked]}>
{consentement && <Ionicons name="checkmark" size={16} color={Colors.white} />}
</View>
<Pressable
style={styles.consentRow}
onPress={() => setConsentement(!consentement)}
accessibilityRole="checkbox"
accessibilityState={{ checked: consentement }}
>
<Ionicons
name={consentement ? 'checkbox' : 'square-outline'}
size={24}
color={consentement ? Colors.primary : Colors.gray[500]}
style={styles.consentIcon}
/>
<Text style={styles.consentText}>
J'accepte le traitement de mes données de bien-être conformément à la <Text style={styles.consentLink}>politique de confidentialité</Text> (RGPD).
J'accepte le traitement de mes données de bien-être conformément à la{' '}
<Text style={styles.consentLink}>politique de confidentialité</Text> (RGPD).
</Text>
</TouchableOpacity>
{errors.consentement && <Text style={styles.errorText}>{errors.consentement}</Text>}
{errors.consentement_rgpd && <Text style={styles.errorText}>{errors.consentement_rgpd}</Text>}
</Pressable>
{consentError ? (
<View style={styles.consentErrorRow}>
<Ionicons name="alert-circle" size={14} color={Colors.error} />
<Text style={styles.consentErrorText}>{consentError}</Text>
</View>
) : null}
<TouchableOpacity style={[styles.button, loading && styles.buttonDisabled]} onPress={handleRegister} disabled={loading}>
<Text style={styles.buttonText}>{loading ? 'Inscription...' : 'S\'inscrire'}</Text>
</TouchableOpacity>
<View style={styles.footer}>
<Text style={styles.footerText}>Déjà un compte ? </Text>
<Link href="/auth/login" asChild>
<TouchableOpacity><Text style={styles.link}>Se connecter</Text></TouchableOpacity>
</Link>
<View style={styles.submit}>
<Button
title="Créer mon compte"
onPress={handleRegister}
loading={loading}
icon="person-add-outline"
fullWidth
/>
</View>
</Card>
<View style={styles.footer}>
<Text style={styles.footerText}>Déjà un compte ?</Text>
<Button
title="Se connecter"
variant="tertiary"
size="sm"
onPress={() => router.push('/auth/login')}
/>
</View>
</ScrollView>
</KeyboardAvoidingView>
@@ -131,27 +174,23 @@ export default function RegisterScreen() {
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
scrollContent: { flexGrow: 1, justifyContent: 'center', padding: 24 },
header: { alignItems: 'center', marginBottom: 32 },
logo: { fontSize: 36, fontWeight: '800', color: Colors.primary },
subtitle: { fontSize: 16, color: Colors.gray[500], marginTop: 8 },
form: { backgroundColor: Colors.gray[50], borderRadius: 4, padding: 24, borderWidth: 1, borderColor: Colors.gray[200] },
title: { fontSize: 24, fontWeight: '700', color: Colors.black, marginBottom: 24 },
row: { flexDirection: 'row' },
inputGroup: { marginBottom: 16 },
label: { fontSize: 14, fontWeight: '600', color: Colors.black, marginBottom: 6 },
input: { backgroundColor: Colors.gray[100], borderTopLeftRadius: 4, borderTopRightRadius: 4, borderBottomWidth: 2, borderBottomColor: Colors.black, paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, color: Colors.black },
errorText: { color: Colors.error, marginTop: 4, fontSize: 12 },
consentRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 10, marginVertical: 12 },
checkbox: { width: 22, height: 22, borderRadius: 4, borderWidth: 2, borderColor: Colors.black, backgroundColor: Colors.white, justifyContent: 'center', alignItems: 'center', marginTop: 1 },
checkboxChecked: { backgroundColor: Colors.primary, borderColor: Colors.primary },
consentText: { flex: 1, fontSize: 13, color: Colors.black, lineHeight: 18 },
consentLink: { color: Colors.primary, fontWeight: '600', textDecorationLine: 'underline' },
button: { backgroundColor: Colors.primary, borderRadius: 4, paddingVertical: 16, alignItems: 'center', marginTop: 12 },
buttonDisabled: { opacity: 0.6 },
buttonText: { fontSize: 16, fontWeight: '700', color: Colors.white },
footer: { flexDirection: 'row', justifyContent: 'center', marginTop: 20 },
footerText: { color: Colors.gray[500], fontSize: 14 },
link: { color: Colors.primary, fontSize: 14, fontWeight: '600', textDecorationLine: 'underline' },
container: { flex: 1, backgroundColor: Colors.gray[50] },
scrollContent: { flexGrow: 1, justifyContent: 'center', padding: Spacing.xl },
nameRow: { flexDirection: 'row', flexWrap: 'wrap', gap: Spacing.md },
nameField: { flexGrow: 1, flexBasis: 130 },
consentRow: {
flexDirection: 'row',
alignItems: 'flex-start',
gap: Spacing.sm,
paddingVertical: Spacing.sm,
minHeight: MIN_TOUCH,
},
consentIcon: { marginTop: 1 },
consentText: { flex: 1, fontSize: FontSize.xs, color: Colors.black, lineHeight: 19 },
consentLink: { color: Colors.primary, fontWeight: FontWeight.semibold, textDecorationLine: 'underline' },
consentErrorRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.xs, marginTop: Spacing.xs },
consentErrorText: { fontSize: FontSize.xs, color: Colors.error },
submit: { marginTop: Spacing.lg },
footer: { marginTop: Spacing.xl, alignItems: 'center' },
footerText: { fontSize: FontSize.sm, color: Colors.gray[500] },
});