# ALDANA Phase 2 — Vitrine publique 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:** Construire les 9 pages publiques d'ALDANA en HTML/CSS/JS vanilla, dans la direction visuelle du mockup Base44 (palette beige+terre, Cormorant Garamond + Inter, animations sobres). Pas de booking ni paiement encore (P4-P5), juste la vitrine consultable.

**Architecture:**

- **Couche présentation** : Layout PHP unique (`views/layouts/public.php`) inclut Navbar + Footer en partials, slot `$content` pour le corps de page rendu en amont.
- **Couche contrôleur** : `PublicController` avec une méthode par page, fait les appels Repository/Service, rend la vue, passe au Layout.
- **Couche données** : 4 repositories minces (`PracticeRepository`, `ClassRepository`, `PlanRepository`, `EventRepository`) + `SettingsService` clé/valeur pour `t_site_settings`.
- **Assets** : 1 CSS global `public/assets/css/aldana.css` (variables + reset + composants + utilities), 1 JS minimal `public/assets/js/aldana.js` (menu mobile, IntersectionObserver pour animations entrée).
- **SEO** : title + meta description + OG par page, sitemap.xml généré, robots.txt.

**Tech Stack:** PHP 8.2+, MySQL 9.1 (deps P0/P1), HTML5 sémantique, CSS3 avec variables, JS ES2020+ vanilla (pas de framework), Google Fonts (Cormorant Garamond + Inter).

**Pré-requis :** P0 + P1 livrées. Base `aldanadb` peuplée.

---

## File Structure

```
public/
├── assets/
│   ├── css/
│   │   └── aldana.css                  ← Task 1 (palette, reset, composants)
│   ├── js/
│   │   └── aldana.js                   ← Task 2 (menu mobile, animations)
│   └── img/                            ← Task 3 (placeholders + assets du mockup)
└── (index.php existant)

src/
├── Controllers/
│   ├── HelloController.php             ← à supprimer (Task 14)
│   └── PublicController.php            ← Task 10 (9 méthodes, 1 par page)
├── Repositories/
│   ├── PracticeRepository.php          ← Task 7
│   ├── ClassRepository.php             ← Task 7
│   ├── PlanRepository.php              ← Task 7
│   └── EventRepository.php             ← Task 7
└── Services/
    └── SettingsService.php             ← Task 8

views/
├── layouts/
│   └── public.php                      ← Task 4 (HTML doctype + head + slots)
├── partials/
│   ├── navbar.php                      ← Task 5
│   ├── footer.php                      ← Task 6
│   ├── meta.php                        ← Task 4 (intégré au layout)
│   └── animations-script.php           ← intégré au layout
└── pages/
    ├── public/
    │   ├── home.php                    ← Task 13
    │   ├── yoga-et-moi.php             ← Task 12 (groupe statiques)
    │   ├── le-lieu.php                 ← Task 12
    │   ├── contact.php                 ← Task 12
    │   ├── cours-particuliers.php      ← Task 12
    │   ├── pratiques.php               ← Task 11 (groupe data-driven)
    │   ├── tarifs.php                  ← Task 11
    │   ├── evenements.php              ← Task 11
    │   └── planning.php                ← Task 11
    └── errors/
        ├── 404.php                     ← Task 15
        └── 500.php                     ← Task 15

config/
└── routes.php                          ← Task 14 (modifier — ajouter 12 routes)
```

---

## Liste des tâches (16 au total)

| # | Tâche | Détail |
|---|---|---|
| 1 | CSS global `aldana.css` | Variables (palette mockup), reset, typo, composants base, utilities, responsive |
| 2 | JS global `aldana.js` | Menu mobile toggle + IntersectionObserver pour fade-in |
| 3 | Copier 7 images du mockup vers `public/assets/img/` | placeholders esthétiques en attendant photos Aurane |
| 4 | Layout `views/layouts/public.php` | Doctype + head SEO + Navbar + slot content + Footer + scripts |
| 5 | Partial `views/partials/navbar.php` | Logo ALDANA + 8 liens + toggle mobile |
| 6 | Partial `views/partials/footer.php` | 3 colonnes (brand, nav, contact) + signature quote |
| 7 | 4 Repositories | PracticeRepository, ClassRepository, PlanRepository, EventRepository — méthodes `list()`/`listActive()`/etc. |
| 8 | `SettingsService` | `get($key, $default = null)` avec cache statique |
| 9 | `BaseController` | Helper protégé `render($view, $data, $title, $description)` qui appelle Layout |
| 10 | `PublicController` | 9 méthodes + `home()` qui combine les 4 services |
| 11 | 4 pages data-driven | pratiques, tarifs, evenements, planning (HTML+données) |
| 12 | 4 pages statiques | yoga-et-moi, le-lieu, contact, cours-particuliers (HTML+settings) |
| 13 | Page home `views/pages/public/home.php` | Hero + 3 sections (pratiques, à propos teaser, CTA) |
| 14 | Modifier `config/routes.php` + `public/index.php` | Ajouter 12 routes publiques + 3 légales, retirer HelloController |
| 15 | Pages d'erreur 404/500 stylées | views/pages/errors/404.php + 500.php |
| 16 | `sitemap.xml` + `robots.txt` + smoke test + commit P2 | générés statiquement |

