diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index b5c7695..d3c44ed 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -2,22 +2,22 @@ import { Navigate, Route, Routes } from "react-router-dom";
import { RedirectIfAuthenticated } from "./features/auth/RedirectIfAuthenticated";
import { RequireAuth } from "./features/auth/RequireAuth";
import { AppLayout } from "./layouts/AppLayout";
-import { ImportRecipePage } from "./pages/ImportRecipePage";
-import { LoginPage } from "./pages/LoginPage";
+import { LoginPage } from "./pages/auth/LoginPage";
+import { SignupPage } from "./pages/auth/SignupPage";
import { OnboardingAllergensPage } from "./pages/onboarding/OnboardingAllergensPage";
import { OnboardingDietPage } from "./pages/onboarding/OnboardingDietPage";
import { OnboardingHouseholdPage } from "./pages/onboarding/OnboardingHouseholdPage";
import { OnboardingSourcesPage } from "./pages/onboarding/OnboardingSourcesPage";
-import { PlanningPage } from "./pages/PlanningPage";
-import { RecipeFormPage } from "./pages/RecipeFormPage";
-import { RecipesPage } from "./pages/RecipesPage";
-import { ShoppingListPage } from "./pages/ShoppingListPage";
-import { SignupPage } from "./pages/SignupPage";
+import { PlanningPage } from "./pages/planning/PlanningPage";
+import { ImportRecipePage } from "./pages/recipes/ImportRecipePage";
+import { RecipeFormPage } from "./pages/recipes/RecipeFormPage";
+import { RecipesPage } from "./pages/recipes/RecipesPage";
import { AccountSettingsPage } from "./pages/settings/AccountSettingsPage";
import { CreditsPage } from "./pages/settings/CreditsPage";
import { HouseholdSettingsPage } from "./pages/settings/HouseholdSettingsPage";
import { PreferencesPage } from "./pages/settings/PreferencesPage";
import { UserPreferencesPage } from "./pages/settings/UserPreferencesPage";
+import { ShoppingListPage } from "./pages/shopping-list/ShoppingListPage";
/**
* Top-level route table. Every authenticated section is nested under one
diff --git a/apps/web/src/pages/ComingSoonPage.scss b/apps/web/src/components/ui/ComingSoonPage.scss
similarity index 100%
rename from apps/web/src/pages/ComingSoonPage.scss
rename to apps/web/src/components/ui/ComingSoonPage.scss
diff --git a/apps/web/src/components/ui/ComingSoonPage.tsx b/apps/web/src/components/ui/ComingSoonPage.tsx
new file mode 100644
index 0000000..bebebf9
--- /dev/null
+++ b/apps/web/src/components/ui/ComingSoonPage.tsx
@@ -0,0 +1,26 @@
+import "./ComingSoonPage.scss";
+
+interface ComingSoonPageProps {
+ title: string;
+ description: string;
+}
+
+/**
+ * Placeholder rendered by a section that has a route/sidebar entry but no
+ * real feature behind it yet — today only `pages/shopping-list/ShoppingListPage.tsx`
+ * (`Recettes`/`Foyer & profil` both grew real backends since this was
+ * written, see `pages/recipes/`/`pages/settings/`). Kept as a shared,
+ * reusable component (`components/ui/`, not itself a routed page) rather
+ * than inlined into that one page, so a future stub section doesn't need to
+ * hand-roll the same markup — the page that needs it still gets its own
+ * file (and its own copy, via i18n), just wrapping this instead of
+ * rewriting it.
+ */
+export function ComingSoonPage({ title, description }: ComingSoonPageProps) {
+ return (
+
+
{title}
+
{description}
+
+ );
+}
diff --git a/apps/web/src/pages/ComingSoonPage.tsx b/apps/web/src/pages/ComingSoonPage.tsx
deleted file mode 100644
index 0d5a5df..0000000
--- a/apps/web/src/pages/ComingSoonPage.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import "./ComingSoonPage.scss";
-
-interface ComingSoonPageProps {
- title: string;
- description: string;
-}
-
-/**
- * Placeholder rendered by every section that has a route/sidebar entry but
- * no real feature behind it yet (Recettes, Liste de courses, Foyer &
- * profil — see `RecipesPage.tsx` etc.). One shared component instead of
- * three near-identical markup blocks; each page still gets its own file
- * (and its own copy, via i18n) so building out a real feature later means
- * rewriting one dedicated file, not splitting a generic route.
- */
-export function ComingSoonPage({ title, description }: ComingSoonPageProps) {
- return (
-
-
{title}
-
{description}
-
- );
-}
diff --git a/apps/web/src/pages/LoginPage.tsx b/apps/web/src/pages/auth/LoginPage.tsx
similarity index 92%
rename from apps/web/src/pages/LoginPage.tsx
rename to apps/web/src/pages/auth/LoginPage.tsx
index 8961f6e..aba3a37 100644
--- a/apps/web/src/pages/LoginPage.tsx
+++ b/apps/web/src/pages/auth/LoginPage.tsx
@@ -2,13 +2,13 @@ import { ErrorCode, loginSchema } from "@batch-cooking/shared";
import { type FormEvent, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
-import { ApiError } from "../api/client";
-import { useAuth } from "../features/auth/AuthContext";
+import { ApiError } from "../../api/client";
+import { useAuth } from "../../features/auth/AuthContext";
// Shared with SignupPage — see the file for why it's colocated in
// features/auth/ rather than duplicated per page.
-import "../features/auth/auth-form.scss";
-import { fieldErrorsFrom } from "../lib/zod-errors";
-import { errorMessageService } from "../services/error-message.service";
+import "../../features/auth/auth-form.scss";
+import { fieldErrorsFrom } from "../../lib/zod-errors";
+import { errorMessageService } from "../../services/error-message.service";
/**
* Login form. Validates client-side first (via the shared `loginSchema`,
diff --git a/apps/web/src/pages/SignupPage.tsx b/apps/web/src/pages/auth/SignupPage.tsx
similarity index 94%
rename from apps/web/src/pages/SignupPage.tsx
rename to apps/web/src/pages/auth/SignupPage.tsx
index 8c481db..8335230 100644
--- a/apps/web/src/pages/SignupPage.tsx
+++ b/apps/web/src/pages/auth/SignupPage.tsx
@@ -2,13 +2,13 @@ import { ErrorCode, signupSchema } from "@batch-cooking/shared";
import { type FormEvent, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
-import { ApiError } from "../api/client";
-import { useAuth } from "../features/auth/AuthContext";
+import { ApiError } from "../../api/client";
+import { useAuth } from "../../features/auth/AuthContext";
// Shared with LoginPage — see the file for why it's colocated in
// features/auth/ rather than duplicated per page.
-import "../features/auth/auth-form.scss";
-import { fieldErrorsFrom } from "../lib/zod-errors";
-import { errorMessageService } from "../services/error-message.service";
+import "../../features/auth/auth-form.scss";
+import { fieldErrorsFrom } from "../../lib/zod-errors";
+import { errorMessageService } from "../../services/error-message.service";
/**
* Signup form (profile creation). Validates client-side first (via the
diff --git a/apps/web/src/pages/PlanningPage.tsx b/apps/web/src/pages/planning/PlanningPage.tsx
similarity index 98%
rename from apps/web/src/pages/PlanningPage.tsx
rename to apps/web/src/pages/planning/PlanningPage.tsx
index e9e082e..7b59c41 100644
--- a/apps/web/src/pages/PlanningPage.tsx
+++ b/apps/web/src/pages/planning/PlanningPage.tsx
@@ -15,8 +15,8 @@ import {
} from "@batch-cooking/shared";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
-import { apiClient } from "../api/client";
-import { type PlanningSlot, RecipePickerDialog } from "../features/planning/RecipePickerDialog";
+import { apiClient } from "../../api/client";
+import { type PlanningSlot, RecipePickerDialog } from "../../features/planning/RecipePickerDialog";
import "./planning-page.scss";
/** Load state for the `GET /planning` call — a discriminated union so a stale/impossible combination (e.g. "loading" with data) can't be represented. */
diff --git a/apps/web/src/pages/planning-page.scss b/apps/web/src/pages/planning/planning-page.scss
similarity index 100%
rename from apps/web/src/pages/planning-page.scss
rename to apps/web/src/pages/planning/planning-page.scss
diff --git a/apps/web/src/pages/ImportRecipePage.tsx b/apps/web/src/pages/recipes/ImportRecipePage.tsx
similarity index 96%
rename from apps/web/src/pages/ImportRecipePage.tsx
rename to apps/web/src/pages/recipes/ImportRecipePage.tsx
index 6314c9c..51e5689 100644
--- a/apps/web/src/pages/ImportRecipePage.tsx
+++ b/apps/web/src/pages/recipes/ImportRecipePage.tsx
@@ -1,8 +1,8 @@
import { MEALS, type Meal, WEEK_DAYS, type WeekDay } from "@batch-cooking/shared";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
-import { RecipeImportForm } from "../features/recipes/RecipeImportForm";
-import "../features/recipes/recipes.scss";
+import { RecipeImportForm } from "../../features/recipes/RecipeImportForm";
+import "../../features/recipes/recipes.scss";
/**
* Reads and validates `?planningDate=&planningWeekDay=&planningMeal=` off
diff --git a/apps/web/src/pages/RecipeFormPage.tsx b/apps/web/src/pages/recipes/RecipeFormPage.tsx
similarity index 95%
rename from apps/web/src/pages/RecipeFormPage.tsx
rename to apps/web/src/pages/recipes/RecipeFormPage.tsx
index 66de3cd..8a13fb1 100644
--- a/apps/web/src/pages/RecipeFormPage.tsx
+++ b/apps/web/src/pages/recipes/RecipeFormPage.tsx
@@ -10,14 +10,14 @@ import {
import { type FormEvent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
-import { ApiError, apiClient } from "../api/client";
-import { DietTagSelect } from "../features/recipes/DietTagSelect";
-import { IngredientPicker } from "../features/recipes/IngredientPicker";
-import { IngredientRow } from "../features/recipes/IngredientRow";
-import { type StepDraft, StepListEditor } from "../features/recipes/StepListEditor";
-import "../features/recipes/recipes.scss";
-import { makeClientKey } from "../lib/client-key";
-import { errorMessageService } from "../services/error-message.service";
+import { ApiError, apiClient } from "../../api/client";
+import { DietTagSelect } from "../../features/recipes/DietTagSelect";
+import { IngredientPicker } from "../../features/recipes/IngredientPicker";
+import { IngredientRow } from "../../features/recipes/IngredientRow";
+import { type StepDraft, StepListEditor } from "../../features/recipes/StepListEditor";
+import "../../features/recipes/recipes.scss";
+import { makeClientKey } from "../../lib/client-key";
+import { errorMessageService } from "../../services/error-message.service";
/** In display order — mirrors `RecipeVisibility` (schema.prisma/shared types). */
const VISIBILITY_OPTIONS: RecipeVisibility[] = ["PERSONAL", "HOUSE", "PUBLIC"];
diff --git a/apps/web/src/pages/RecipesPage.tsx b/apps/web/src/pages/recipes/RecipesPage.tsx
similarity index 95%
rename from apps/web/src/pages/RecipesPage.tsx
rename to apps/web/src/pages/recipes/RecipesPage.tsx
index 7d963b2..0a963b9 100644
--- a/apps/web/src/pages/RecipesPage.tsx
+++ b/apps/web/src/pages/recipes/RecipesPage.tsx
@@ -2,22 +2,25 @@ import { ErrorCode, type RecipeSummaryView } from "@batch-cooking/shared";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
-import { ApiError, apiClient } from "../api/client";
-import { RecipeDetailPanel, type RecipeDetailState } from "../features/recipes/RecipeDetailPanel";
+import { ApiError, apiClient } from "../../api/client";
+import {
+ RecipeDetailPanel,
+ type RecipeDetailState,
+} from "../../features/recipes/RecipeDetailPanel";
import {
RecipeSourcesPanel,
type SourceItemSelection,
-} from "../features/recipes/RecipeSourcesPanel";
-import { RecipeTable } from "../features/recipes/RecipeTable";
+} from "../../features/recipes/RecipeSourcesPanel";
+import { RecipeTable } from "../../features/recipes/RecipeTable";
import {
isSourceTab,
parseSourceTabValue,
type RecipesPageTab,
RecipeTabs,
sourceTabValue,
-} from "../features/recipes/RecipeTabs";
-import { useEnabledSources } from "../features/recipes/useEnabledSources";
-import "../features/recipes/recipes.scss";
+} from "../../features/recipes/RecipeTabs";
+import { useEnabledSources } from "../../features/recipes/useEnabledSources";
+import "../../features/recipes/recipes.scss";
/** Debounce for the search field — avoids firing a request on every keystroke, same idea as the household name's autosave (`HouseholdSettingsPage`). */
const SEARCH_DEBOUNCE_MS = 300;
diff --git a/apps/web/src/pages/ShoppingListPage.tsx b/apps/web/src/pages/shopping-list/ShoppingListPage.tsx
similarity index 82%
rename from apps/web/src/pages/ShoppingListPage.tsx
rename to apps/web/src/pages/shopping-list/ShoppingListPage.tsx
index 090be39..5d76b17 100644
--- a/apps/web/src/pages/ShoppingListPage.tsx
+++ b/apps/web/src/pages/shopping-list/ShoppingListPage.tsx
@@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next";
-import { ComingSoonPage } from "./ComingSoonPage";
+import { ComingSoonPage } from "../../components/ui/ComingSoonPage";
/** Shopping list section — routed at `/liste-de-courses`. No backend yet, stub for now. */
export function ShoppingListPage() {
diff --git a/specs/frontend-architecture.md b/specs/frontend-architecture.md
index 1e2020d..560448d 100644
--- a/specs/frontend-architecture.md
+++ b/specs/frontend-architecture.md
@@ -21,7 +21,8 @@ apps/web/src/
│ └── ui/ # primitives réutilisables partout, voir plus bas
│ ├── Dialog.tsx + dialog.scss # modale (élément