diff --git a/docs/superpowers/plans/2026-08-10-minimalist-ui.md b/docs/superpowers/plans/2026-08-10-minimalist-ui.md
new file mode 100644
index 0000000..5e9d447
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-10-minimalist-ui.md
@@ -0,0 +1,1291 @@
+# Ombrora Minimalist UI -- Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Restyle the Ombrora frontend with Tailwind CSS v4, light/dark mode, flag-image language switcher, SEO-optimized markup, and a hero + reassurance layout.
+
+**Architecture:** `src/app/[locale]/layout.tsx` is the real shell (renders ``/`
`); the root `src/app/layout.tsx` just passes children through. All styling goes through a single `src/app/globals.css` imported in the locale layout. next-themes wraps the tree and toggles a `.dark` class on ``.
+
+**Tech Stack:** Next.js 16 App Router, Tailwind CSS v4 (`@tailwindcss/postcss`), next-themes, `next/font/google` (Inter), next-intl v4.
+
+## Global Constraints
+
+- Tailwind v4 only -- no `tailwind.config.ts`, config lives in CSS (`@custom-variant dark`)
+- `npm install -D` for tailwindcss + @tailwindcss/postcss (build-time, not runtime)
+- `next-themes` pure JS -- fine for o2switch
+- Flag SVGs stored in `/public/flags/` -- no CDN runtime calls
+- Inter font via `next/font/google` with `subsets: ['latin', 'latin-ext']` -- covers FR/ES/IT accents
+- `NEXT_PUBLIC_BASE_URL` env var for canonical URLs -- set in `.env.local`
+- No external icon library -- SVG paths are inlined
+- Never touch files in `src/app/api/`, `src/lib/`, `worker/`, or any test file
+- `src/app/layout.tsx` (root) -- do not modify, it just returns children
+- `src/app/page.tsx` (non-locale) -- do not modify
+
+## File Map
+
+```
+NEW
+ postcss.config.mjs
+ src/app/globals.css
+ src/components/ThemeToggle.tsx
+ src/components/Header.tsx
+ src/components/ReassuranceSection.tsx
+ public/flags/en.svg
+ public/flags/fr.svg
+ public/flags/es.svg
+ public/flags/it.svg
+
+MODIFIED
+ messages/en.json
+ messages/fr.json
+ messages/es.json
+ messages/it.json
+ src/app/[locale]/layout.tsx
+ src/app/[locale]/page.tsx
+ src/app/[locale]/status/[uuid]/page.tsx
+ src/components/SubmitForm.tsx
+ src/components/StatusView.tsx
+ src/components/LanguageSwitcher.tsx
+```
+
+---
+
+### Task 1: Tailwind CSS v4 setup
+
+**Files:**
+- Create: `postcss.config.mjs`
+- Create: `src/app/globals.css`
+- Modify: `.env.local` (create if absent)
+- DevDependencies: `tailwindcss`, `@tailwindcss/postcss`
+
+**Interfaces:**
+- Produces: Tailwind utility classes available in all TSX files; `dark:` variant active when `` has class `dark`
+
+- [ ] **Step 1: Install Tailwind**
+
+```bash
+npm install -D tailwindcss @tailwindcss/postcss
+```
+
+Expected: `tailwindcss` and `@tailwindcss/postcss` appear in `devDependencies` in `package.json`.
+
+- [ ] **Step 2: Create PostCSS config**
+
+Create `postcss.config.mjs` at the repo root:
+
+```js
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+}
+```
+
+- [ ] **Step 3: Create globals.css**
+
+Create `src/app/globals.css`:
+
+```css
+@import 'tailwindcss';
+
+/* Dark mode: activated when any ancestor has the .dark class (set by next-themes) */
+@custom-variant dark (&:where(.dark, .dark *));
+```
+
+- [ ] **Step 4: Add NEXT_PUBLIC_BASE_URL to .env.local**
+
+Add to `.env.local` (create the file if it does not exist):
+
+```
+NEXT_PUBLIC_BASE_URL=http://localhost:3000
+```
+
+(Set the production value before deploying to o2switch.)
+
+- [ ] **Step 5: Smoke-test**
+
+```bash
+npm run dev
+```
+
+Open `http://localhost:3000/fr`. Add `className="text-violet-600"` temporarily to any element in `src/app/[locale]/page.tsx` and verify the element turns violet in the browser. Remove the temporary class.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add postcss.config.mjs src/app/globals.css .env.local package.json package-lock.json
+git commit -m "feat: install Tailwind CSS v4 with PostCSS"
+```
+
+---
+
+### Task 2: Update translation files (all 4 locales)
+
+**Files:**
+- Modify: `messages/en.json`, `messages/fr.json`, `messages/es.json`, `messages/it.json`
+
+**Interfaces:**
+- Produces: new keys `meta.description`, `home.subheading`, `home.formats`, `reassurance.*`, `footer.copyright`
+- Consumed by: Tasks 7, 8, 9, 10 (layout, SubmitForm, ReassuranceSection, home page)
+
+- [ ] **Step 1: Replace messages/en.json**
+
+```json
+{
+ "meta": {
+ "title": "Ombrora -- Free Video Downloader",
+ "description": "Download YouTube, Vimeo, and more in mp4, mp3, webm -- free, fast, no account required."
+ },
+ "home": {
+ "heading": "Download your videos for free",
+ "subheading": "Free, fast, no account required",
+ "urlLabel": "Video URL",
+ "format": "Format",
+ "quality": "Quality",
+ "subtitles": "Subtitles (fr, en)",
+ "advanced": "Advanced options",
+ "formats": "mp4 · mp3 · webm · mkv",
+ "submit": "Download",
+ "submitting": "Sending...",
+ "errorRateLimit": "Too many submissions. Try again in an hour.",
+ "errorGeneric": "Submission error."
+ },
+ "status": {
+ "heading": "Download status",
+ "notFound": "Download not found.",
+ "loading": "Loading...",
+ "pending": "Pending",
+ "processing": "Processing...",
+ "polling": "This page refreshes automatically every 5 seconds.",
+ "ready": "Download ready",
+ "download": "Download",
+ "linkExpires": "Link valid until",
+ "failed": "Download failed.",
+ "deleted": "The file has expired and was deleted. The download is no longer available.",
+ "unknown": "Unknown status"
+ },
+ "reassurance": {
+ "free": "Free",
+ "freeDesc": "No account, no sign-up",
+ "fast": "Fast",
+ "fastDesc": "Ready in seconds",
+ "private": "Private",
+ "privateDesc": "No data retained",
+ "formats": "Multi-format",
+ "formatsDesc": "mp4, mp3, webm, mkv -- up to 1080p"
+ },
+ "footer": {
+ "copyright": "© 2026 Ombrora"
+ }
+}
+```
+
+- [ ] **Step 2: Replace messages/fr.json**
+
+```json
+{
+ "meta": {
+ "title": "Ombrora -- Telechargeur de videos gratuit",
+ "description": "Telechargez vos videos YouTube, Vimeo et plus en mp4, mp3, webm -- gratuit, rapide, sans compte."
+ },
+ "home": {
+ "heading": "Telechargez vos videos gratuitement",
+ "subheading": "Gratuit, rapide, sans compte",
+ "urlLabel": "URL de la video",
+ "format": "Format",
+ "quality": "Qualite",
+ "subtitles": "Sous-titres (fr, en)",
+ "advanced": "Options avancees",
+ "formats": "mp4 · mp3 · webm · mkv",
+ "submit": "Telecharger",
+ "submitting": "Envoi en cours...",
+ "errorRateLimit": "Trop de soumissions. Reessayez dans une heure.",
+ "errorGeneric": "Erreur de soumission."
+ },
+ "status": {
+ "heading": "Statut du telechargement",
+ "notFound": "Telechargement introuvable.",
+ "loading": "Chargement...",
+ "pending": "En attente",
+ "processing": "En cours de traitement...",
+ "polling": "Cette page se rafraichit automatiquement toutes les 5 secondes.",
+ "ready": "Telechargement pret",
+ "download": "Telecharger",
+ "linkExpires": "Lien valide jusqu'au",
+ "failed": "Echec du telechargement.",
+ "deleted": "Le fichier a expire et a ete supprime. Le telechargement n'est plus disponible.",
+ "unknown": "Statut inconnu"
+ },
+ "reassurance": {
+ "free": "Gratuit",
+ "freeDesc": "Sans compte, sans inscription",
+ "fast": "Rapide",
+ "fastDesc": "Pret en quelques secondes",
+ "private": "Prive",
+ "privateDesc": "Aucune donnee conservee",
+ "formats": "Multi-format",
+ "formatsDesc": "mp4, mp3, webm, mkv -- jusqu'en 1080p"
+ },
+ "footer": {
+ "copyright": "© 2026 Ombrora"
+ }
+}
+```
+
+- [ ] **Step 3: Replace messages/es.json**
+
+```json
+{
+ "meta": {
+ "title": "Ombrora -- Descargador de videos gratuito",
+ "description": "Descarga videos de YouTube, Vimeo y mas en mp4, mp3, webm -- gratis, rapido, sin cuenta."
+ },
+ "home": {
+ "heading": "Descarga tus videos gratis",
+ "subheading": "Gratis, rapido, sin cuenta",
+ "urlLabel": "URL del video",
+ "format": "Formato",
+ "quality": "Calidad",
+ "subtitles": "Subtitulos (fr, en)",
+ "advanced": "Opciones avanzadas",
+ "formats": "mp4 · mp3 · webm · mkv",
+ "submit": "Descargar",
+ "submitting": "Enviando...",
+ "errorRateLimit": "Demasiadas solicitudes. Intentalo de nuevo en una hora.",
+ "errorGeneric": "Error de envio."
+ },
+ "status": {
+ "heading": "Estado de la descarga",
+ "notFound": "Descarga no encontrada.",
+ "loading": "Cargando...",
+ "pending": "Pendiente",
+ "processing": "Procesando...",
+ "polling": "Esta pagina se actualiza automaticamente cada 5 segundos.",
+ "ready": "Descarga lista",
+ "download": "Descargar",
+ "linkExpires": "Enlace valido hasta",
+ "failed": "Error de descarga.",
+ "deleted": "El archivo ha expirado y fue eliminado. La descarga ya no esta disponible.",
+ "unknown": "Estado desconocido"
+ },
+ "reassurance": {
+ "free": "Gratuito",
+ "freeDesc": "Sin cuenta, sin registro",
+ "fast": "Rapido",
+ "fastDesc": "Listo en segundos",
+ "private": "Privado",
+ "privateDesc": "Sin datos retenidos",
+ "formats": "Multi-formato",
+ "formatsDesc": "mp4, mp3, webm, mkv -- hasta 1080p"
+ },
+ "footer": {
+ "copyright": "© 2026 Ombrora"
+ }
+}
+```
+
+- [ ] **Step 4: Replace messages/it.json**
+
+```json
+{
+ "meta": {
+ "title": "Ombrora -- Scaricatore di video gratuito",
+ "description": "Scarica video da YouTube, Vimeo e altro in mp4, mp3, webm -- gratis, veloce, senza account."
+ },
+ "home": {
+ "heading": "Scarica i tuoi video gratuitamente",
+ "subheading": "Gratuito, veloce, senza account",
+ "urlLabel": "URL del video",
+ "format": "Formato",
+ "quality": "Qualita",
+ "subtitles": "Sottotitoli (fr, en)",
+ "advanced": "Opzioni avanzate",
+ "formats": "mp4 · mp3 · webm · mkv",
+ "submit": "Scarica",
+ "submitting": "Invio in corso...",
+ "errorRateLimit": "Troppe richieste. Riprova tra un'ora.",
+ "errorGeneric": "Errore di invio."
+ },
+ "status": {
+ "heading": "Stato del download",
+ "notFound": "Download non trovato.",
+ "loading": "Caricamento...",
+ "pending": "In attesa",
+ "processing": "In elaborazione...",
+ "polling": "Questa pagina si aggiorna automaticamente ogni 5 secondi.",
+ "ready": "Download pronto",
+ "download": "Scarica",
+ "linkExpires": "Link valido fino al",
+ "failed": "Download fallito.",
+ "deleted": "Il file e scaduto ed e stato eliminato. Il download non e piu disponibile.",
+ "unknown": "Stato sconosciuto"
+ },
+ "reassurance": {
+ "free": "Gratuito",
+ "freeDesc": "Senza account, senza registrazione",
+ "fast": "Veloce",
+ "fastDesc": "Pronto in pochi secondi",
+ "private": "Privato",
+ "privateDesc": "Nessun dato conservato",
+ "formats": "Multi-formato",
+ "formatsDesc": "mp4, mp3, webm, mkv -- fino a 1080p"
+ },
+ "footer": {
+ "copyright": "© 2026 Ombrora"
+ }
+}
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add messages/
+git commit -m "feat: add SEO, reassurance, and subheading translation keys"
+```
+
+---
+
+### Task 3: Download flag SVGs
+
+**Files:**
+- Create: `public/flags/en.svg`, `public/flags/fr.svg`, `public/flags/es.svg`, `public/flags/it.svg`
+
+**Interfaces:**
+- Produces: static SVG files served at `/flags/{locale}.svg`
+- Consumed by: Task 5 (LanguageSwitcher)
+
+- [ ] **Step 1: Create the flags directory and download SVGs**
+
+Run in PowerShell from the repo root:
+
+```powershell
+New-Item -ItemType Directory -Force -Path public/flags
+Invoke-WebRequest -Uri "https://flagcdn.com/us.svg" -OutFile "public/flags/en.svg"
+Invoke-WebRequest -Uri "https://flagcdn.com/fr.svg" -OutFile "public/flags/fr.svg"
+Invoke-WebRequest -Uri "https://flagcdn.com/es.svg" -OutFile "public/flags/es.svg"
+Invoke-WebRequest -Uri "https://flagcdn.com/it.svg" -OutFile "public/flags/it.svg"
+```
+
+Expected: 4 SVG files appear under `public/flags/`. Each file starts with `
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function MoonIcon() {
+ return (
+
+
+
+ )
+}
+
+export function ThemeToggle() {
+ const { theme, setTheme } = useTheme()
+ const [mounted, setMounted] = useState(false)
+
+ useEffect(() => { setMounted(true) }, [])
+
+ if (!mounted) {
+ // Prevents hydration mismatch -- renders a same-size placeholder
+ return
+ }
+
+ return (
+ setTheme(theme === 'dark' ? 'light' : 'dark')}
+ aria-label="Basculer le theme"
+ className="rounded-lg p-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-800 transition-colors"
+ >
+ {theme === 'dark' ? : }
+
+ )
+}
+```
+
+- [ ] **Step 3: Verify the component compiles**
+
+```bash
+npx tsc --noEmit
+```
+
+Expected: no errors in `ThemeToggle.tsx`.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/components/ThemeToggle.tsx package.json package-lock.json
+git commit -m "feat: add ThemeToggle component with next-themes"
+```
+
+---
+
+### Task 5: Restyle LanguageSwitcher
+
+**Files:**
+- Modify: `src/components/LanguageSwitcher.tsx`
+
+**Interfaces:**
+- Consumes: `/public/flags/{locale}.svg` (from Task 3)
+- Produces: `export function LanguageSwitcher()` -- same signature as before, now uses ` ` flags
+
+- [ ] **Step 1: Rewrite LanguageSwitcher.tsx**
+
+```tsx
+'use client'
+
+import { useLocale } from 'next-intl'
+import { useRouter, usePathname } from '@/navigation'
+import { routing } from '@/i18n/routing'
+
+const FLAG_LABELS: Record = {
+ en: 'English',
+ fr: 'Francais',
+ es: 'Espanol',
+ it: 'Italiano',
+}
+
+export function LanguageSwitcher() {
+ const locale = useLocale()
+ const router = useRouter()
+ const pathname = usePathname()
+
+ return (
+
+ {routing.locales.map((loc) => (
+
router.replace(pathname, { locale: loc })}
+ aria-label={FLAG_LABELS[loc]}
+ className={[
+ 'rounded transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500',
+ loc === locale
+ ? 'opacity-100 ring-2 ring-violet-500 ring-offset-1 ring-offset-white dark:ring-offset-slate-950'
+ : 'opacity-40 hover:opacity-70',
+ ].join(' ')}
+ >
+
+
+ ))}
+
+ )
+}
+```
+
+- [ ] **Step 2: Verify**
+
+```bash
+npx tsc --noEmit
+```
+
+Open `http://localhost:3000/fr`. The header should show 4 flag images. The active flag has a violet ring; others are dimmed.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/components/LanguageSwitcher.tsx
+git commit -m "feat: replace emoji flags with SVG flag images in LanguageSwitcher"
+```
+
+---
+
+### Task 6: Header component
+
+**Files:**
+- Create: `src/components/Header.tsx`
+
+**Interfaces:**
+- Consumes: `ThemeToggle` (Task 4), `LanguageSwitcher` (Task 5)
+- Produces: `export function Header()` -- sticky header with logo, theme toggle, language switcher
+
+- [ ] **Step 1: Create Header.tsx**
+
+```tsx
+import { ThemeToggle } from '@/components/ThemeToggle'
+import { LanguageSwitcher } from '@/components/LanguageSwitcher'
+
+export function Header() {
+ return (
+
+ )
+}
+```
+
+- [ ] **Step 2: Verify**
+
+```bash
+npx tsc --noEmit
+```
+
+Expected: no errors.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/components/Header.tsx
+git commit -m "feat: add Header component with logo, theme toggle, and language switcher"
+```
+
+---
+
+### Task 7: Locale layout update
+
+**Files:**
+- Modify: `src/app/[locale]/layout.tsx`
+
+**Interfaces:**
+- Consumes: `Header` (Task 6), `globals.css` (Task 1), translation keys `meta.*` and `footer.*` (Task 2)
+- Produces: full page shell with Inter font, ThemeProvider, SEO metadata, JSON-LD, Header, footer
+
+- [ ] **Step 1: Rewrite src/app/[locale]/layout.tsx**
+
+```tsx
+import type { Metadata } from 'next'
+import { Inter } from 'next/font/google'
+import { NextIntlClientProvider } from 'next-intl'
+import { getTranslations, getMessages } from 'next-intl/server'
+import { notFound } from 'next/navigation'
+import { ThemeProvider } from 'next-themes'
+import { routing } from '@/i18n/routing'
+import { Header } from '@/components/Header'
+import '../globals.css'
+
+const inter = Inter({ subsets: ['latin', 'latin-ext'] })
+
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ locale: string }>
+}): Promise {
+ const { locale } = await params
+ const t = await getTranslations({ locale, namespace: 'meta' })
+ const baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? ''
+
+ return {
+ title: t('title'),
+ description: t('description'),
+ robots: { index: true, follow: true },
+ openGraph: {
+ title: t('title'),
+ description: t('description'),
+ url: `${baseUrl}/${locale}`,
+ siteName: 'Ombrora',
+ locale,
+ },
+ alternates: {
+ canonical: `${baseUrl}/${locale}`,
+ languages: Object.fromEntries(
+ routing.locales.map((l) => [l, `/${l}`])
+ ),
+ },
+ }
+}
+
+export default async function LocaleLayout({
+ children,
+ params,
+}: {
+ children: React.ReactNode
+ params: Promise<{ locale: string }>
+}) {
+ const { locale } = await params
+
+ if (!(routing.locales as readonly string[]).includes(locale)) {
+ notFound()
+ }
+
+ const messages = await getMessages()
+ const t = await getTranslations({ locale, namespace: 'footer' })
+ const baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? ''
+
+ const jsonLd = {
+ '@context': 'https://schema.org',
+ '@type': 'WebApplication',
+ name: 'Ombrora',
+ url: baseUrl,
+ description: 'Free, fast, private video downloader.',
+ applicationCategory: 'MultimediaApplication',
+ operatingSystem: 'Web',
+ offers: {
+ '@type': 'Offer',
+ price: '0',
+ priceCurrency: 'EUR',
+ },
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+ )
+}
+```
+
+- [ ] **Step 2: Verify build**
+
+```bash
+npx tsc --noEmit
+npm run dev
+```
+
+Open `http://localhost:3000/fr`. You should see:
+- Inter font applied
+- Header with "Ombrora" logo + toggle + flags
+- A footer with "© 2026 Ombrora"
+- Dark mode toggle works (background switches between white and slate-950)
+- Page `` in the browser tab matches the French translation
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/app/[locale]/layout.tsx
+git commit -m "feat: update locale layout with Inter font, ThemeProvider, SEO metadata, JSON-LD, Header, and footer"
+```
+
+---
+
+### Task 8: Restyle SubmitForm
+
+**Files:**
+- Modify: `src/components/SubmitForm.tsx`
+
+**Interfaces:**
+- Consumes: translation keys `home.*` (Task 2)
+- Produces: `export function SubmitForm()` -- same API/behavior as before, Tailwind-styled
+
+- [ ] **Step 1: Rewrite src/components/SubmitForm.tsx**
+
+```tsx
+'use client'
+
+import { useState, FormEvent } from 'react'
+import { useTranslations } from 'next-intl'
+import { useRouter } from '@/navigation'
+
+const FORMATS = ['mp4', 'mp3', 'webm', 'mkv']
+const QUALITIES = ['best', '1080p', '720p', '480p', '360p']
+
+function Spinner() {
+ return (
+
+
+
+
+ )
+}
+
+const selectClass =
+ 'rounded-lg border border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-2 py-1 text-gray-900 dark:text-gray-50 text-sm outline-none focus:ring-2 focus:ring-violet-500'
+
+export function SubmitForm() {
+ const t = useTranslations('home')
+ const router = useRouter()
+ const [url, setUrl] = useState('')
+ const [format, setFormat] = useState('mp4')
+ const [quality, setQuality] = useState('best')
+ const [subtitles, setSubtitles] = useState(false)
+ const [extraArgs, setExtraArgs] = useState('')
+ const [error, setError] = useState(null)
+ const [loading, setLoading] = useState(false)
+
+ async function handleSubmit(e: FormEvent) {
+ e.preventDefault()
+ setError(null)
+ setLoading(true)
+
+ const res = await fetch('/api/downloads', {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({
+ url,
+ format,
+ quality,
+ subtitles,
+ extraArgs: extraArgs.trim() || null,
+ }),
+ })
+
+ setLoading(false)
+
+ if (res.status === 429) {
+ setError(t('errorRateLimit'))
+ return
+ }
+ if (!res.ok) {
+ const body = await res.json().catch(() => ({}))
+ setError((body as { error?: string }).error ?? t('errorGeneric'))
+ return
+ }
+
+ const { uuid } = (await res.json()) as { uuid: string }
+ router.push(`/status/${uuid}`)
+ }
+
+ return (
+
+ )
+}
+```
+
+- [ ] **Step 2: Verify**
+
+```bash
+npx tsc --noEmit
+```
+
+Open `http://localhost:3000/fr`. The form should show: URL input with violet button inline, format/quality/subtitles options below. Hover on the button -- it darkens. Toggle dark mode -- everything inverts correctly. The glow is visible behind the input.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/components/SubmitForm.tsx
+git commit -m "feat: restyle SubmitForm with Tailwind, inline button, and violet glow"
+```
+
+---
+
+### Task 9: ReassuranceSection component
+
+**Files:**
+- Create: `src/components/ReassuranceSection.tsx`
+
+**Interfaces:**
+- Consumes: translation namespace `reassurance.*` (Task 2)
+- Produces: `export function ReassuranceSection()` -- 4-card grid, server component
+
+- [ ] **Step 1: Create src/components/ReassuranceSection.tsx**
+
+```tsx
+import { getTranslations } from 'next-intl/server'
+
+function GiftIcon() {
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+function BoltIcon() {
+ return (
+
+
+
+ )
+}
+
+function LockIcon() {
+ return (
+
+
+
+
+ )
+}
+
+function FilmIcon() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export async function ReassuranceSection({ locale }: { locale: string }) {
+ const t = await getTranslations({ locale, namespace: 'reassurance' })
+
+ const items = [
+ { title: t('free'), desc: t('freeDesc'), Icon: GiftIcon },
+ { title: t('fast'), desc: t('fastDesc'), Icon: BoltIcon },
+ { title: t('private'), desc: t('privateDesc'), Icon: LockIcon },
+ { title: t('formats'), desc: t('formatsDesc'), Icon: FilmIcon },
+ ]
+
+ return (
+
+
+ {items.map(({ title, desc, Icon }) => (
+
+
+
+
+
{title}
+
{desc}
+
+ ))}
+
+
+ )
+}
+```
+
+- [ ] **Step 2: Verify**
+
+```bash
+npx tsc --noEmit
+```
+
+Expected: no errors.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/components/ReassuranceSection.tsx
+git commit -m "feat: add ReassuranceSection with 4 benefit cards"
+```
+
+---
+
+### Task 10: Home page update
+
+**Files:**
+- Modify: `src/app/[locale]/page.tsx`
+
+**Interfaces:**
+- Consumes: `SubmitForm` (Task 8), `ReassuranceSection` (Task 9), translation keys `home.heading`, `home.subheading`, `home.formats` (Task 2)
+- Produces: full home page with hero section + form + reassurance
+
+- [ ] **Step 1: Rewrite src/app/[locale]/page.tsx**
+
+```tsx
+import { getTranslations } from 'next-intl/server'
+import { SubmitForm } from '@/components/SubmitForm'
+import { ReassuranceSection } from '@/components/ReassuranceSection'
+
+export default async function Home({
+ params,
+}: {
+ params: Promise<{ locale: string }>
+}) {
+ const { locale } = await params
+ const t = await getTranslations({ locale, namespace: 'home' })
+
+ return (
+
+
+
+ {t('heading')}
+
+
+ {t('subheading')}
+
+
+
+ {t('formats')}
+
+
+
+
+
+ )
+}
+```
+
+- [ ] **Step 2: Verify visually**
+
+Open `http://localhost:3000/fr`. You should see:
+- Centered `` "Telechargez vos videos gratuitement"
+- Sub-heading "Gratuit, rapide, sans compte"
+- URL input with violet button and glow
+- Format/quality/subtitle options below
+- "mp4 · mp3 · webm · mkv" label below options
+- 4 reassurance cards in a 2x2 (mobile) or 4x1 (desktop) grid
+- Footer "© 2026 Ombrora"
+
+Switch to dark mode. Switch locales. Confirm all translations update.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/app/[locale]/page.tsx
+git commit -m "feat: add hero section and reassurance grid to home page"
+```
+
+---
+
+### Task 11: StatusView + status page
+
+**Files:**
+- Modify: `src/components/StatusView.tsx`
+- Modify: `src/app/[locale]/status/[uuid]/page.tsx`
+
+**Interfaces:**
+- Consumes: translation keys `status.*` (Task 2); same polling API as before
+- Produces: styled status card with spinner, download button, error block
+
+- [ ] **Step 1: Rewrite src/components/StatusView.tsx**
+
+```tsx
+'use client'
+
+import { useEffect, useState } from 'react'
+import { useTranslations, useLocale } from 'next-intl'
+
+type DownloadData = {
+ uuid: string
+ status: 'PENDING' | 'PROCESSING' | 'DONE' | 'FAILED' | 'FILE_DELETED'
+ format: string
+ quality: string
+ fileName: string | null
+ fileSize: string | null
+ errorMsg: string | null
+ downloadToken: string | null
+ tokenExpiresAt: string | null
+}
+
+function Spinner() {
+ return (
+
+
+
+
+ )
+}
+
+export function StatusView({ uuid }: { uuid: string }) {
+ const t = useTranslations('status')
+ const locale = useLocale()
+ const [data, setData] = useState(null)
+ const [notFound, setNotFound] = useState(false)
+
+ useEffect(() => {
+ let active = true
+
+ async function poll() {
+ const res = await fetch(`/api/downloads/${uuid}`)
+ if (!active) return
+
+ if (res.status === 404) {
+ setNotFound(true)
+ return
+ }
+
+ const body = (await res.json()) as DownloadData
+ setData(body)
+
+ if (body.status === 'PENDING' || body.status === 'PROCESSING') {
+ setTimeout(poll, 5_000)
+ }
+ }
+
+ poll()
+ return () => { active = false }
+ }, [uuid])
+
+ const cardClass = 'max-w-md w-full mx-auto rounded-2xl border border-gray-100 dark:border-slate-800 bg-gray-50 dark:bg-slate-900 p-8'
+
+ if (notFound) {
+ return (
+
+ )
+ }
+
+ if (!data) {
+ return (
+
+ )
+ }
+
+ if (data.status === 'PENDING' || data.status === 'PROCESSING') {
+ return (
+
+
+
+ {data.status === 'PENDING' ? t('pending') : t('processing')}
+
+
{t('polling')}
+
+ )
+ }
+
+ if (data.status === 'DONE' && data.downloadToken) {
+ const sizeMb = data.fileSize
+ ? `(${(Number(data.fileSize) / 1_048_576).toFixed(1)} MB)`
+ : ''
+ return (
+
+
+
+ {t('ready')} {sizeMb}
+
+
+ {t('download')} {data.fileName}
+
+ {data.tokenExpiresAt && (
+
+ {t('linkExpires')}{' '}
+ {new Date(data.tokenExpiresAt).toLocaleString(locale)}
+
+ )}
+
+ )
+ }
+
+ if (data.status === 'FAILED') {
+ return (
+
+
{t('failed')}
+
+ {data.errorMsg}
+
+
+ )
+ }
+
+ if (data.status === 'FILE_DELETED') {
+ return (
+
+ )
+ }
+
+ return (
+
+
{t('unknown')}: {data.status}
+
+ )
+}
+```
+
+- [ ] **Step 2: Rewrite src/app/[locale]/status/[uuid]/page.tsx**
+
+```tsx
+import { getTranslations } from 'next-intl/server'
+import { StatusView } from '@/components/StatusView'
+
+export default async function StatusPage({
+ params,
+}: {
+ params: Promise<{ locale: string; uuid: string }>
+}) {
+ const { locale, uuid } = await params
+ const t = await getTranslations({ locale, namespace: 'status' })
+
+ return (
+
+
+ {t('heading')}
+
+
+
+ )
+}
+```
+
+- [ ] **Step 3: Verify visually**
+
+Submit a URL via the home form. On the status page, you should see:
+- A centred card with spinner while PENDING/PROCESSING
+- A green check + download button when DONE
+- A red block with error text when FAILED
+
+Test dark mode on the status page.
+
+- [ ] **Step 4: Verify type-check and existing tests still pass**
+
+```bash
+npx tsc --noEmit
+npm test
+```
+
+Expected: TypeScript clean, all existing backend tests pass (no UI tests exist, none added).
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/components/StatusView.tsx src/app/[locale]/status/[uuid]/page.tsx
+git commit -m "feat: restyle StatusView and status page with Tailwind"
+```