---

## Task 1: CSS global `aldana.css`

**Files:**

- Create: `public/assets/css/aldana.css`

Variables HSL reprises exactement du mockup `_mockup_base44/src/index.css` pour cohérence visuelle.

- [ ] **Step 1: Écrire le CSS complet (~250 lignes)**

```css
/* ============================================
   ALDANA — Aurane Aldana Yoga Versailles
   Variables, reset, typo, composants, utilities
   Inspiré du mockup Base44 (palette muted dévotionnelle)
   ============================================ */

@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400&family=Inter:wght@300;400;500;600&display=swap');

:root {
  /* Palette HSL (reprise du mockup, retirée le dark mode V1) */
  --background: hsl(30, 16%, 89%);
  --foreground: hsl(0, 0%, 10%);
  --card: hsl(30, 12%, 85%);
  --primary: hsl(25, 40%, 40%);
  --primary-foreground: hsl(30, 16%, 95%);
  --secondary: hsl(30, 10%, 80%);
  --muted: hsl(30, 10%, 82%);
  --muted-foreground: hsl(0, 0%, 40%);
  --border: hsl(30, 10%, 76%);
  --destructive: hsl(0, 84%, 60%);

  /* Typographie */
  --font-heading: 'Cormorant Garamond', Georgia, serif;
  --font-body: 'Inter', system-ui, sans-serif;

  /* Spacing & sizing */
  --radius: 0.25rem;
  --max-width: 80rem;
  --gutter: clamp(1.5rem, 5vw, 4rem);

  /* Animation */
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --duration-slow: 0.8s;
  --duration-base: 0.4s;
}

/* ============================================
   Reset minimaliste
   ============================================ */

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body { font-family: var(--font-body); font-weight: 300; line-height: 1.6; color: var(--foreground); background: var(--background); -webkit-font-smoothing: antialiased; }
img, svg, video { display: block; max-width: 100%; height: auto; }
button { font: inherit; border: none; background: none; cursor: pointer; color: inherit; }
a { color: inherit; text-decoration: none; }
input, textarea { font: inherit; color: inherit; }

/* ============================================
   Typographie
   ============================================ */

h1, h2, h3, h4 { font-family: var(--font-heading); font-weight: 300; line-height: 1.2; }
h1 { font-size: clamp(2.5rem, 6vw, 4.5rem); }
h2 { font-size: clamp(2rem, 5vw, 3.5rem); }
h3 { font-size: clamp(1.5rem, 3vw, 2rem); }
em, .italic { font-style: italic; }
.overline { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.2em; color: var(--muted-foreground); margin-bottom: 1rem; display: block; font-weight: 400; }
.lead { font-size: 1.125rem; max-width: 38rem; }

/* ============================================
   Layout
   ============================================ */

.container { max-width: var(--max-width); margin-inline: auto; padding-inline: var(--gutter); }
main { min-height: 100vh; }
section { padding-block: clamp(4rem, 10vw, 8rem); }
.section-tight { padding-block: clamp(2rem, 6vw, 5rem); }

/* ============================================
   Navbar
   ============================================ */

.navbar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; padding-block: 1.25rem; transition: background var(--duration-base) var(--ease), backdrop-filter var(--duration-base) var(--ease); }
.navbar.scrolled { background: hsla(30, 16%, 89%, 0.9); backdrop-filter: blur(12px); box-shadow: 0 1px 0 var(--border); }
.navbar-inner { display: flex; align-items: center; justify-content: space-between; gap: 2rem; }
.navbar-brand { font-family: var(--font-heading); font-size: 1.5rem; font-weight: 300; letter-spacing: 0.15em; text-transform: uppercase; }
.navbar-links { display: none; gap: 2.5rem; }
.navbar-link { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.15em; color: var(--muted-foreground); transition: color var(--duration-base) var(--ease); }
.navbar-link:hover, .navbar-link.active { color: var(--foreground); }
.navbar-toggle { display: block; padding: 0.5rem; }

@media (min-width: 768px) {
  .navbar-links { display: flex; }
  .navbar-toggle { display: none; }
}

.navbar-mobile { display: none; position: fixed; inset: 0 0 0 auto; width: min(90vw, 22rem); background: var(--background); padding: 5rem 2rem 2rem; box-shadow: -1px 0 0 var(--border); transform: translateX(100%); transition: transform var(--duration-base) var(--ease); z-index: 99; }
.navbar-mobile.open { display: block; transform: translateX(0); }
.navbar-mobile .navbar-link { display: block; padding: 1rem 0; font-size: 0.875rem; border-bottom: 1px solid var(--border); }

/* ============================================
   Hero
   ============================================ */

.hero { position: relative; min-height: 85vh; display: flex; align-items: center; padding-top: 6rem; overflow: hidden; }
.hero-image { position: absolute; inset: 0; z-index: -1; }
.hero-image img { width: 100%; height: 100%; object-fit: cover; opacity: 0.6; }
.hero-image::after { content: ''; position: absolute; inset: 0; background: linear-gradient(to right, var(--background) 0%, hsla(30, 16%, 89%, 0.4) 60%, transparent 100%); }
.hero-content { max-width: 38rem; }
.hero-title { margin-block: 1rem 2rem; }
.hero-cta { display: inline-block; margin-top: 2rem; padding: 1.25rem 3rem; background: var(--foreground); color: var(--background); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.2em; transition: background var(--duration-base) var(--ease); }
.hero-cta:hover { background: var(--primary); }

/* ============================================
   Cards & grilles
   ============================================ */

.grid-3 { display: grid; gap: 1.5rem; grid-template-columns: 1fr; }
.grid-2 { display: grid; gap: 2.5rem; grid-template-columns: 1fr; align-items: center; }
@media (min-width: 768px) { .grid-3 { grid-template-columns: repeat(3, 1fr); } .grid-2 { grid-template-columns: 1fr 1fr; gap: 4rem; } }

.card { background: var(--card); padding: 2rem; border-radius: var(--radius); transition: transform var(--duration-base) var(--ease); }
.card:hover { transform: translateY(-4px); }

.tile { position: relative; aspect-ratio: 3 / 4; overflow: hidden; }
.tile img { width: 100%; height: 100%; object-fit: cover; transition: transform var(--duration-slow) var(--ease); }
.tile:hover img { transform: scale(1.05); }
.tile-overlay { position: absolute; inset: 0; background: linear-gradient(to top, hsla(0, 0%, 10%, 0.7) 0%, hsla(0, 0%, 10%, 0.1) 50%, transparent 100%); }
.tile-caption { position: absolute; left: 1.5rem; right: 1.5rem; bottom: 1.5rem; color: var(--background); }
.tile-caption h3 { color: inherit; margin-bottom: 0.25rem; }
.tile-caption .overline { color: hsla(30, 16%, 95%, 0.7); margin-bottom: 0.5rem; }

/* ============================================
   Scales (intensité, rythme, chaleur — t_practices)
   ============================================ */

.scale { display: flex; align-items: center; gap: 0.5rem; margin-block: 0.5rem; font-size: 0.875rem; }
.scale-label { width: 6rem; color: var(--muted-foreground); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.1em; }
.scale-dots { display: flex; gap: 0.25rem; }
.scale-dot { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--border); }
.scale-dot.filled { background: var(--primary); }

/* ============================================
   Boutons
   ============================================ */

.btn { display: inline-block; padding: 1rem 2.5rem; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.2em; cursor: pointer; transition: all var(--duration-base) var(--ease); border: 1px solid transparent; }
.btn-primary { background: var(--foreground); color: var(--background); }
.btn-primary:hover { background: var(--primary); }
.btn-outline { background: transparent; color: var(--foreground); border-color: var(--border); }
.btn-outline:hover { background: var(--foreground); color: var(--background); border-color: var(--foreground); }

/* ============================================
   Footer
   ============================================ */

.footer { background: var(--foreground); color: var(--background); padding-block: 4rem; }
.footer-grid { display: grid; gap: 3rem; grid-template-columns: 1fr; }
@media (min-width: 768px) { .footer-grid { grid-template-columns: 2fr 1fr 1fr; } }
.footer-brand h3 { font-size: 1.75rem; margin-bottom: 1rem; letter-spacing: 0.1em; text-transform: uppercase; }
.footer-brand p { opacity: 0.6; max-width: 22rem; }
.footer h4 { font-family: var(--font-body); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.2em; opacity: 0.4; margin-bottom: 1.5rem; font-weight: 500; }
.footer-links { display: flex; flex-direction: column; gap: 0.75rem; }
.footer-links a { opacity: 0.7; transition: opacity var(--duration-base) var(--ease); font-size: 0.875rem; }
.footer-links a:hover { opacity: 1; }
.footer-bottom { border-top: 1px solid hsla(30, 16%, 95%, 0.1); margin-top: 3rem; padding-top: 2rem; display: flex; flex-direction: column; gap: 1rem; align-items: center; opacity: 0.4; font-size: 0.75rem; }
@media (min-width: 768px) { .footer-bottom { flex-direction: row; justify-content: space-between; } }
.footer-quote { font-family: var(--font-heading); font-style: italic; }

/* ============================================
   Animations d'entrée (utilisées avec IntersectionObserver)
   ============================================ */

.reveal { opacity: 0; transform: translateY(1.5rem); transition: opacity var(--duration-slow) var(--ease), transform var(--duration-slow) var(--ease); }
.reveal.visible { opacity: 1; transform: none; }

/* ============================================
   Utilities
   ============================================ */

.text-center { text-align: center; }
.text-muted { color: var(--muted-foreground); }
.text-primary { color: var(--primary); }
.mt-1 { margin-top: 0.5rem; } .mt-2 { margin-top: 1rem; } .mt-3 { margin-top: 1.5rem; } .mt-4 { margin-top: 2rem; } .mt-6 { margin-top: 3rem; } .mt-8 { margin-top: 4rem; }
.mb-1 { margin-bottom: 0.5rem; } .mb-2 { margin-bottom: 1rem; } .mb-3 { margin-bottom: 1.5rem; } .mb-4 { margin-bottom: 2rem; } .mb-6 { margin-bottom: 3rem; }
.flex { display: flex; }
.gap-2 { gap: 1rem; } .gap-4 { gap: 2rem; }
.italic-quote { font-family: var(--font-heading); font-style: italic; font-size: 1.25rem; color: var(--muted-foreground); border-left: 2px solid var(--primary); padding-left: 1.5rem; }
```

