feat: student mode support for thanks page (admin-auth only)

- add hidden student_mode field in add.php form
- pass mode=student through redirect to thanks.php in formulaire.php
- thanks.php renders clean student thank-you page (no header, centered button)
- add CSS for .thanks-student-page, .btn-new-form, .thanks-success, .thanks-error
- admin auth always required; student mode is purely UI variant on the physical machine
This commit is contained in:
Pontoporeia
2026-04-15 13:49:25 +02:00
parent c3affd2285
commit f4aba500e6
7 changed files with 254 additions and 81 deletions

22
TODO.md
View File

@@ -1,16 +1,10 @@
# TODO # TODO
## Paramètres page cleanup - [x] Make thanks.php respect student mode (no header, centered "add new form" button)
- [x] Remove card syntax (`.admin-settings-section` border/radius containers) - [x] Add hidden input `student_mode` in add.php form when in student mode
- [x] Replace pill toggles with native semantic checkboxes inside `<fieldset>` - [x] Append `mode=student` to thanks redirect in formulaire.php
- [x] Move "delete all TFE" danger zone into Maintenance section - [x] Update thanks.php to detect student mode, hide header, show centered button
- [x] Use `<fieldset>` for danger zones (semantic, with `<legend>` instead of `<div>`) - [x] Cleanup public/admin/add.php — standardise fieldsets and add licence explanation sections from docs PDF
- [x] Update CSS: new `.param-*` classes for flat semantic layout - [x] Organise all fields into `<fieldset>/<legend>` blocks: Informations du TFE, Composition du jury, Cadre académique, Fichiers, Métadonnées complémentaires
- [x] Exclude parametres sections from generic `.admin-body main > section` card styling via `aria-labelledby` prefix - [x] Remove double-wrapping of jury-fieldset (it has its own `<fieldset>`)
- [x] Add "Degrés d'ouverture et licences" section (Libre / Interne / Interdit + Généralités) wrapped in `if ($studentMode)` — hidden in admin
## Add TFE: admin/student mode toggle
- [x] Add `?mode=student` query param to same add.php page
- [x] Student mode: no admin header/nav, just the form with a back-link
- [x] Admin mode: full admin header/nav, with "Mode étudiant ↗" toggle link (opens in new tab)
- [x] Auth guard stays the same — still requires admin login
- [x] Add `.student-body`, `.thesis-add-header`, `.mode-toggle`, `.form-footer` CSS

Binary file not shown.

View File

