diff --git a/.gitignore b/.gitignore
index 3a8d141..ca408e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,6 +49,9 @@ Thumbs.db
/node_modules
+# Build output
+app/public/assets/dist/
+
# PHPStan cache
.phpstan.result.cache
diff --git a/TODO.md b/TODO.md
index c7a3cde..b556239 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,14 +1,25 @@
# TODO
> Last updated: 2026-06-24
-> Context: Inline JS/CSS + gzip analysis (see docs/ANALYSIS_INLINE_JS_CSS_MINIFY.md)
+> Context: Setup biome + rolldown + lightningcss build pipeline for JS/CSS bundling & minification
+
+## Completed
+- [x] #build-pipeline Setup biome + rolldown + lightningcss build pipeline ✓
+ - [x] #build-packagejson Create package.json with devDependencies ✓
+ - [x] #build-biomecss Update biome.json to handle CSS formatting ✓
+ - [x] #build-rolldown-config Create rolldown.config.mjs + build-js.mjs for JS bundling ✓
+ - [x] #build-lightningcss Add lightningcss CSS bundling (resolve @import chain) ✓
+ - [x] #build-justfile Add just build/deploy recipes + integrate build into deploy ✓
+ - [x] #build-head Update head.php + form-page.php + controllers to use bundled assets ✓
+ - [x] #build-gitignore Add dist/ to .gitignore ✓
+ - [x] #build-cssfix Fix stray `}` syntax error in admin.css line 305 ✓
## Pending
- [ ] #rep-student-touch Replace hover student popover with tap-to-open drawer for mobile `(repertoire.php, repertoire.css)`
- [ ] #rep-polish Polish: scroll-position memory on HTMX swap, animation tuning `(repertoire.css)`
- [ ] #icon-color-verify Verify icon colors render correctly across all pages (header, admin tables, forms, dialogs, cleanup modal)
-## Completed
+## Completed (before this session)
- [x] #gzip-nginx Enable gzip compression in nginx config `(nginx/xamxam.conf)` ✓
- [x] #extract-inline-js Move inline JS to external files across 17 templates → 15 new JS files created `(app/public/assets/js/app/*.js)` ✓
- [x] #inline-icon-helper Create `icon()` PHP helper + auto-load in bootstrap `(src/icon.php, bootstrap.php)` ✓
@@ -63,7 +74,4 @@
- [x] #extra-css-admin Update `head.php` to support `$extraCssAdmin` for admin-only stylesheets `(head.php)` ✓
## Deferred / Blocked
-- [ ] #minify-js Minify custom JS files (post-extraction, ~1,763 lines across 9 files)
-- [ ] #bundle-css Bundle CSS to eliminate @import waterfall (18 files, ~6,200 lines)
-- [ ] #build-step Add build step (justfile commands) for JS minification + CSS bundling
- [ ] #tighten-csp Tighten CSP to remove 'unsafe-inline' after inline JS extraction
diff --git a/app/public/admin/index.php b/app/public/admin/index.php
index dff438f..4064de9 100644
--- a/app/public/admin/index.php
+++ b/app/public/admin/index.php
@@ -514,8 +514,8 @@ if ($isHtmx) {
include APP_ROOT . '/templates/admin/index-table.php';
}
} else {
- $extraCssAdmin = ['/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css'];
- $extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/js/app/file-upload-filepond.js'];
+ $extraCssAdmin = [];
+ $extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/dist/admin.min.js'];
require_once APP_ROOT . '/templates/head.php';
include APP_ROOT . '/templates/header.php';
if ($tab === 'trash') {
diff --git a/app/public/admin/parametres.php b/app/public/admin/parametres.php
index 8b0bb47..583eaa4 100644
--- a/app/public/admin/parametres.php
+++ b/app/public/admin/parametres.php
@@ -74,7 +74,7 @@ if (empty($_SESSION['csrf_token'])) {
}
$isAdmin = true; $bodyClass = 'admin-body';
-$extraCssAdmin = ['/assets/css/system.css'];
+$extraCssAdmin = ['/assets/dist/system.min.css'];
require_once APP_ROOT . '/templates/head.php';
include APP_ROOT . '/templates/header.php';
include APP_ROOT . '/templates/admin/parametres.php';
diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css
index 6d131b1..8c94190 100644
--- a/app/public/assets/css/admin.css
+++ b/app/public/assets/css/admin.css
@@ -302,7 +302,6 @@
font-weight: 500;
white-space: nowrap;
}
-}
/* ── Table ──────────────────────────────────────────────────────────────── */
/* Base table/th/td styles live in components/tables.css */
diff --git a/app/public/assets/js/app/admin-entry.js b/app/public/assets/js/app/admin-entry.js
new file mode 100644
index 0000000..ccadb85
--- /dev/null
+++ b/app/public/assets/js/app/admin-entry.js
@@ -0,0 +1,28 @@
+/**
+ * Admin JS entry — all JS needed on admin pages.
+ *
+ * Import order matters for dependencies:
+ * - htmx-global-setup must come first (HTMX event listeners)
+ * - Vendor scripts (htmx, FilePond) are loaded separately via
-
+