- [ ] **Step 2: Vérifier que le fichier est lisible (taille raisonnable, syntaxe OK)**

```powershell
Get-Item public/assets/css/aldana.css | Select-Object Name, Length
```

Attendu : Length > 5000 bytes.

---

## Task 2: JS global `aldana.js`

**Files:**

- Create: `public/assets/js/aldana.js`

- [ ] **Step 1: Écrire le JS minimal**

```javascript
/* ALDANA — JS vanilla minimal (menu mobile + animations entrée) */

(() => {
  'use strict';

  // ---- Navbar : ajout de classe scrolled au scroll ----
  const navbar = document.querySelector('.navbar');
  if (navbar) {
    const onScroll = () => {
      navbar.classList.toggle('scrolled', window.scrollY > 40);
    };
    window.addEventListener('scroll', onScroll, { passive: true });
    onScroll();
  }

  // ---- Menu mobile : toggle ----
  const toggle = document.querySelector('.navbar-toggle');
  const mobileMenu = document.querySelector('.navbar-mobile');
  if (toggle && mobileMenu) {
    toggle.addEventListener('click', () => {
      const open = mobileMenu.classList.toggle('open');
      toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
    });
    // Ferme au clic sur un lien
    mobileMenu.querySelectorAll('a').forEach(a =>
      a.addEventListener('click', () => mobileMenu.classList.remove('open'))
    );
    // Ferme à l'appui sur Échap
    document.addEventListener('keydown', e => {
      if (e.key === 'Escape') mobileMenu.classList.remove('open');
    });
  }

  // ---- Animations d'entrée via IntersectionObserver ----
  const reveals = document.querySelectorAll('.reveal');
  if (reveals.length && 'IntersectionObserver' in window) {
    const io = new IntersectionObserver(
      entries => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            entry.target.classList.add('visible');
            io.unobserve(entry.target);
          }
        });
      },
      { threshold: 0.15, rootMargin: '0px 0px -10% 0px' }
    );
    reveals.forEach(el => io.observe(el));
  } else {
    // Fallback : tout visible si pas d'IO
    reveals.forEach(el => el.classList.add('visible'));
  }
})();
```