@@ -9,6 +9,8 @@ ini_set('error_log', 'error.log');
AdminAuth::requireLogin(); AdminAuth::requireLogin();
$studentMode = isset($_POST['student_mode']) && $_POST['student_mode'] === '1';
// Verify CSRF token // Verify CSRF token
if (!isset($_POST['csrf_token'], $_SESSION['csrf_token']) if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
@@ -26,7 +28,11 @@ try {
unset($_SESSION['csrf_token']); unset($_SESSION['csrf_token']);
header('Location: ../thanks.php?id=' . urlencode($thesisId)); $redirect = '../thanks.php?id=' . urlencode($thesisId);
if ($studentMode) {
$redirect .= '&mode=student';
}
header('Location: ' . $redirect);
exit(); exit();
} catch (Exception $e) { } catch (Exception $e) {
@@ -35,11 +41,16 @@ try {
App::flash('error', $e->getMessage()); App::flash('error', $e->getMessage());
$_SESSION['form_data'] = $_POST; $_SESSION['form_data'] = $_POST;
$redirect = '../add.php';
if ($studentMode) {
$redirect .= '?mode=student';
}
$autofocusField = ThesisCreateController::autofocusFieldForError($e->getMessage()); $autofocusField = ThesisCreateController::autofocusFieldForError($e->getMessage());
if ($autofocusField !== null) { if ($autofocusField !== null) {
App::flashAutofocus($autofocusField); App::flashAutofocus($autofocusField);
} }
header('Location: ../add.php'); header('Location: ' . $redirect);
exit(); exit();
} }

View File

@@ -23,11 +23,9 @@ try {
$formData = $_SESSION['form_data'] ?? []; $formData = $_SESSION['form_data'] ?? [];
unset($_SESSION['form_data']); unset($_SESSION['form_data']);
$autofocusField = App::consumeAutofocus(); $autofocusField = App::consumeAutofocus();
// Flash error consumed by the flash-messages partial below.
/** /**
* Merge autofocus into the $attrs array for a given field. * Merge autofocus into the $attrs array for a given field.
* Only adds the attribute when $autofocusField matches $fieldName.
*/ */
function withAutofocus(string $fieldName, array $attrs = []): array { function withAutofocus(string $fieldName, array $attrs = []): array {
global $autofocusField; global $autofocusField;
@@ -74,13 +72,19 @@ if ($studentMode) {
<form action="actions/formulaire.php" method="post" enctype="multipart/form-data" class="admin-form"> <form action="actions/formulaire.php" method="post" enctype="multipart/form-data" class="admin-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>">
<?php if ($studentMode): ?>
<input type="hidden" name="student_mode" value="1">
<?php endif; ?>
<!-- ═══════════════════ Informations du TFE ═══════════════════ -->
<fieldset>
<legend>Informations du TFE</legend>
<?php $name = 'titre'; $label = 'Titre :'; $value = old('titre'); $required = true; $attrs = withAutofocus('titre'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?> <?php $name = 'titre'; $label = 'Titre :'; $value = old('titre'); $required = true; $attrs = withAutofocus('titre'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'subtitle'; $label = 'Sous-titre (si applicable) :'; $value = old('subtitle'); $required = false; include APP_ROOT . '/templates/partials/form/text-field.php'; ?> <?php $name = 'subtitle'; $label = 'Sous-titre (si applicable) :'; $value = old('subtitle'); $required = false; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = old('auteurice'); $required = true; $attrs = withAutofocus('auteurice', ['autocomplete' => 'name']); include APP_ROOT . '/templates/partials/form/text-field.php'; ?> <?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = old('auteurice'); $required = true; $attrs = withAutofocus('auteurice', ['autocomplete' => 'name']); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'mail'; $label = 'Contact(s) (optionnel) [mail/site/insta/etc.] :'; $value = old('mail'); $attrs = ['autocomplete' => 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?> <?php $name = 'mail'; $label = 'Contact(s) (optionnel) [mail/site/insta/etc.] :'; $value = old('mail'); $attrs = ['autocomplete' => 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<!-- Contact visibility -->
<div class="admin-form-group"> <div class="admin-form-group">
<label class="admin-checkbox-label"> <label class="admin-checkbox-label">
<input type="checkbox" name="contact_public" value="1" <input type="checkbox" name="contact_public" value="1"
@@ -90,8 +94,21 @@ if ($studentMode) {
<small>Si cette case est cochée, votre contact apparaîtra sur la page publique de votre TFE.</small> <small>Si cette case est cochée, votre contact apparaîtra sur la page publique de votre TFE.</small>
</div> </div>
<div>
<label for="synopsis">Synopsis :</label>
<textarea id="synopsis" name="synopsis"
rows="7" required
<?= $autofocusField === 'synopsis' ? 'autofocus' : '' ?>><?= old('synopsis') ?></textarea>
</div>
</fieldset>
<!-- ═══════════════════ Composition du jury ═══════════════════ -->
<?php require APP_ROOT . '/templates/partials/form/jury-fieldset.php'; ?> <?php require APP_ROOT . '/templates/partials/form/jury-fieldset.php'; ?>
<!-- ═══════════════════ Cadre académique ═══════════════════ -->
<fieldset>
<legend>Cadre académique</legend>
<?php <?php
$name = 'année'; $label = 'Année :'; $value = old('année'); $required = true; $name = 'année'; $label = 'Année :'; $value = old('année'); $required = true;
$type = 'number'; $type = 'number';
@@ -101,24 +118,27 @@ if ($studentMode) {
?> ?>
<?php $name = 'orientation'; $label = 'Orientation :'; $options = $orientations; $selected = $formData['orientation'] ?? ''; $required = true; $placeholder = ''; $attrs = withAutofocus('orientation'); include APP_ROOT . '/templates/partials/form/select-field.php'; ?> <?php $name = 'orientation'; $label = 'Orientation :'; $options = $orientations; $selected = $formData['orientation'] ?? ''; $required = true; $placeholder = ''; $attrs = withAutofocus('orientation'); include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
<?php $name = 'ap'; $label = 'Atelier pluridisciplinaire :'; $options = $apPrograms; $selected = $formData['ap'] ?? ''; $required = true; $placeholder = ''; $attrs = withAutofocus('ap'); include APP_ROOT . '/templates/partials/form/select-field.php'; ?> <?php $name = 'ap'; $label = 'Atelier pluridisciplinaire :'; $options = $apPrograms; $selected = $formData['ap'] ?? ''; $required = true; $placeholder = ''; $attrs = withAutofocus('ap'); include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
<?php $name = 'finality'; $label = 'Finalité du master :'; $options = $finalityTypes; $selected = $formData['finality'] ?? ''; $required = true; $placeholder = ''; $attrs = withAutofocus('finality'); include APP_ROOT . '/templates/partials/form/select-field.php'; ?> <?php $name = 'finality'; $label = 'Finalité du master :'; $options = $finalityTypes; $selected = $formData['finality'] ?? ''; $required = true; $placeholder = ''; $attrs = withAutofocus('finality'); include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
<?php $name = 'languages'; $label = 'Langue(s) :'; $options = $languages; $checked = $formData['languages'] ?? []; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?> <?php $name = 'languages'; $label = 'Langue(s) :'; $options = $languages; $checked = $formData['languages'] ?? []; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
<?php $name = 'formats'; $label = 'Format(s) :'; $options = $formatTypes; $checked = $formData['formats'] ?? []; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?> <?php $name = 'formats'; $label = 'Format(s) :'; $options = $formatTypes; $checked = $formData['formats'] ?? []; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
<?php $name = 'tag'; $label = 'Mots-clés :'; $value = old('tag'); $placeholder = 'sociologie, anthropologie, ...'; $hint = 'Séparez par des virgules. Max 10 mots-clés.'; $attrs = withAutofocus('tag'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?> <?php $name = 'tag'; $label = 'Mots-clés :'; $value = old('tag'); $placeholder = 'sociologie, anthropologie, ...'; $hint = 'Séparez par des virgules. Max 10 mots-clés.'; $attrs = withAutofocus('tag'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
</fieldset>
<!-- Synopsis --> <!-- ═══════════════════ Fichiers ═══════════════════ -->
<div> <fieldset>
<label for="synopsis">Synopsis :</label> <legend>Fichiers</legend>
<textarea id="synopsis" name="synopsis"
rows="7" required <?php $name = 'couverture'; $label = 'Image de couverture :'; $accept = 'image/jpeg,image/png'; $hint = 'JPG, PNG. Taille max : 10 MB.'; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
<?= $autofocusField === 'synopsis' ? 'autofocus' : '' ?>><?= old('synopsis') ?></textarea> <?php $name = 'banner'; $label = 'Image bannière (accueil) :'; $accept = 'image/jpeg,image/png,image/webp'; $hint = 'JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 5 MB.'; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
</div> <?php $name = 'files'; $label = 'Fichiers du TFE :'; $accept = '.pdf,.jpg,.jpeg,.png,.mp4,.zip,.vtt'; $hint = 'PDF, JPG, PNG, MP4, ZIP. Max 50 MB par fichier. Pour les vidéos, un fichier .vtt de sous-titres peut être joint (il sera associé automatiquement à la vidéo correspondante).'; $multiple = true; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
</fieldset>
<!-- ═══════════════════ Métadonnées complémentaires ═══════════════════ -->
<fieldset>
<legend>Métadonnées complémentaires</legend>
<?php $name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes; $selected = $formData['license_id'] ?? ''; $placeholder = '— Inconnue —'; include APP_ROOT . '/templates/partials/form/select-field.php'; ?> <?php $name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes; $selected = $formData['license_id'] ?? ''; $placeholder = '— Inconnue —'; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
@@ -126,18 +146,10 @@ if ($studentMode) {
<?php $name = 'lien'; $label = 'Lien (site / ressource) :'; $value = old('lien'); $type = 'url'; $placeholder = 'https://...'; $attrs = withAutofocus('lien'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?> <?php $name = 'lien'; $label = 'Lien (site / ressource) :'; $value = old('lien'); $type = 'url'; $placeholder = 'https://...'; $attrs = withAutofocus('lien'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'couverture'; $label = 'Image de couverture :'; $accept = 'image/jpeg,image/png'; $hint = 'JPG, PNG. Taille max : 10 MB.'; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
<?php $name = 'banner'; $label = 'Image bannière (accueil) :'; $accept = 'image/jpeg,image/png,image/webp'; $hint = 'JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 5 MB.'; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
<?php $name = 'files'; $label = 'Fichiers du TFE :'; $accept = '.pdf,.jpg,.jpeg,.png,.mp4,.zip,.vtt'; $hint = 'PDF, JPG, PNG, MP4, ZIP. Max 50 MB par fichier. Pour les vidéos, un fichier .vtt de sous-titres peut être joint (il sera associé automatiquement à la vidéo correspondante).'; $multiple = true; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
<?php <?php
// Visibility select — only show options enabled in settings
$accessOptions = array_map(function($at) { $accessOptions = array_map(function($at) {
return ['id' => $at['id'], 'name' => $at['name']]; return ['id' => $at['id'], 'name' => $at['name']];
}, $enabledAccessTypes); }, $enabledAccessTypes);
// Default: Interne (id=2)
$defaultAccessType = 2; $defaultAccessType = 2;
$selectedAccessType = isset($formData['access_type_id']) $selectedAccessType = isset($formData['access_type_id'])
? (int)$formData['access_type_id'] ? (int)$formData['access_type_id']
@@ -151,6 +163,56 @@ if ($studentMode) {
$attrs = []; $attrs = [];
include APP_ROOT . '/templates/partials/form/select-field.php'; include APP_ROOT . '/templates/partials/form/select-field.php';
?> ?>
</fieldset>
<?php if ($studentMode): ?>
<!-- ═══════════════════ Degrés d'ouverture ═══════════════════ -->
<fieldset class="licence-explanation">
<legend>Degrés d'ouverture et licences</legend>
<div class="licence-info">
<h3>Je veux que mon TFE soit disponible sous les conditions suivantes :</h3>
<div class="licence-degree">
<h4>🔓 Libre</h4>
<p>Mon TFE est en libre accès à tout le monde sur la plateforme des TFE ainsi que dans la bibliothèque de l'erg. Je suis conscient·e des responsabilités et obligations légales qui viennent avec une diffusion externe et acquiesce avoir lu la documentation prévue à cet effet par l'erg, ainsi qu'avoir discuté des enjeux d'une publication avec l'équipe pédagogique. J'accepte de partager mes droits de diffusion avec l'erg, ce uniquement dans le cadre d'une diffusion sur la plateforme xamxam.</p>
<ul>
<li><label><input type="checkbox" name="cc4r" value="1"> J'accepte les conditions collectives de réutilisation (CC4r) <em class="hint">(pas obligatoire)</em></label></li>
<li><label><input type="checkbox" name="specific_license" value="1"> Je souhaite appliquer une licence spécifique à mon travail <em class="hint">(pas obligatoire)</em></label></li>
</ul>
<p class="licence-note"><em>Au moins une des deux cases doit être cochée pour le degré Libre.</em></p>
</div>
<div class="licence-degree">
<h4>🔒 Interne</h4>
<p>Mon TFE et ma note d'intention ne sont accessibles que sur place en physique ainsi que sur la plateforme xamxam par la communauté erg. Une note descriptive est disponible sur le site à toustes. J'autorise une (ré-)utilisation et diffusion dans un contexte académique et didactique au sein de l'erg.</p>
<p class="licence-note"><em>La diffusion limitée est protégée par le cadre académique/didactique, le travail pourrait donc être diffusé en interne et être cité par d'autres étudiant·es sans implications légales pour l'auteur·ice ni pour l'école.</em></p>
<ul>
<li><label><input type="checkbox" name="cc4r" value="1"> J'accepte les conditions collectives de réutilisation (CC4r) <em class="hint">(pas obligatoire)</em></label></li>
<li><label><input type="checkbox" name="specific_license" value="1"> Je souhaite appliquer une licence spécifique à mon travail <em class="hint">(pas obligatoire)</em></label></li>
</ul>
<p class="licence-note"><em>Au moins une des deux cases doit être cochée.</em></p>
</div>
<div class="licence-degree">
<h4>🚫 Interdit</h4>
<p>Mon TFE n'est pas disponible en physique ni sur le site. Une note descriptive est disponible sur le site.</p>
</div>
</div>
<div class="licence-generalites">
<h3>Généralités</h3>
<ul>
<li>L'auteur·ice peut décider entre trois degrés de partage de son travail : <strong>libre</strong>, <strong>interne</strong>, <strong>interdit</strong>.</li>
<li>L'auteur·ice peut, à tout moment, décider de <strong>restreindre</strong> le degré d'accès à son travail. Il ne peut néanmoins pas l'ouvrir davantage.</li>
<li>Le choix effectué dans ce formulaire sera d'application <strong>une semaine après la soutenance orale</strong> de l'auteur·ice. Celui-ci peut donc décider de restreindre ce choix avant sa publication (mais pas l'ouvrir).</li>
<li>L'erg se réserve le droit de restreindre le degré d'ouverture du TFE ce en accord avec le règlement.</li>
<li>Dans tous les cas, l'auteur·ice garde les droits d'auteurs, de diffusion, d'utilisation, etc. de son travail sauf si la licence choisie restreindrait ses droits.</li>
<li>La diffusion « xamxam » est indépendante de la diffusion à la BAIU.</li>
</ul>
</div>
</fieldset>
<?php endif; ?>
<div class="form-footer"> <div class="form-footer">
<button type="submit" name="go">Soumettre</button> <button type="submit" name="go">Soumettre</button>

View File

@@ -3,14 +3,16 @@
require_once __DIR__ . "/../../config/bootstrap.php"; require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../src/AdminAuth.php'; require_once __DIR__ . '/../../src/AdminAuth.php';
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
AdminAuth::requireLogin();
// Configure error reporting // Configure error reporting
ini_set('display_errors', 0); ini_set('display_errors', 0);
ini_set('log_errors', 1); ini_set('log_errors', 1);
ini_set('error_log', 'error.log'); ini_set('error_log', 'error.log');
$studentMode = isset($_GET['mode']) && $_GET['mode'] === 'student';
if (!$studentMode) {
AdminAuth::requireLogin();
}
require_once __DIR__ . '/../../src/Database.php'; require_once __DIR__ . '/../../src/Database.php';
// Security: Validate thesis ID parameter // Security: Validate thesis ID parameter
@@ -62,10 +64,48 @@ function formatFileSize($bytes) {
// Set page title for header // Set page title for header
$pageTitle = "Récapitulatif TFE"; $pageTitle = "Récapitulatif TFE";
?> ?>
<?php $isAdmin = true; $bodyClass = 'admin-body'; require_once APP_ROOT . '/templates/head.php'; ?> <?php
<?php include APP_ROOT . '/templates/header.php'; ?> $isAdmin = true;
if ($studentMode) {
$bodyClass = 'admin-body student-body';
require_once APP_ROOT . '/templates/head.php';
} else {
$bodyClass = 'admin-body';
require_once APP_ROOT . '/templates/head.php';
include APP_ROOT . '/templates/header.php';
}
?>
<main id="main-content"> <main id="main-content">
<?php if ($studentMode): ?>
<!-- ═══════════════════ STUDENT MODE: Thank you page ═══════════════════ -->
<div class="thanks-student-page">
<?php if ($error): ?>
<div class="thanks-error">
<h1>⚠ Oups…</h1>
<p><?= htmlspecialchars($error) ?></p>
<a href="/admin/add.php?mode=student" class="btn-new-form">← Retour au formulaire</a>
</div>
<?php elseif ($thesis): ?>
<div class="thanks-success">
<h1>Merci 🎉</h1>
<p class="thanks-message">
Ton TFE <strong><?= htmlspecialchars($thesis['title']) ?></strong> a bien été soumis.
</p>
<a href="/admin/add.php?mode=student" class="btn-new-form">+ Ajouter un nouveau TFE</a>
</div>
<?php else: ?>
<div class="thanks-error">
<h1>Erreur</h1>
<p>Aucune donnée à afficher.</p>
<a href="/admin/add.php?mode=student" class="btn-new-form">← Retour au formulaire</a>
</div>
<?php endif; ?>
</div>
<?php else: ?>
<!-- ═══════════════════ ADMIN MODE: Recap page ═══════════════════ -->
<h1>Récapitulatif TFE</h1> <h1>Récapitulatif TFE</h1>
<?php if ($error): ?> <?php if ($error): ?>
@@ -148,6 +188,8 @@ $pageTitle = "Récapitulatif TFE";
<p class="admin-muted">Aucune donnée à afficher.</p> <p class="admin-muted">Aucune donnée à afficher.</p>
<p><a href="/admin/add.php" class="admin-btn-secondary">Retour au formulaire</a></p> <p><a href="/admin/add.php" class="admin-btn-secondary">Retour au formulaire</a></p>
<?php endif; ?> <?php endif; ?>
<?php endif; ?>
</main> </main>
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?> <?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>

View File

@@ -1748,3 +1748,66 @@
.form-footer button:hover { .form-footer button:hover {
background: var(--accent-secondary); background: var(--accent-secondary);
} }
/* ── Student thanks page ────────────────────────────────────────────────── */
.thanks-student-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
text-align: center;
}
.thanks-success,
.thanks-error {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-m);
max-width: 520px;
}
.thanks-success h1,
.thanks-error h1 {
font-size: var(--step-3);
margin: 0;
letter-spacing: 0.06em;
}
.thanks-message {
font-size: var(--step-0);
color: var(--text-primary);
margin: 0;
line-height: 1.6;
}
.thanks-error p {
font-size: var(--step-0);
color: var(--text-secondary);
margin: 0;
}
.btn-new-form {
display: inline-flex;
align-items: center;
gap: var(--space-2xs);
padding: var(--space-m) var(--space-2xl);
background: var(--accent-primary);
color: var(--accent-foreground);
border: none;
border-radius: 6px;
font-size: var(--step-0);
font-weight: 600;
font-family: inherit;
text-decoration: none;
cursor: pointer;
letter-spacing: 0.04em;
transition: background 0.15s, transform 0.15s;
margin-top: var(--space-s);
}
.btn-new-form:hover {
background: var(--accent-secondary);
transform: translateY(-1px);
}

1
storage/maintenance.flag Normal file
View File

@@ -0,0 +1 @@
2026-04-15T11:53:16+00:00