diff --git a/TODO.md b/TODO.md
index dd234fe..2b0753e 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,129 +1,12 @@
-# XAMXAM TODO
-
-## Completed
-
-- [x] PeerTube integration — two parallel systems (backup direct upload + PeerTube API)
- - [x] `PeerTubeService.php` — credentials CRUD + OAuth2 password grant + multipart upload to `/api/v1/videos/upload`
- - [x] Migration `021_peertube_settings.sql` — `peertube_settings` table (singleton) + `peertube_upload_enabled` feature flag (default 0 = disabled)
- - [x] `actions/settings.php` — `peertube` section handler (toggle + credential save)
- - [x] `admin/parametres.php` — PeerTube section UI (instance URL, username, password, channel ID, privacy)
- - [x] `templates/admin/parametres.php` — PeerTube settings form between SMTP and admin account sections
- - [x] `admin/partage/fichiers-fragment.php` — shows `` for video/audio when enabled, keeps TODO notice when disabled
- - [x] `ThesisCreateController` — `handlePeerTubeUpload()` uploads video/audio to PeerTube, stores watch URL as `thesis_files` row
- - [x] `ThesisEditController` — same `handlePeerTubeUpload()` method for edit workflow
- - [x] `templates/public/tfe.php` — renders PeerTube iframe embed for files whose path contains `/videos/watch/`
- - [x] `AdminLogger` — `logPeerTubeUpdate()` audit method
- - [x] Direct file upload fallback: when `peertube_upload_enabled = 0`, standard `` + local storage works unchanged
-
-- [x] Backoffice fieldset reorder — Note contextuelle merged in, Lien BAIU added, removed from Métadonnées
- - [x] Backoffice order: Note contextuelle → Points du jury → Remarques → Lien BAIU → Exemplaire BAIU → Exemplaire ERG → Contact interne
- - [x] Removed standalone "Note contextuelle" fieldset (now inside Backoffice)
- - [x] Lien BAIU moved from Métadonnées complémentaires into Backoffice
- - [x] Métadonnées fieldset now: pages, minutes, annexes only
-
-- [x] Form fixes batch
- - [x] bentopdf link clearer: "PDFs trop lourds ? https://bentopdf.com/" (full URL visible)
- - [x] Multiple promoteurices: interne and ULB fields now dynamic (add/remove rows, same as lecteurs)
- - [x] Contact visibility duplication removed from admin forms (`showContact = false`; `mail` field in fieldset-tfe-info covers it)
- - [x] Asterisk corrections in files section: note_intention, website URL, video, audio all show red asterisk + `required` when non-admin
- - [x] ULB promoteurice asterisk + required when finality=Approfondi (JS toggles `*` + `required` on first ULB input)
- - [x] Controllers handle `jury_promoteur` and `jury_promoteur_ulb_name` as both scalar and array (backwards compat)
-
-- [x] Fix `just serve` — justfile shebang recipes (`deploy-env`, `reencrypt-password`) used space indentation instead of tabs, causing "extra leading whitespace" parse error
-
-- [x] PDF 100 MB limit + bentopdf mention
- - [x] `ThesisCreateController`: `MAX_PDF_SIZE = 100 MB`; PDFs checked against it, other files still 500 MB
- - [x] `ThesisEditController`: same per-PDF limit applied
- - [x] `fichiers-fragment.php`: note d'intention and TFE hints mention 100 MB PDF limit + bentopdf.com link
- - [x] `form.php` edit-mode new-files hint updated
- - [x] `file-field.php`: added `$hintRaw` flag to allow HTML in hints
-
-- [x] Format types: reorder, rename, add Image/Écriture
- - [x] Migration 019: add Écriture
- - [x] Migration 020: add `sort_order` column, rename Autre → Etc. / Autre, add Image, set display order (Écriture · Image · Audio · Vidéo · Site web · Performance · Objet éditorial · Installation · Etc. / Autre)
- - [x] `Database.php` format_types query uses `ORDER BY sort_order, id`
- - [x] `fichiers-fragment.php` uses `ORDER BY sort_order, id`; Image/Vidéo/Audio IDs resolved via name map
- - [x] TODO: Vidéo + Audio — PeerTube API upload (notice shown in form for now)
-
-- [x] Combined Format + Fichiers into HTMX-swappable block
- - [x] `partage/fichiers-fragment.php` — new combined fragment: format checkboxes + fichiers fieldset that adapts based on selected formats (upload inputs / URL fields / both)
- - [x] Route `/partage/fichiers-fragment` added to `partage/index.php`
- - [x] `admin/fichiers-fragment.php` — admin-gated wrapper for the same fragment (sets `admin_mode=1`)
- - [x] `admin/format-website-fragment.php` — admin-gated fragment for edit-mode website URL fieldset toggle
- - [x] `form.php` — add/partage mode: replaced separate Format + Fichiers + website-url-fieldset with single `#format-fichiers-block` server-rendered via shared fragment
- - [x] `form.php` — edit mode: Format checkboxes wire to `admin/format-website-fragment.php` → `#edit-website-url-fieldset` (existing-file management untouched)
- - [x] `checkbox-list.php` — added `$hxInclude` variable (defaults to `'this, #website-url-fieldset'`) so callers can customise included fields
-
-- [x] TDD analysis + new test suites
- - [x] **Bug fixed**: `SearchController::handleSearch()` — `$coverMap` undefined variable + never populated for search results
- - [x] `ShareLinkTest` (13 tests) — `generateSlug`, all `validateLink` branches, `verifyPassword`, `incrementUsage`, `objet_restriction`
- - [x] `PureLogicTest` (31 tests) — `TfeController` helpers (meta, OG image, jury split, captions), `ThesisCreateController` helpers (autofocus, detectFileType, authorSlug), `ThesisEditController::buildFileSizeInfo`, `ExportController` CSV column consistency, `SearchController` coverMap regression
- - [x] Private helpers promoted to `protected` in `TfeController`, `ThesisCreateController`, `ThesisEditController` to enable subclass-based testing without reflection
-
-- [x] Form save audit + TDD
- - [x] `createThesis()` missing `duration_pages`/`duration_minutes` columns — fixed
- - [x] `ThesisCreateController` not passing raw page/minute values to `createThesis()` — fixed (`durationPages`, `durationMinutes` extracted and passed)
- - [x] `FormSaveTest.php` — 14 red-green tests covering create+edit round-trips for all fields
-
-- [x] Language form improvements
- - [x] Add Néerlandais as default language option (schema + migration 017)
- - [x] `language_autre` conditionally required via HTMX fragment (replaced custom JS)
- - [x] `language_autre` saved via `getOrCreateLanguage()` in both create and edit controllers
- - [x] `formData['languages']` wired in edit.php so checkboxes are pre-checked
- - [x] `duration_pages`/`duration_minutes` saved in `updateThesis()` and read back in `getThesisRawFields()`
- - [x] `beforeunload-guard` applied to add and partage forms too
-
-- [x] Audit + fix direct PHP URL references blocked by nginx catch-all `deny all`
- - [x] `/request-access.php` fetch in `tfe.php` → `/request-access`
- - [x] `/media.php?path=` in `form.php` (×2) and `admin/recapitulatif.php` → `/media?path=`
-
-- [x] Fix 403 on `/language-autre-fragment.php` from `edit.php`
- - [x] Root cause: standalone root-level PHP file blocked by nginx catch-all `deny all`
- - [x] Moved logic to `partage/language-autre-fragment.php` (shared include)
- - [x] Added route `/partage/language-autre-fragment` in `partage/index.php`
- - [x] Added `admin/language-autre-fragment.php` (AdminAuth gated, includes shared logic)
- - [x] `form.php` picks URL based on `$mode` (`partage` vs admin)
- - [x] Deleted `public/language-autre-fragment.php`; nginx unchanged
-
-- [x] Merge banner images into cover images
- - [x] Migration 016: copy `storage/banners/*` → `storage/covers/`, insert `thesis_files` cover records, clear `banner_path`, remove banners dir
- - [x] Remove banner fieldset from edit form (`form.php`)
- - [x] Remove banner fieldset from student submission form (`fieldset-files.php`: rename to couverture)
- - [x] Update `ThesisEditController::save()` — remove banner upload/removal logic
- - [x] Update `ThesisCreateController::submit()` — remove `handleBannerUpload` call
- - [x] Update `Database::handleCoverUpload()` — add webp support, raise limit to 20 MB
- - [x] Remove `Database::setBannerPath()`, `handleBannerUpload()`, `getThesisBannerPath()`
- - [x] Update `Database::deleteThesis()` / `bulkDeleteTheses()` — remove banner file cleanup
- - [x] `HomeController`: batch-load covers for all items, remove banner_path fallback
- - [x] `SearchController::handleSearch()`: batch-load covers, pass `$coverMap` to view
- - [x] `SearchController::handleStudentPreview()`: load covers, pass `$coverMap` to partial
- - [x] `TfeController::resolveOgImage()`: use cover file_type instead of banner_path
- - [x] `home.php`: use only `$coverMap` (no banner_path fallback)
- - [x] `search.php`: show cover thumbnail on result cards
- - [x] `student-preview.php`: use `$coverMap` instead of `banner_path`
- - [x] Migration applied and file moved to `applied/`
-
-- [x] Remove `required` from all form inputs in admin add/edit
- - [x] Introduced `$adminMode` flag in `form.php` (true when `$mode` is `'add'` or `'edit'`)
- - [x] Hidden "champs obligatoires" note in admin mode
- - [x] All `$required = true` callers in `form.php`, `fieldset-tfe-info.php`, `fieldset-academic.php`, `fieldset-licence-explanation.php`, `fieldset-files.php` changed to `!$adminMode`
- - [x] Hardcoded `required` HTML attributes in `fieldset-tfe-info.php` (synopsis, objet radios), `fieldset-licence-explanation.php` (access type radios), `jury-fieldset.php` (promoteur, lecteurs interne/externe) gated on `!$adminMode`
- - [x] Dynamic JS `ulbInput.required` in jury fieldset also gated
# TODO
-- [x] Make all heading font sizes the same (slightly smaller than current h1) in common.css
-- [x] Remove individual font-size overrides from other CSS files so they inherit
-- [x] Standardise header nav structure: admin uses nav-left/nav-right like public
-- [x] Unify font-size for all nav links (logo + nav links all use var(--step--1))
-- [x] Clean up redundant CSS rules (.nav-logo, .nav-left-links)
-- [x] Update admin.css selectors to match new header structure
-- [x] Bump nav font-size to var(--step-0)
-- [x] Add small inverted top gradient to admin body
-- [x] Commit
-- [x] Cap home page cards grid to max 3 columns (was auto-fill, now repeat(3, 1fr) with 2→1 column breakpoints)
-- [x] Remove Modifier link from admin header when on edit page
-- [x] Move admin nav links to right side, keep only logo on left
-- [x] Remove Mots-clés from admin header, add as button in dashboard toolbar; use grid layout (title|stats, search|buttons)
-- [x] Group admin toolbar buttons: + Ajouter + Mots-clés stacked above Import/Export
-- [x] Stack admin filters vertically: search+button row above dropdowns row
-- [x] Standardise form inputs/selects/textareas in common.css: padding, --radius var, 2px accent border on focus
+- [x] Add fixed top gradient on partage main element, mirror of bottom gradient
+- [x] Remove bottom border on `.thesis-add-header`
+- [x] Add subtitle "Formulaire pour XAMXAM" below partage header, with Ductus link to /
+- [x] Switch `.thesis-add-header` to grid layout
+- [ ] Create `admin/operation.php` — unified add/edit page
+- [ ] Wire up route: `?id=` → edit mode, no id → add mode
+- [ ] Update all references: `add.php` → `operation.php`, `edit.php` → `operation.php?id=`
+- [ ] Keep old `add.php` and `edit.php` as redirect stubs
+- [ ] Keep action endpoints (`actions/formulaire.php`, `actions/edit.php`) unchanged
+- [ ] Test both flows
diff --git a/app/.env b/app/.env
new file mode 100644
index 0000000..eaca243
--- /dev/null
+++ b/app/.env
@@ -0,0 +1 @@
+APP_KEY=M+4xc/c9/b4H+ScjO4cU82FzcZRO+xp5pzYNvJapUOQ=
diff --git a/app/migrations/applied/022_form_help_blocks_name_enabled.sql b/app/migrations/applied/022_form_help_blocks_name_enabled.sql
new file mode 100644
index 0000000..192cba3
--- /dev/null
+++ b/app/migrations/applied/022_form_help_blocks_name_enabled.sql
@@ -0,0 +1,16 @@
+-- Add name (human-readable title) and enabled (show/hide toggle) columns
+-- to form_help_blocks. Drop sort_order — blocks are now fixed-position,
+-- one per fieldset, no reordering.
+
+ALTER TABLE form_help_blocks ADD COLUMN name TEXT NOT NULL DEFAULT '';
+ALTER TABLE form_help_blocks ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1;
+
+-- Backfill names from the FORM_HELP_LABELS mapping.
+UPDATE form_help_blocks SET name = 'Introduction' WHERE key = 'partage_intro';
+UPDATE form_help_blocks SET name = 'Informations du TFE' WHERE key = 'fieldset_tfe_info';
+UPDATE form_help_blocks SET name = 'Note Synopsis' WHERE key = 'fieldset_synopsis';
+UPDATE form_help_blocks SET name = 'Composition du jury' WHERE key = 'fieldset_jury';
+UPDATE form_help_blocks SET name = 'Cadre académique' WHERE key = 'fieldset_academic';
+UPDATE form_help_blocks SET name = 'Fichiers' WHERE key = 'fieldset_files';
+UPDATE form_help_blocks SET name = 'Visibilité / Accès' WHERE key = 'fieldset_access';
+UPDATE form_help_blocks SET name = 'E-mail de confirmation' WHERE key = 'fieldset_email';
diff --git a/app/migrations/applied/023_add_missing_help_blocks.sql b/app/migrations/applied/023_add_missing_help_blocks.sql
new file mode 100644
index 0000000..9646aad
--- /dev/null
+++ b/app/migrations/applied/023_add_missing_help_blocks.sql
@@ -0,0 +1,8 @@
+-- Add missing form help block keys for all student-form fieldsets.
+-- fieldset_synopsis is already seeded but injected inside Informations du TFE via $synopsisExtra.
+-- fieldset_academic already exists but was never wired in form.php for partage mode.
+
+INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled) VALUES
+ ('fieldset_languages', 'Langue(s)', '', 1),
+ ('fieldset_keywords', 'Mots-clés', '', 1),
+ ('fieldset_metadata', 'Métadonnées complémentaires', '', 1);
diff --git a/app/public/admin/actions/form-help-reorder.php b/app/public/admin/actions/form-help-reorder.php
index 9c2418a..7fa5d40 100644
--- a/app/public/admin/actions/form-help-reorder.php
+++ b/app/public/admin/actions/form-help-reorder.php
@@ -1,47 +1,11 @@
reorderFormHelpBlocks($keys);
-} catch (Exception $e) {
- error_log('form-help-reorder error: ' . $e->getMessage());
- http_response_code(500);
- echo 'Erreur lors de la sauvegarde.';
- exit;
-}
-
http_response_code(204);
exit;
diff --git a/app/public/admin/add.php b/app/public/admin/add.php
index bba0e84..84c1375 100644
--- a/app/public/admin/add.php
+++ b/app/public/admin/add.php
@@ -27,7 +27,7 @@ $autofocusField = App::consumeAutofocus();
$siteSettings = Database::getInstance()->getAllSettings();
// Form help blocks
$helpBlocks = Database::getInstance()->getAllFormHelpBlocks();
-$helpFn = fn(string $key) => $helpBlocks[$key]['content'] ?? '';
+$helpFn = fn(string $key) => empty($helpBlocks[$key]['enabled']) ? '' : ($helpBlocks[$key]['content'] ?? '');
function withAutofocus(string $fieldName, array $attrs = []): array {
global $autofocusField;
diff --git a/app/public/admin/contenus-edit.php b/app/public/admin/contenus-edit.php
index 2ef536f..c59635c 100644
--- a/app/public/admin/contenus-edit.php
+++ b/app/public/admin/contenus-edit.php
@@ -50,7 +50,7 @@ try {
} elseif ($formHelpKey) {
$editType = "form_help";
$formHelpContent = $db->getFormHelpBlock($formHelpKey);
- $editTitle = Database::FORM_HELP_LABELS[$formHelpKey] ?? $formHelpKey;
+ $editTitle = $formHelpKey;
} else {
$editType = "apropos";
$value = $db->getAproposContent($aproposKey);
diff --git a/app/public/admin/edit.php b/app/public/admin/edit.php
index 6a695a5..7b0202e 100644
--- a/app/public/admin/edit.php
+++ b/app/public/admin/edit.php
@@ -19,7 +19,7 @@ $autofocusField = App::consumeAutofocus();
// Form help blocks for editable généralités
$helpBlocks = Database::getInstance()->getAllFormHelpBlocks();
-$helpFn = fn(string $key) => $helpBlocks[$key]['content'] ?? '';
+$helpFn = fn(string $key) => empty($helpBlocks[$key]['enabled']) ? '' : ($helpBlocks[$key]['content'] ?? '');
function old($key, $default = "") {
global $formData;
diff --git a/app/public/admin/form-help-inline-fragment.php b/app/public/admin/form-help-inline-fragment.php
new file mode 100644
index 0000000..885d051
--- /dev/null
+++ b/app/public/admin/form-help-inline-fragment.php
@@ -0,0 +1,196 @@
+toggleFormHelpBlock($key);
+ renderCollapsed($db, $key);
+ exit;
+ }
+
+ // Save requires CSRF
+ if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
+ || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
+ http_response_code(403);
+ echo 'Token invalide.';
+ exit;
+ }
+
+ // save
+ $content = $_POST['content'] ?? '';
+ $name = trim($_POST['name'] ?? '');
+ try {
+ $db->setFormHelpBlock($key, $content);
+ if ($name !== '') {
+ $db->setFormHelpBlockName($key, $name);
+ }
+ require_once __DIR__ . '/../../src/AdminLogger.php';
+ AdminLogger::make()->logFormStructureEdit($key);
+ } catch (Exception $e) {
+ error_log('form-help-inline save error: ' . $e->getMessage());
+ http_response_code(500);
+ echo 'Erreur lors de la sauvegarde.';
+ exit;
+ }
+
+ $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
+ renderCollapsed($db, $key);
+ exit;
+}
+
+// ── GET ──────────────────────────────────────────────────────────────────────
+$db = new Database();
+$editMode = ($_GET['edit'] ?? '') === '1';
+
+if ($editMode) {
+ renderEditor($db, $key);
+} else {
+ renderCollapsed($db, $key);
+}
+
+// ═══════════════════════════════════════════════════════════════════════════════
+// RENDER HELPERS
+// ═══════════════════════════════════════════════════════════════════════════════
+
+function renderCollapsed(Database $db, string $key): void
+{
+ $blocks = $db->getAllFormHelpBlocks();
+ $b = $blocks[$key] ?? ['content' => '', 'name' => '', 'enabled' => 0];
+ $name = $b['name'] ?: $key;
+ $content = $b['content'] ?? '';
+ $enabled = (int)($b['enabled'] ?? 1);
+ $hasContent = trim($content) !== '';
+
+ $mdHtml = '';
+ if ($hasContent) {
+ require_once APP_ROOT . '/src/Parsedown.php';
+ $pd = new Parsedown();
+ $pd->setSafeMode(true);
+ $mdHtml = $pd->text($content);
+ }
+ ?>
+
+
+
+
+
= htmlspecialchars($name) ?>
+
+
= $mdHtml ?>
+
+
— vide —
+
+
+
+
+
+
+
+
+
+
+
+
+
+ getAllFormHelpBlocks();
+ $b = $blocks[$key] ?? ['content' => '', 'name' => ''];
+ $name = $b['name'] ?: $key;
+ $content = $b['content'] ?? '';
+ ?>
+
+ getAllLicenseTypes();
+?>
+
+
diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css
index 2ab6004..d7f4edc 100644
--- a/app/public/assets/css/admin.css
+++ b/app/public/assets/css/admin.css
@@ -16,7 +16,6 @@
white-space: nowrap;
scrollbar-width: thin;
}
-.admin-body header nav .nav-left,
.admin-body header nav .nav-right-links {
flex-shrink: 0;
}
@@ -1480,7 +1479,7 @@
}
/* ═══════════════════════════════════════════════════════════════════════════
- Form Help Blocks — drag-and-drop builder (contenus.php)
+ Form Help Blocks — static structure view (contenus.php)
═══════════════════════════════════════════════════════════════════════════ */
.fhb-hint {
@@ -1489,225 +1488,250 @@
margin-bottom: var(--space-m);
}
-.fhb-layout {
+/* ── Structure container ───────────────────────────────────────────────────── */
+
+.fhb-structure {
display: grid;
- grid-template-columns: 1fr 1fr;
- gap: var(--space-m);
- align-items: start;
+ grid-template-columns: 1fr;
+ gap: var(--space-xs);
+ max-width: 100%;
margin-top: var(--space-m);
}
-@media (max-width: 800px) {
- .fhb-layout {
- grid-template-columns: 1fr;
- }
-}
+/* ── Fieldset cards (static reference) ─────────────────────────────────────── */
-/* ── Panels ─────────────────────────────────────────────────────────────── */
-
-.fhb-sortable-panel,
-.fhb-form-preview-panel {
+.fhb-fieldset-card {
border: 1px solid var(--border-primary);
border-radius: var(--radius);
- padding: var(--space-s);
+ padding: var(--space-xs) var(--space-s);
background: var(--bg-secondary);
}
-.fhb-panel-title {
+.fhb-fieldset-card-legend {
font-size: var(--step-0);
font-weight: 600;
- margin: 0 0 var(--space-3xs) 0;
- letter-spacing: 0.03em;
-}
-
-.fhb-panel-desc {
- font-size: var(--step--2);
- color: var(--text-secondary);
- margin: 0 0 var(--space-xs) 0;
-}
-
-/* ── Saving indicator ─────────────────────────────────────────────────────── */
-
-.fhb-saving {
- display: none;
- align-items: center;
- gap: var(--space-2xs);
- font-size: var(--step--1);
- color: var(--accent-primary);
- padding: var(--space-2xs) 0;
-}
-
-.fhb-saving.htmx-request {
- display: flex;
-}
-
-/* ── Draggable block cards ─────────────────────────────────────────────────── */
-
-.fhb-sortable {
- display: flex;
- flex-direction: column;
- gap: var(--space-2xs);
- padding: 0;
- margin: 0;
-}
-
-.fhb-block-card {
- display: flex;
- align-items: center;
- gap: var(--space-xs);
- background: var(--bg-primary);
- border: 1px solid var(--border-primary);
- border-left: 4px solid var(--accent-primary);
- border-radius: var(--radius);
- padding: var(--space-2xs) var(--space-xs);
- cursor: default;
- transition: box-shadow 0.15s, border-color 0.15s;
-}
-
-.fhb-block-card:hover {
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
- border-color: var(--accent-primary);
-}
-
-.fhb-drag-handle {
- font-size: 1.2em;
- color: var(--text-tertiary);
- cursor: grab;
- flex-shrink: 0;
- line-height: 1;
- user-select: none;
- padding: 2px 4px;
-}
-
-.fhb-drag-handle:active {
- cursor: grabbing;
-}
-
-.fhb-block-info {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.fhb-block-label {
- font-size: var(--step--1);
- font-weight: 500;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.fhb-block-preview {
- font-size: var(--step--2);
- color: var(--text-secondary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.fhb-block-empty {
- font-size: var(--step--2);
- color: var(--text-tertiary);
- font-style: italic;
-}
-
-.fhb-edit-btn {
- flex-shrink: 0;
- font-size: var(--step--2) !important;
- padding: 2px var(--space-xs) !important;
-}
-
-/* ── SortableJS state classes ─────────────────────────────────────────────── */
-
-.fhb-ghost {
- opacity: 0.35;
- background: var(--accent-muted);
- border-color: var(--accent-primary);
-}
-
-.fhb-chosen {
- box-shadow: 0 4px 16px rgba(149, 87, 181, 0.25);
- border-color: var(--accent-primary);
-}
-
-.fhb-dragging {
- opacity: 0.9;
- box-shadow: 0 8px 24px rgba(0,0,0,0.15);
-}
-
-/* ── Form structure preview (right panel) ─────────────────────────────────── */
-
-.fhb-form-preview {
- display: flex;
- flex-direction: column;
- gap: var(--space-2xs);
-}
-
-.fhb-fieldset-preview {
- border: 1px solid var(--border-secondary);
- border-radius: var(--radius);
- padding: var(--space-xs);
- background: var(--bg-primary);
-}
-
-.fhb-fieldset-legend {
- font-size: var(--step--1);
- font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--space-3xs);
padding-bottom: var(--space-3xs);
border-bottom: 1px solid var(--border-primary);
}
-.fhb-fieldset-inputs {
+.fhb-fieldset-card-inputs {
margin: 0;
padding: 0 0 0 var(--space-s);
- list-style: disc;
+ list-style: none;
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-3xs) var(--space-m);
}
-.fhb-fieldset-inputs li {
+.fhb-fieldset-card-inputs li {
font-size: var(--step--2);
color: var(--text-secondary);
line-height: 1.6;
}
-.fhb-anchor {
- display: flex;
- align-items: center;
- gap: var(--space-2xs);
- border-radius: var(--radius);
- padding: var(--space-3xs) var(--space-xs);
- font-size: var(--step--2);
- border: 1px dashed var(--border-primary);
- background: transparent;
-}
-
-.fhb-anchor--filled {
- border-color: var(--accent-primary);
- background: var(--accent-muted);
- color: var(--accent-secondary);
-}
-
-.fhb-anchor--empty {
+.fhb-fieldset-card-inputs li::before {
+ content: '· ';
color: var(--text-tertiary);
}
-.fhb-anchor-icon {
- flex-shrink: 0;
- font-style: normal;
+/* ── Help block wrapper ────────────────────────────────────────────────────── */
+
+.fhb-block-wrapper {
+ /* container for the fhb-inline, one per help block */
}
-.fhb-anchor-label {
- flex: 1;
-}
+/* ── Inline help block (collapsed state) ───────────────────────────────────── */
-.fhb-anchor-pos {
- font-size: var(--step--2);
- font-weight: 600;
- color: var(--accent-primary);
- background: var(--accent-muted);
+.fhb-inline {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ gap: var(--space-s);
+ align-items: center;
+ border: 1px solid var(--border-primary);
+ border-left: 4px solid var(--accent-primary);
border-radius: var(--radius);
- padding: 0 4px;
+ padding: var(--space-xs) var(--space-s);
+ background: var(--bg-primary);
+ transition: opacity 0.15s;
+}
+
+.fhb-inline:hover {
+ opacity: 0.85;
+}
+
+/* Disabled state — one line only, content hidden */
+.fhb-inline--disabled {
+ border-left-color: var(--text-tertiary);
+ opacity: 0.55;
+}
+
+.fhb-inline--disabled:hover {
+ opacity: 0.75;
+}
+
+.fhb-inline--disabled .fhb-md-preview,
+.fhb-inline--disabled .fhb-inline-empty {
+ display: none;
+}
+
+/* Editing state */
+.fhb-inline--editing {
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-template-rows: auto 1fr auto;
+ border-color: var(--accent-primary);
+ box-shadow: 0 4px 16px rgba(149, 87, 181, 0.15);
+ padding: var(--space-s);
+ min-height: 50vh;
+}
+
+.fhb-inline--editing .fhb-inline-form {
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-template-rows: auto auto 1fr auto;
+ height: 100%;
+}
+
+
+/* ── Left side: name + content ─────────────────────────────────────────────── */
+
+.fhb-inline-body {
+ min-width: 0;
+}
+
+.fhb-inline-name {
+ font-size: var(--step--1);
+ font-weight: 600;
+ color: var(--accent-secondary);
+ margin-bottom: var(--space-3xs);
+}
+
+/* ── Small rendered Markdown preview ──────────────────────────────────────── */
+
+.fhb-md-preview {
+ font-size: var(--step--2);
+ color: var(--text-secondary);
+ line-height: 1.45;
+ max-height: 6em;
+ overflow: hidden;
+}
+
+.fhb-md-preview p {
+ margin: 0;
+}
+
+.fhb-md-preview p + p {
+ margin-top: var(--space-3xs);
+}
+
+.fhb-md-preview ul,
+.fhb-md-preview ol {
+ margin: var(--space-3xs) 0;
+ padding-left: var(--space-s);
+}
+
+.fhb-md-preview li {
+ margin-bottom: 0;
+}
+
+.fhb-md-preview strong { font-weight: 600; }
+.fhb-md-preview em { font-style: italic; }
+.fhb-md-preview code {
+ font-size: 0.9em;
+ background: var(--bg-secondary);
+ padding: 0 var(--space-4xs);
+ border-radius: 3px;
+}
+
+.fhb-inline-empty {
+ font-size: var(--step--2);
+ color: var(--text-tertiary);
+ font-style: italic;
+}
+
+/* ── Right side: edit button + live dot ───────────────────────────────────── */
+
+.fhb-inline-actions {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: var(--space-2xs);
+ flex-shrink: 0;
+}
+
+.fhb-toggle-form {
+ margin: 0;
+ line-height: 0;
+}
+
+/* Live dot — green when on, red when off */
+.fhb-dot {
+ display: block;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ transition: opacity 0.15s;
+}
+
+.fhb-dot:hover {
+ opacity: 0.7;
+}
+
+.fhb-dot--on {
+ background: #2d6a4f;
+}
+
+.fhb-dot--off {
+ background: #c0392b;
+}
+
+/* ── Editor form ──────────────────────────────────────────────────────────── */
+
+.fhb-edit-name-row {
+ margin-bottom: var(--space-xs);
+}
+
+.fhb-edit-label {
+ display: block;
+ font-size: var(--step--2);
+ font-weight: 500;
+ color: var(--text-primary);
+ margin-bottom: var(--space-3xs);
+}
+
+.fhb-name-input {
+ width: 100%;
+ max-width: 400px;
+ padding: var(--space-2xs) var(--space-xs);
+ border: 1px solid var(--border-primary);
+ border-radius: var(--radius);
+ font-size: var(--step--1);
+ font-family: var(--font-body);
+}
+
+.fhb-name-input:focus {
+ outline: none;
+ border-color: var(--accent-primary);
+ box-shadow: 0 0 0 2px var(--accent-muted);
+}
+
+.fhb-overtype-editor .--type-container {
+ border-radius: var(--radius);
+}
+
+.fhb-edit-buttons {
+ display: grid;
+ grid-auto-flow: column;
+ grid-auto-columns: max-content;
+ gap: var(--space-xs);
+ margin-top: var(--space-xs);
+}
+
+.fhb-inline-form {
+ margin-top: var(--space-xs);
}
diff --git a/app/public/assets/css/common.css b/app/public/assets/css/common.css
index 02742ac..512eeff 100644
--- a/app/public/assets/css/common.css
+++ b/app/public/assets/css/common.css
@@ -3,346 +3,349 @@
*,
*::before,
*::after {
- box-sizing: border-box;
+ box-sizing: border-box;
}
html,
body {
- margin: 0;
- padding: 0;
- height: 100%;
- overflow: hidden;
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ overflow: hidden;
}
body {
- font-family: var(--font-body);
- background: var(--bg-primary);
- color: var(--text-primary);
- background: linear-gradient(
- 180deg,
- rgba(0, 0, 0, 0) 92%,
- rgba(149, 87, 181, 1) 100%
- );
- display: flex;
- flex-direction: column;
+ font-family: var(--font-body);
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ background: linear-gradient(
+ 180deg,
+ rgba(0, 0, 0, 0) 92%,
+ rgba(149, 87, 181, 1) 100%
+ );
+ display: flex;
+ flex-direction: column;
}
a {
- color: inherit;
- text-decoration: none;
+ color: inherit;
+ text-decoration: none;
}
a:hover {
- text-decoration: none;
+ text-decoration-line: underline;
+ text-decoration-style: wavy;
+ text-decoration-thickness: 1px;
}
header {
- vertical-align: center;
- flex-shrink: 0;
- background: linear-gradient(
- 180deg,
- var(--gradient-1) 0%,
- var(--gradient-2) 33%,
- var(--gradient-3) 66%,
- var(--gradient-4) 100%
- );
+ vertical-align: center;
+ flex-shrink: 0;
+ background: linear-gradient(
+ 180deg,
+ var(--gradient-1) 0%,
+ var(--gradient-2) 33%,
+ var(--gradient-3) 66%,
+ var(--gradient-4) 100%
+ );
- .nav-logo {
- text-decoration: none;
- font-size: var(--step--1);
+ .nav-logo {
+ text-decoration: none;
+ }
+
+ .nav-left-links,
+ .nav-right-links {
+ display: flex;
+ gap: var(--space-l);
+ align-items: center;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+
+ nav {
+ padding: var(--space-s) var(--space-s);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: var(--step-0);
+
+ a {
+ font-family: var(--font-display);
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: var(--accent-foreground);
+ text-decoration: none;
+ padding: var(--space-3xs) var(--space-xs);
+ border-radius: var(--radius);
+ text-shadow:
+ 0 0 16px var(--header-shadow-strong),
+ 0 0 32px var(--header-shadow-soft);
}
- .nav-left {
- display: flex;
- align-items: center;
- gap: var(--space-l);
+ ul {
+ display: flex;
+ gap: var(--space-l);
+ align-items: center;
+ list-style: none;
+ margin: 0;
+ padding: 0;
}
- .nav-left-links,
- .nav-right-links {
- display: flex;
- gap: var(--space-l);
- align-items: center;
- list-style: none;
- margin: 0;
- padding: 0;
+ ul a {
+ transition: opacity 0.15s;
}
- nav {
- padding: var(--space-s) var(--space-s);
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-size: var(--step-0);
-
- a {
- font-family: var(--font-display);
- letter-spacing: 0.12em;
- text-transform: uppercase;
- color: var(--accent-foreground);
- text-decoration: none;
- padding: var(--space-3xs) var(--space-xs);
- border-radius: var(--radius);
- text-shadow:
- 0 0 16px var(--header-shadow-strong),
- 0 0 32px var(--header-shadow-soft);
- }
-
- ul {
- display: flex;
- gap: var(--space-l);
- align-items: center;
- list-style: none;
- margin: 0;
- padding: 0;
- }
-
- ul a {
- transition: opacity 0.15s;
- }
-
- ul a:hover {
- opacity: 1;
- }
+ ul a:hover {
+ opacity: 1;
}
+ }
- ul a[aria-current="page"] {
- opacity: 1;
- border-bottom: 1px solid var(--header-nav-active-border);
- padding-bottom: 1px;
- }
+ ul a[aria-current="page"] {
+ opacity: 1;
+ border-bottom: 1px solid var(--header-nav-active-border);
+ padding-bottom: 1px;
+ }
- /* nav-top-row: transparent wrapper at desktop — children become
- direct flex items of nav, preserving the existing layout */
- .nav-top-row {
- display: contents;
- }
+ /* nav-top-row: transparent wrapper at desktop - children become
+ direct flex items of nav, preserving the existing layout */
+ .nav-top-row {
+ display: contents;
+ }
- /* nav-mobile-links: mobile-only dropdown, hidden at desktop */
- .nav-mobile-links {
- display: none; /* overridden to block inside the mobile media query */
- }
+ /* nav-mobile-links: mobile-only dropdown, hidden at desktop */
+ .nav-mobile-links {
+ display: none; /* overridden to block inside the mobile media query */
+ }
}
/* ============================================================
- HAMBURGER MENU — public nav (pure CSS, checkbox trick)
+ HAMBURGER MENU - public nav (pure CSS, checkbox trick)
DOM order inside (public only):
input.menu-btn ← off-screen checkbox
nav
- div.nav-top-row ← always-visible row (logo + burger)
- div.nav-left ← logo + desktop link list
- ul.nav-right-links ← desktop right links
+ div.nav-top-row ← always-visible row
+ ul.nav-left-links ← logo + Répertoire
+ ul.nav-right-links ← Licences, À Propos
label.menu-icon ← burger icon trigger
ul.nav-mobile-links ← full dropdown (hidden by default)
At desktop: .menu-icon and .nav-mobile-links are display:none.
.nav-top-row is display:contents so its children
- participate directly in nav’s flex row.
+ participate directly in nav's flex row.
At mobile: nav becomes a flex column. .nav-top-row is a real
flex row (logo | burger). .nav-mobile-links expands
via max-height on checkbox:checked.
============================================================ */
-/* Off-screen checkbox — triggered by its label */
+/* Off-screen checkbox - triggered by its label */
.menu-btn {
- position: absolute;
- top: -9999px;
- left: -9999px;
+ position: absolute;
+ top: -9999px;
+ left: -9999px;
}
-/* Burger label — takes no space at desktop */
+/* Burger label - takes no space at desktop */
.menu-icon {
- display: none;
- cursor: pointer;
- padding: var(--space-2xs) var(--space-s);
- align-items: center;
- justify-content: center;
+ display: none;
+ cursor: pointer;
+ padding: var(--space-2xs) var(--space-s);
+ align-items: center;
+ justify-content: center;
}
/* Middle bar of the burger icon */
.navicon {
- background: var(--accent-foreground);
- display: block;
- height: 2px;
- width: 24px;
- position: relative;
- transition: all 0.3s ease-out;
+ background: var(--accent-foreground);
+ display: block;
+ height: 2px;
+ width: 24px;
+ position: relative;
+ transition: all 0.3s ease-out;
}
/* Top and bottom bars */
.navicon::before,
.navicon::after {
- content: "";
- background: var(--accent-foreground);
- display: block;
- height: 2px;
- width: 100%;
- position: absolute;
- transition: all 0.3s ease-out;
+ content: "";
+ background: var(--accent-foreground);
+ display: block;
+ height: 2px;
+ width: 100%;
+ position: absolute;
+ transition: all 0.3s ease-out;
}
.navicon::before {
- top: -7px;
+ top: -7px;
}
.navicon::after {
- bottom: -7px;
+ bottom: -7px;
}
/* ---- Mobile ---- */
@media screen and (max-width: 640px) {
- /* Nav becomes a flex column: top-row on row 1, dropdown on row 2 */
- header nav[aria-label="Navigation principale"] {
- display: flex;
- flex-direction: column;
- align-items: stretch;
- padding: 0;
- }
+ /* Nav becomes a flex column: top-row on row 1, dropdown on row 2 */
+ header nav[aria-label="Navigation principale"] {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ padding: 0;
+ }
- /* Top row: logo left, hamburger right */
- header nav[aria-label="Navigation principale"] .nav-top-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: var(--space-s);
- }
+ /* Top row: logo left, hamburger right */
+ header nav[aria-label="Navigation principale"] .nav-top-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--space-s);
+ }
- /* Hide desktop link lists inside the top row */
- header nav[aria-label="Navigation principale"] .nav-left-links,
- header nav[aria-label="Navigation principale"] .nav-right-links {
- display: none;
- }
+ /* Hide desktop link lists inside the top row, but keep the logo visible */
+ header nav[aria-label="Navigation principale"] .nav-right-links {
+ display: none;
+ }
+ header nav[aria-label="Navigation principale"] .nav-left-links {
+ display: flex;
+ gap: 0;
+ }
+ header nav[aria-label="Navigation principale"]
+ .nav-left-links
+ li:not(:first-child) {
+ display: none;
+ }
- /* Reveal the hamburger icon */
- .menu-icon {
- display: flex;
- }
+ /* Reveal the hamburger icon */
+ .menu-icon {
+ display: flex;
+ }
- /* Dropdown: shown as block but clipped to zero height by default */
- header nav[aria-label="Navigation principale"] .nav-mobile-links {
- display: block; /* override the desktop display:none */
- list-style: none;
- margin: 0;
- padding: 0;
- max-height: 0;
- overflow: hidden;
- transition: max-height 0.2s ease-out;
- }
+ /* Dropdown: shown as block but clipped to zero height by default */
+ header nav[aria-label="Navigation principale"] .nav-mobile-links {
+ display: block; /* override the desktop display:none */
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ max-height: 0;
+ overflow: hidden;
+ transition: max-height 0.2s ease-out;
+ }
- /* ---- Open state ---- */
- .menu-btn:checked
- ~ nav[aria-label="Navigation principale"]
- .nav-mobile-links {
- max-height: 300px;
- }
+ /* ---- Open state ---- */
+ .menu-btn:checked
+ ~ nav[aria-label="Navigation principale"]
+ .nav-mobile-links {
+ max-height: 300px;
+ }
- /* Dropdown link rows */
- header nav[aria-label="Navigation principale"] .nav-mobile-links li {
- border-top: 1px solid var(--header-nav-active-border);
- text-align: left;
- }
+ /* Dropdown link rows */
+ header nav[aria-label="Navigation principale"] .nav-mobile-links li {
+ border-top: 1px solid var(--header-nav-active-border);
+ text-align: left;
+ }
- header nav[aria-label="Navigation principale"] .nav-mobile-links li a {
- display: block;
- width: 100%;
- padding: var(--space-s) var(--space-s);
- text-align: left;
- }
+ header nav[aria-label="Navigation principale"] .nav-mobile-links li a {
+ display: block;
+ width: 100%;
+ padding: var(--space-s) var(--space-s);
+ text-align: left;
+ }
- /* ---- Animate burger → X ---- */
- .menu-btn:checked
- ~ nav[aria-label="Navigation principale"]
- .menu-icon
- .navicon {
- background: transparent;
- }
+ /* ---- Animate burger → X ---- */
+ .menu-btn:checked
+ ~ nav[aria-label="Navigation principale"]
+ .menu-icon
+ .navicon {
+ background: transparent;
+ }
- .menu-btn:checked
- ~ nav[aria-label="Navigation principale"]
- .menu-icon
- .navicon::before {
- transform: rotate(-45deg);
- top: 0;
- }
+ .menu-btn:checked
+ ~ nav[aria-label="Navigation principale"]
+ .menu-icon
+ .navicon::before {
+ transform: rotate(-45deg);
+ top: 0;
+ }
- .menu-btn:checked
- ~ nav[aria-label="Navigation principale"]
- .menu-icon
- .navicon::after {
- transform: rotate(45deg);
- bottom: 0;
- }
+ .menu-btn:checked
+ ~ nav[aria-label="Navigation principale"]
+ .menu-icon
+ .navicon::after {
+ transform: rotate(45deg);
+ bottom: 0;
+ }
}
main {
- flex: 1;
- min-height: 0;
- overflow-wrap: anywhere;
+ flex: 1;
+ min-height: 0;
+ overflow-wrap: anywhere;
}
main * {
- overflow-wrap: anywhere;
- word-break: break-word;
+ overflow-wrap: anywhere;
+ word-break: break-word;
}
/* ============================================================
- HEADINGS — global scale, shared by admin + public pages
+ HEADINGS - global scale, shared by admin + public pages
All headings use the same font size (slightly smaller than
the previous h1). Individual page overrides for size have
been removed so everything inherits from here.
============================================================ */
:where(h1, h2, h3, h4, h5, h6) {
- font-family: var(--font-display);
- font-size: var(--step-2);
- font-weight: 400;
- margin: 0 0 var(--space-l) 0;
- line-height: 1.15;
+ font-family: var(--font-display);
+ font-size: var(--step-2);
+ font-weight: 400;
+ margin: 0 0 var(--space-l) 0;
+ line-height: 1.15;
}
/* ============================================================
SEARCH BAR (shared)
============================================================ */
.header-search-wrap {
- padding: 0 0;
- flex-shrink: 0;
- background-color: var(--gradient-4);
- background: linear-gradient(180deg, var(--gradient-4) 0%, #ffffffee 100%);
+ padding: 0 0;
+ flex-shrink: 0;
+ background-color: var(--gradient-4);
+ background: linear-gradient(180deg, var(--gradient-4) 0%, #ffffffee 100%);
}
.header-search-form {
- width: 100%;
+ width: 100%;
}
.header-search-input-wrap {
- position: relative;
- display: flex;
- align-items: center;
+ position: relative;
+ display: flex;
+ align-items: center;
}
.header-search-icon {
- position: absolute;
- left: var(--space-s);
- width: 18px;
- height: 18px;
- stroke: var(--accent-primary);
- pointer-events: none;
+ position: absolute;
+ left: var(--space-s);
+ width: 18px;
+ height: 18px;
+ stroke: var(--accent-primary);
+ pointer-events: none;
}
.header-search-input-wrap input {
- width: 100%;
- padding: var(--space-2xs) var(--space-s) !important;
- padding-left: calc(18px + var(--space-l)) !important;
- border: 1px solid var(--accent-primary) !important;
- border-radius: var(--radius) !important;
- background: var(--bg-primary) !important;
- font-size: var(--step-0) !important;
- color: var(--text-primary) !important;
- font-family: inherit !important;
+ width: 100%;
+ padding: var(--space-2xs) var(--space-s) !important;
+ padding-left: calc(18px + var(--space-l)) !important;
+ border: 1px solid var(--accent-primary) !important;
+ border-radius: var(--radius) !important;
+ background: var(--bg-primary) !important;
+ font-size: var(--step-0) !important;
+ color: var(--text-primary) !important;
+ font-family: inherit !important;
}
.header-search-input-wrap input::placeholder {
- color: var(--accent-primary) !important;
+ color: var(--accent-primary) !important;
}
/* ============================================================
@@ -351,355 +354,361 @@ main * {
/* Visually-hidden but screen-reader-accessible */
.sr-only {
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border: 0;
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
}
/* Skip-to-content link (visible only on keyboard focus) */
.skip-link {
- position: absolute;
- top: -999px;
- left: 1rem;
- z-index: 9999;
- padding: var(--space-2xs) var(--space-s);
- background: var(--accent-primary);
- color: var(--text-primary);
- font-size: var(--step--1);
- font-weight: 600;
- text-decoration: none;
- border-radius: 0 0 4px 4px;
+ position: absolute;
+ top: -999px;
+ left: 1rem;
+ z-index: 9999;
+ padding: var(--space-2xs) var(--space-s);
+ background: var(--accent-primary);
+ color: var(--text-primary);
+ font-size: var(--step--1);
+ font-weight: 600;
+ text-decoration: none;
+ border-radius: 0 0 4px 4px;
}
.skip-link:focus {
- top: 0;
+ top: 0;
}
/* Consistent keyboard-focus ring for all interactive elements */
:focus-visible {
- outline: none;
- box-shadow: 0 0 0 2px var(--accent-primary);
+ outline: none;
+ box-shadow: 0 0 0 2px var(--accent-primary);
+ border-radius: var(--radius);
+ padding: var(--space-3xs) var(--space-xs);
}
/* Respect user motion preferences */
@media (prefers-reduced-motion: reduce) {
- *,
- *::before,
- *::after {
- transition-duration: 0.01ms !important;
- animation-duration: 0.01ms !important;
- }
+ *,
+ *::before,
+ *::after {
+ transition-duration: 0.01ms !important;
+ animation-duration: 0.01ms !important;
+ }
}
/* ============================================================
- FORM ELEMENTS — base input / select / textarea / button
+ FORM ELEMENTS - base input / select / textarea / button
============================================================ */
label {
- display: block;
- margin-bottom: var(--space-3xs);
+ display: block;
+ margin-bottom: var(--space-3xs);
}
-input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="color"]),
+input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not(
+ [type="hidden"]
+):not([type="submit"]):not([type="button"]):not([type="reset"]):not(
+ [type="color"]
+),
select,
textarea {
- font-family: inherit;
- font-size: var(--step--1);
- padding: var(--space-2xs) var(--space-xs);
- border: 1px solid var(--border-primary);
- border-radius: var(--radius);
- background: transparent;
- color: var(--text-primary);
- transition: border-color 0.15s;
+ font-family: inherit;
+ font-size: var(--step--1);
+ padding: var(--space-2xs) var(--space-xs);
+ border: 1px solid var(--border-primary);
+ border-radius: var(--radius);
+ background: transparent;
+ color: var(--text-primary);
+ transition: border-color 0.15s;
}
-input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="color"]):focus,
+input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not(
+ [type="hidden"]
+):not([type="submit"]):not([type="button"]):not([type="reset"]):not(
+ [type="color"]
+):focus,
select:focus,
textarea:focus {
- outline: none;
- border: 2px solid var(--accent-primary);
+ outline: none;
+ border: 2px solid var(--accent-primary);
}
input::placeholder,
textarea::placeholder {
- color: var(--text-tertiary);
- font-size: var(--step--1);
+ color: var(--text-tertiary);
+ font-size: var(--step--1);
}
textarea {
- resize: vertical;
- min-height: 80px;
- line-height: 1.5;
+ resize: vertical;
+ min-height: 80px;
+ line-height: 1.5;
}
select {
- cursor: pointer;
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23999' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
- background-repeat: no-repeat;
- background-position: right 0.5rem center;
- padding-right: 1.5rem;
- -webkit-appearance: none;
- appearance: none;
+ cursor: pointer;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23999' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 0.5rem center;
+ padding-right: 1.5rem;
+ -webkit-appearance: none;
+ appearance: none;
}
/* ============================================================
- BUTTONS — shared .btn base class
+ BUTTONS - shared .btn base class
Targets both and