- [ ] **Step 2: Vérifier le fichier**

```powershell
Get-Content public/assets/js/aldana.js -Head 5
```

---

## Task 3: Copier images du mockup

**Files:** copier les 7 PNG du mockup vers `public/assets/img/`

- [ ] **Step 1: Copier les 7 images**

```powershell
Copy-Item _mockup_base44/src/lib/images/*.png public/assets/img/
Get-ChildItem public/assets/img/*.png | Measure-Object | Select-Object Count
```

Attendu : `Count : 7`.

Les noms d'origine sont conservés (hash). Sera renommé proprement quand les vraies photos d'Aurane seront fournies. Pour l'instant, on les référence par leur nom hash dans les vues.

---

## Task 4: Layout `views/layouts/public.php`

**Files:**

- Create: `views/layouts/public.php`

- [ ] **Step 1: Créer le layout**

```php
<?php
declare(strict_types=1);
/**
 * Variables attendues :
 *   $title       : string  (titre de la page, sera complété par "— ALDANA")
 *   $description : string  (meta description, ~155 chars max)
 *   $content     : string  (HTML déjà rendu de la page, NON échappé)
 *   $currentPath : string  (URI actuel pour mettre en surbrillance le lien actif)
 *   $canonical   : string  (URL canonique absolue)
 */
$siteName = 'ALDANA';
$fullTitle = ($title ?? 'Le yoga d\'Aurane à Versailles') . ' — ' . $siteName;
$description = $description ?? 'ALDANA — Le yoga d\'Aurane à Versailles. Cours collectifs, cours particuliers et week-ends bien-être à la Maison Amara.';
$currentPath = $currentPath ?? '/';
$canonical = $canonical ?? (config('app.url') . $currentPath);
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= e($fullTitle) ?></title>
    <meta name="description" content="<?= e($description) ?>">
    <link rel="canonical" href="<?= e($canonical) ?>">

    <meta property="og:type" content="website">
    <meta property="og:title" content="<?= e($fullTitle) ?>">
    <meta property="og:description" content="<?= e($description) ?>">
    <meta property="og:url" content="<?= e($canonical) ?>">
    <meta property="og:locale" content="fr_FR">

    <meta name="twitter:card" content="summary_large_image">

    <link rel="stylesheet" href="/assets/css/aldana.css">
    <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='75' font-size='80' font-family='Georgia' fill='%238f6240'>A</text></svg>">
</head>
<body>
    <?= view('partials.navbar', ['currentPath' => $currentPath]) ?>

    <main>
        <?= $content ?? '' ?>
    </main>

    <?= view('partials.footer') ?>

    <script src="/assets/js/aldana.js" defer></script>
</body>
</html>
```

- [ ] **Step 2: Vérifier la syntaxe PHP**

```powershell
C:\wamp64\bin\php\php8.4.0\php.exe -l views/layouts/public.php
```

---

## Task 5: Partial `views/partials/navbar.php`

**Files:**

- Create: `views/partials/navbar.php`

- [ ] **Step 1: Créer le partial**

```php
<?php
declare(strict_types=1);
$currentPath = $currentPath ?? '/';
$links = [
    ['path' => '/', 'label' => 'Accueil'],
    ['path' => '/yoga-et-moi', 'label' => 'Aurane'],
    ['path' => '/pratiques', 'label' => 'Pratiques'],
    ['path' => '/le-lieu', 'label' => 'Le lieu'],
    ['path' => '/planning', 'label' => 'Planning'],
    ['path' => '/tarifs', 'label' => 'Tarifs'],
    ['path' => '/cours-particuliers', 'label' => 'Cours particuliers'],
    ['path' => '/evenements', 'label' => 'Événements'],
];
?>
<nav class="navbar" aria-label="Navigation principale">
    <div class="container navbar-inner">
        <a href="/" class="navbar-brand">ALDANA</a>

        <div class="navbar-links">
            <?php foreach ($links as $link): ?>
                <a href="<?= e($link['path']) ?>"
                   class="navbar-link <?= $currentPath === $link['path'] ? 'active' : '' ?>">
                    <?= e($link['label']) ?>
                </a>
            <?php endforeach; ?>
        </div>

        <button class="navbar-toggle" aria-label="Ouvrir le menu" aria-expanded="false" aria-controls="navbar-mobile">
            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
                <line x1="3" y1="6" x2="21" y2="6"/>
                <line x1="3" y1="12" x2="21" y2="12"/>
                <line x1="3" y1="18" x2="21" y2="18"/>
            </svg>
        </button>
    </div>

    <div class="navbar-mobile" id="navbar-mobile" aria-label="Menu mobile">
        <?php foreach ($links as $link): ?>
            <a href="<?= e($link['path']) ?>"
               class="navbar-link <?= $currentPath === $link['path'] ? 'active' : '' ?>">
                <?= e($link['label']) ?>
            </a>
        <?php endforeach; ?>
    </div>
</nav>
```

---

## Task 6: Partial `views/partials/footer.php`

**Files:**

- Create: `views/partials/footer.php`

- [ ] **Step 1: Créer le footer**

```php
<?php
declare(strict_types=1);
$settings = \App\Services\SettingsService::getInstance();
$quote = $settings->get('signature_quote', 'Il ne faut jamais sous-estimer l\'influence du hasard sur l\'existence de tout être');
$address = $settings->get('lieu_address', '16b Passages de la Geôle, 78000 Versailles');
$email = $settings->get('contact_email', 'aldana.aurane@hotmail.fr');
$phone = $settings->get('contact_phone', '06 74 96 99 33');
$year = date('Y');
?>
<footer class="footer">
    <div class="container">
        <div class="footer-grid">
            <div class="footer-brand">
                <h3>ALDANA</h3>
                <p>Un sanctuaire où le mouvement rencontre le silence. La force dans l'immobilité.</p>
            </div>

            <div>
                <h4>Navigation</h4>
                <div class="footer-links">
                    <a href="/yoga-et-moi">Aurane</a>
                    <a href="/pratiques">Pratiques</a>
                    <a href="/le-lieu">Le lieu</a>
                    <a href="/planning">Planning</a>
                    <a href="/tarifs">Tarifs</a>
                </div>
            </div>

            <div>
                <h4>Contact</h4>
                <div class="footer-links">
                    <span><?= e($address) ?></span>
                    <a href="mailto:<?= e($email) ?>"><?= e($email) ?></a>
                    <a href="tel:<?= e($settings->get('contact_phone_intl', '+33674969933')) ?>"><?= e($phone) ?></a>
                </div>
            </div>
        </div>

        <div class="footer-bottom">
            <p>© <?= e((string) $year) ?> ALDANA. Tous droits réservés. ·
               <a href="/mentions-legales" style="opacity: inherit;">Mentions légales</a> ·
               <a href="/politique-de-confidentialite" style="opacity: inherit;">RGPD</a> ·
               <a href="/cgv" style="opacity: inherit;">CGV</a>
            </p>
            <p class="footer-quote">« <?= e($quote) ?> »</p>
        </div>
    </div>
</footer>
```

---

## Task 7: 4 Repositories (Practice, Class, Plan, Event)

**Files:**

- Create: `src/Repositories/PracticeRepository.php`
- Create: `src/Repositories/ClassRepository.php`
- Create: `src/Repositories/PlanRepository.php`
- Create: `src/Repositories/EventRepository.php`

- [ ] **Step 1: PracticeRepository.php**

```php
<?php
declare(strict_types=1);

namespace App\Repositories;

use App\Helpers\Database;
use PDO;

final class PracticeRepository
{
    private PDO $pdo;

    public function __construct()
    {
        $this->pdo = Database::getInstance()->getConnection();
    }

    public function listActive(): array
    {
        $stmt = $this->pdo->query(
            'SELECT * FROM t_practices WHERE active = 1 ORDER BY display_order ASC, name ASC'
        );
        return $stmt->fetchAll();
    }

    public function findBySlug(string $slug): ?array
    {
        $stmt = $this->pdo->prepare('SELECT * FROM t_practices WHERE slug = ? AND active = 1');
        $stmt->execute([$slug]);
        $row = $stmt->fetch();
        return $row ?: null;
    }
}
```

- [ ] **Step 2: ClassRepository.php**

```php
<?php
declare(strict_types=1);

namespace App\Repositories;

use App\Helpers\Database;
use PDO;

final class ClassRepository
{
    private PDO $pdo;

    public function __construct()
    {
        $this->pdo = Database::getInstance()->getConnection();
    }

    /**
     * Cours collectifs entre deux dates (incluses), avec infos pratique jointes.
     */
    public function listBetween(string $fromDate, string $toDate): array
    {
        $stmt = $this->pdo->prepare(
            'SELECT c.*, p.name AS practice_name, p.slug AS practice_slug
             FROM t_classes c
             JOIN t_practices p ON p.practice_ID = c.practice_ID
             WHERE c.class_date BETWEEN ? AND ? AND c.status = "scheduled"
             ORDER BY c.class_date ASC, c.class_time ASC'
        );
        $stmt->execute([$fromDate, $toDate]);
        return $stmt->fetchAll();
    }
}
```

- [ ] **Step 3: PlanRepository.php**

```php
<?php
declare(strict_types=1);

namespace App\Repositories;

use App\Helpers\Database;
use PDO;

final class PlanRepository
{
    private PDO $pdo;

    public function __construct()
    {
        $this->pdo = Database::getInstance()->getConnection();
    }

    public function listActive(): array
    {
        $stmt = $this->pdo->query(
            'SELECT * FROM t_subscription_plans WHERE active = 1 ORDER BY display_order ASC'
        );
        return $stmt->fetchAll();
    }
}
```

- [ ] **Step 4: EventRepository.php**

```php
<?php
declare(strict_types=1);

namespace App\Repositories;

use App\Helpers\Database;
use PDO;

final class EventRepository
{
    private PDO $pdo;

    public function __construct()
    {
        $this->pdo = Database::getInstance()->getConnection();
    }

    public function listUpcomingPublished(): array
    {
        $stmt = $this->pdo->query(
            'SELECT * FROM t_events
             WHERE status IN ("published","sold_out") AND date_start >= NOW()
             ORDER BY date_start ASC'
        );
        return $stmt->fetchAll();
    }
}
```

- [ ] **Step 5: Vérifier la syntaxe des 4 fichiers**

```powershell
foreach ($f in @('PracticeRepository.php','ClassRepository.php','PlanRepository.php','EventRepository.php')) {
  C:\wamp64\bin\php\php8.4.0\php.exe -l "src/Repositories/$f"
}
```

---

## Task 8: SettingsService

**Files:**

- Create: `src/Services/SettingsService.php`

- [ ] **Step 1: Créer le service**

```php
<?php
declare(strict_types=1);

namespace App\Services;

use App\Helpers\Database;
use PDO;

/**
 * Lecture des settings clé/valeur (t_site_settings) avec cache statique
 * pour éviter les requêtes répétées dans la même requête HTTP.
 */
final class SettingsService
{
    private static ?SettingsService $instance = null;
    private array $cache = [];
    private bool $loaded = false;
    private PDO $pdo;

    private function __construct()
    {
        $this->pdo = Database::getInstance()->getConnection();
    }

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function get(string $key, ?string $default = null): ?string
    {
        $this->load();
        return $this->cache[$key] ?? $default;
    }

    public function all(): array
    {
        $this->load();
        return $this->cache;
    }

    private function load(): void
    {
        if ($this->loaded) {
            return;
        }
        $stmt = $this->pdo->query('SELECT setting_key, setting_value FROM t_site_settings');
        foreach ($stmt->fetchAll() as $row) {
            $this->cache[$row['setting_key']] = $row['setting_value'];
        }
        $this->loaded = true;
    }
}
```

---

## Task 9: BaseController

**Files:**

- Create: `src/Controllers/BaseController.php`

- [ ] **Step 1: Créer le contrôleur de base**

```php
<?php
declare(strict_types=1);

namespace App\Controllers;

/**
 * Contrôleur abstrait : helper `render()` qui rend une vue
 * et la place dans le layout public.
 */
abstract class BaseController
{
    protected function render(
        string $viewName,
        array $data = [],
        ?string $title = null,
        ?string $description = null,
        ?string $layout = 'layouts.public'
    ): void {
        $content = view($viewName, $data);
        echo view($layout, [
            'title'       => $title,
            'description' => $description,
            'content'     => $content,
            'currentPath' => $_SERVER['REQUEST_URI'] ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '/',
            'canonical'   => rtrim(config('app.url'), '/') . ($_SERVER['REQUEST_URI'] ?? '/'),
        ]);
    }
}
```

---

## Task 10: PublicController

**Files:**

- Create: `src/Controllers/PublicController.php`
- Delete: `src/Controllers/HelloController.php` (sera supprimé en Task 14 après remplacement de la route /)

- [ ] **Step 1: Créer le contrôleur**

```php
<?php
declare(strict_types=1);

namespace App\Controllers;

use App\Repositories\PracticeRepository;
use App\Repositories\PlanRepository;
use App\Repositories\ClassRepository;
use App\Repositories\EventRepository;
use App\Services\SettingsService;

final class PublicController extends BaseController
{
    public function home(): void
    {
        $settings   = SettingsService::getInstance();
        $practices  = (new PracticeRepository())->listActive();

        $this->render('pages.public.home', [
            'practices' => $practices,
            'heroTitle' => $settings->get('home_hero_title', 'Le silence en mouvement'),
            'heroSub'   => $settings->get('home_hero_subtitle', 'Le yoga d\'Aurane à Versailles'),
        ],
        title: 'Le yoga d\'Aurane à Versailles',
        description: 'Cours de yoga et Power Yoga à Versailles avec Aurane Aldana. Studio La Maison Amara, cours collectifs et particuliers.');
    }

    public function yogaEtMoi(): void
    {
        $this->render('pages.public.yoga-et-moi', [],
            title: 'Aurane',
            description: 'Aurane Aldana, professeure de yoga à Versailles. Son parcours, sa philosophie, sa pratique.');
    }

    public function pratiques(): void
    {
        $practices = (new PracticeRepository())->listActive();
        $this->render('pages.public.pratiques', ['practices' => $practices],
            title: 'Pratiques',
            description: 'Les pratiques enseignées par Aurane Aldana : Yoga, Power Yoga. Texture, rythme, intensité de chaque modalité.');
    }

    public function leLieu(): void
    {
        $this->render('pages.public.le-lieu', [],
            title: 'Le lieu',
            description: 'La Maison Amara, le lieu où enseigne Aurane Aldana à Versailles. Adresse, accès, ambiance.');
    }

    public function planning(): void
    {
        $from    = (new \DateTime('today'))->format('Y-m-d');
        $to      = (new \DateTime('today +14 days'))->format('Y-m-d');
        $classes = (new ClassRepository())->listBetween($from, $to);

        $this->render('pages.public.planning', ['classes' => $classes, 'fromDate' => $from, 'toDate' => $to],
            title: 'Planning & Réservation',
            description: 'Planning des cours de yoga à Versailles. Réservez votre séance avec Aurane Aldana.');
    }

    public function coursParticuliers(): void
    {
        $this->render('pages.public.cours-particuliers', [],
            title: 'Cours particuliers',
            description: 'Cours de yoga particuliers à Versailles avec Aurane Aldana. À domicile ou en studio. Pratique sur-mesure.');
    }

    public function evenements(): void
    {
        $events = (new EventRepository())->listUpcomingPublished();
        $this->render('pages.public.evenements', ['events' => $events],
            title: 'Week-Ends Bien-Être',
            description: 'Week-ends bien-être organisés par Aurane Aldana. Yoga, retraites, ressourcement.');
    }

    public function tarifs(): void
    {
        $plans = (new PlanRepository())->listActive();
        $this->render('pages.public.tarifs', ['plans' => $plans],
            title: 'Tarifs',
            description: 'Tarifs des cours de yoga ALDANA à Versailles : séance unitaire, carnets, abonnement mensuel.');
    }

    public function contact(): void
    {
        $this->render('pages.public.contact', [],
            title: 'Contact',
            description: 'Contacter Aurane Aldana à Versailles. Email, téléphone, formulaire.');
    }

    public function mentionsLegales(): void
    {
        $this->render('pages.public.mentions-legales', [],
            title: 'Mentions légales',
            description: 'Mentions légales du site ALDANA.');
    }

    public function politiqueConfidentialite(): void
    {
        $this->render('pages.public.politique-de-confidentialite', [],
            title: 'Politique de confidentialité',
            description: 'Politique de confidentialité du site ALDANA. RGPD.');
    }

    public function cgv(): void
    {
        $this->render('pages.public.cgv', [],
            title: 'Conditions générales de vente',
            description: 'CGV ALDANA — Aurane Aldana yoga Versailles.');
    }
}
```

---

## Task 11: 4 pages data-driven (pratiques, tarifs, evenements, planning)

**Files:**

- Create: `views/pages/public/pratiques.php`
- Create: `views/pages/public/tarifs.php`
- Create: `views/pages/public/evenements.php`
- Create: `views/pages/public/planning.php`

Code des 4 fichiers détaillé pendant l'exécution (chacun ~50-80 lignes). Pattern commun : un header avec overline + h1 + lead, puis le contenu rendu depuis `$practices`/`$plans`/`$events`/`$classes`.

---

## Task 12: 4 pages statiques (yoga-et-moi, le-lieu, contact, cours-particuliers)

**Files:**

- Create: `views/pages/public/yoga-et-moi.php`
- Create: `views/pages/public/le-lieu.php`
- Create: `views/pages/public/contact.php`
- Create: `views/pages/public/cours-particuliers.php`

Contenu rédigé directement dans les vues (en-dur HTML), avec quelques settings lus dynamiquement (citation, address, email, tel) via `SettingsService`. Détaillé pendant l'exécution.

---

## Task 13: Page Home

**Files:**

- Create: `views/pages/public/home.php`

Sections :
- Hero (titre + sous-titre + CTA Planning)
- Aperçu pratiques (3 cartes depuis `$practices`)
- À propos Aurane (teaser + CTA /yoga-et-moi)
- CTA final (citation signature)

---

## Task 14: Mettre à jour routes.php + index.php

**Files:**

- Modify: `config/routes.php`
- Modify: `public/index.php` (gestion 404 stylée)
- Delete: `src/Controllers/HelloController.php` (la route / pointe maintenant sur PublicController::home)

- [ ] **Step 1: Réécrire routes.php**

```php
<?php
declare(strict_types=1);

return [
    'GET /'                              => ['App\Controllers\PublicController', 'home'],
    'GET /yoga-et-moi'                   => ['App\Controllers\PublicController', 'yogaEtMoi'],
    'GET /pratiques'                     => ['App\Controllers\PublicController', 'pratiques'],
    'GET /le-lieu'                       => ['App\Controllers\PublicController', 'leLieu'],
    'GET /planning'                      => ['App\Controllers\PublicController', 'planning'],
    'GET /cours-particuliers'            => ['App\Controllers\PublicController', 'coursParticuliers'],
    'GET /evenements'                    => ['App\Controllers\PublicController', 'evenements'],
    'GET /tarifs'                        => ['App\Controllers\PublicController', 'tarifs'],
    'GET /contact'                       => ['App\Controllers\PublicController', 'contact'],
    'GET /mentions-legales'              => ['App\Controllers\PublicController', 'mentionsLegales'],
    'GET /politique-de-confidentialite'  => ['App\Controllers\PublicController', 'politiqueConfidentialite'],
    'GET /cgv'                           => ['App\Controllers\PublicController', 'cgv'],
];
```

- [ ] **Step 2: Modifier `public/index.php` pour rendre les 404/500 via le layout**

Remplacer le bloc `else { 404 }` et le bloc `catch { 500 }` pour utiliser le helper `view()`.

- [ ] **Step 3: Supprimer HelloController.php**

```powershell
Remove-Item src/Controllers/HelloController.php
```

---

## Task 15: Pages d'erreur 404 + 500

**Files:**

- Create: `views/pages/errors/404.php`
- Create: `views/pages/errors/500.php`

Pages stylées avec le layout public.

---

## Task 16: sitemap.xml + robots.txt + smoke test + commit

**Files:**

- Create: `public/sitemap.xml`
- Create: `public/robots.txt`

- [ ] **Step 1: Créer sitemap.xml** (statique pour V1, généré dynamiquement plus tard si besoin)

- [ ] **Step 2: Créer robots.txt**

- [ ] **Step 3: Smoke test** — lancer le serveur PHP intégré, faire un curl sur chaque route, vérifier code 200

- [ ] **Step 4: Commit P2**

```powershell
git add public/ src/ views/ config/routes.php docs/superpowers/plans/2026-05-18-aldana-p2-vitrine.md
git commit -m "feat(P2): vitrine publique — 9 pages + 3 légales + Layout + assets"
```

---

## Self-Review

**Couverture spec §3.1** : les 9 pages publiques + 3 légales sont créées (Tasks 11-13). ✓

**Couverture spec §2 (positionnement)** : palette muted + dévotionnel sobre conservé, Cormorant Garamond + Inter, overlines tracking 0.2em. ✓

**Couverture spec §6.4 (conventions front)** : HTML5 sémantique, BEM-like classes, mobile-first, IntersectionObserver, animations CSS pures. ✓

**Pas de dépendance JS framework** ✓ (pas de framer-motion, pas de React, pas de Tailwind compilé — juste vanilla)

**Cohérence des noms** :
- `SettingsService::getInstance()` utilisé dans footer.php et PublicController ✓
- `view('partials.navbar', ...)` ↔ `views/partials/navbar.php` (helper `view()` translate dots) ✓
- `BaseController::render()` appelé par les 12 méthodes du PublicController ✓

**Risques** :
- L'helper `view()` doit gérer correctement le chargement de `views/partials/...` quand appelé depuis l'intérieur d'une vue (Task 4 layout appelle `view('partials.navbar')`). Vérifier en P0 que le helper résout bien.
- Le `$_SERVER['REQUEST_URI']` peut contenir des query strings — bien le parser via `parse_url()` (fait dans BaseController).
- Routes en lecture seule pour V1 — toutes en GET ; les POST (booking, formulaires) arriveront P3+.

---

## Sortie attendue fin P2

- 12 routes publiques GET fonctionnelles (200 OK + HTML rendu cohérent)
- Layout partagé, palette + typo + animations alignés au mockup
- Données lues depuis `aldanadb` (3 formules en /tarifs, 2 pratiques en /pratiques, 0 cours en /planning car pas encore seedés, 0 events)
- 404 + 500 stylées
- sitemap.xml + robots.txt
- Tests : `php -S localhost:8000 -t public/` + curl sur les 12 routes → toutes 200
- Commit P2 sur `main`

**Durée estimée** : 5-7 jours dev humain · ~1h en exécution Claude inline parallélisée.

**Prochain plan** : `2026-05-18-aldana-p3-auth.md` (login, register, forgot/reset password, sessions sécurisées, espace membre vide).
