# Story 3.7: Navigation mobile - Chemin Libre et Bottom Bar

Status: review

## Story

As a visiteur mobile,
I want naviguer facilement avec une interface adaptée au tactile,
so that l'expérience reste immersive sur petit écran.

## Acceptance Criteria

1. **Given** le visiteur est sur mobile (< 768px) **When** il accède à la navigation **Then** le "Chemin Libre" affiche les zones en cards verticales scrollables (`ZoneCard`)
2. **And** chaque `ZoneCard` affiche : illustration, nom de la zone, statut (visité/nouveau/verrouillé)
3. **And** une ligne décorative relie les cards visuellement (effet chemin)
4. **And** un tap sur une zone navigue vers la section correspondante
5. **And** la zone Contact affiche un cadenas si `contactUnlocked` est `false`
6. **Given** la bottom bar mobile est affichée **When** le visiteur interagit **Then** 3 icônes sont accessibles : Carte (ouvre le Chemin Libre), Progression (affiche le %), Paramètres
7. **And** les touch targets font au minimum 48x48px
8. **And** la bottom bar est fixe et toujours visible
9. **And** le narrateur s'affiche au-dessus de la bottom bar quand actif

## Tasks / Subtasks

- [x] **Task 1: Créer le composant ZoneCard** (AC: #1, #2, #5)
  - [x] Créer `frontend/app/components/feature/ZoneCard.vue`
  - [x] Props : zone (MapZone), isVisited, isLocked, isCurrent
  - [x] Afficher illustration, nom traduit, statut visuel
  - [x] Icône cadenas si verrouillé
  - [x] Badge "Nouveau" si non visité
  - [x] Checkmark si visité

- [x] **Task 2: Créer le composant CheminLibre** (AC: #1, #3, #4)
  - [x] Créer `frontend/app/components/feature/CheminLibre.vue`
  - [x] Afficher les 5 zones en cards verticales
  - [x] Ligne décorative reliant les cards (SVG ou CSS)
  - [x] Scroll vertical natif
  - [x] Gestion du tap pour navigation

- [x] **Task 3: Créer le composant BottomBar** (AC: #6, #7, #8)
  - [x] Créer `frontend/app/components/layout/BottomBar.vue`
  - [x] 3 boutons : Carte, Progression, Paramètres
  - [x] Touch targets minimum 48x48px
  - [x] Position fixe en bas
  - [x] Variable CSS --bottom-bar-height pour le spacing

- [x] **Task 4: Intégrer le drawer Chemin Libre** (AC: #1)
  - [x] Au tap sur Carte dans BottomBar, ouvrir le CheminLibre
  - [x] Le CheminLibre s'affiche en slide-up depuis le bas
  - [x] Overlay pour fermer en tapant à l'extérieur
  - [x] Handle de glissement pour fermer

- [x] **Task 5: Intégrer le modal Progression** (AC: #6)
  - [x] Au tap sur Progression, afficher le détail
  - [x] Réutiliser le composant ProgressBar avec compact mode
  - [x] Afficher la liste des sections visitées/restantes

- [x] **Task 6: Intégrer les paramètres** (AC: #6)
  - [x] Au tap sur Paramètres, ouvrir un drawer
  - [x] Options : langue, mode Express (lien CV), réinitialiser
  - [x] Consentement RGPD accessible

- [x] **Task 7: Gérer le positionnement du narrateur** (AC: #9)
  - [x] Variable CSS --bottom-bar-height définie
  - [x] Le NarratorBubble utilise cette variable pour son bottom
  - [x] Pas de chevauchement entre narrateur et bottom bar

- [x] **Task 8: Responsive design**
  - [x] BottomBar visible uniquement < 768px
  - [x] CheminLibre adapté aux petits écrans
  - [x] Safe-area-inset pour les appareils avec notch

- [x] **Task 9: Tests et validation**
  - [x] Build validé sans erreurs
  - [x] Touch targets 48px minimum (min-w-12 min-h-12)
  - [x] Navigation entre zones fonctionnelle
  - [x] Drawer Chemin Libre avec slide-up animation
  - [x] Narrateur positionné au-dessus de la bottom bar

## Dev Notes

### Composant ZoneCard

```vue
<!-- frontend/app/components/feature/ZoneCard.vue -->
<script setup lang="ts">
import type { MapZone } from '~/data/mapZones'

const props = defineProps<{
  zone: MapZone
  isVisited: boolean
  isLocked: boolean
  isCurrent: boolean
}>()

const emit = defineEmits<{
  select: []
}>()

const { locale, t } = useI18n()

const zoneName = computed(() => {
  return locale.value === 'fr' ? props.zone.label.fr : props.zone.label.en
})

const statusText = computed(() => {
  if (props.isLocked) return t('zone.locked')
  if (props.isVisited) return t('zone.visited')
  return t('zone.new')
})

const statusClass = computed(() => {
  if (props.isLocked) return 'text-gray-500'
  if (props.isVisited) return 'text-green-400'
  return 'text-sky-accent'
})
</script>

<template>
  <button
    type="button"
    class="zone-card relative w-full flex items-center gap-4 p-4 bg-sky-dark-50 rounded-xl border border-sky-dark-100 transition-all active:scale-98"
    :class="[
      isCurrent && 'ring-2 ring-sky-accent',
      isLocked && 'opacity-60',
    ]"
    :disabled="isLocked"
    @click="emit('select')"
  >
    <!-- Illustration / Icône -->
    <div
      class="shrink-0 w-16 h-16 rounded-lg flex items-center justify-center text-3xl"
      :style="{ backgroundColor: `${zone.color}20` }"
    >
      <template v-if="isLocked">
        🔒
      </template>
      <template v-else>
        <img
          v-if="zone.icon.startsWith('/')"
          :src="zone.icon"
          :alt="zoneName"
          class="w-10 h-10"
        />
        <span v-else>{{ zone.icon }}</span>
      </template>
    </div>

    <!-- Contenu -->
    <div class="flex-1 text-left">
      <h3 class="font-ui font-semibold text-sky-text text-lg">
        {{ zoneName }}
      </h3>
      <p class="text-sm" :class="statusClass">
        {{ statusText }}
      </p>
    </div>

    <!-- Indicateurs -->
    <div class="shrink-0">
      <!-- Checkmark si visité -->
      <span
        v-if="isVisited && !isLocked"
        class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-green-500/20 text-green-400"
      >
        ✓
      </span>
      <!-- Badge "Nouveau" si non visité et non verrouillé -->
      <span
        v-else-if="!isVisited && !isLocked"
        class="inline-flex items-center justify-center px-2 py-1 rounded-full bg-sky-accent/20 text-sky-accent text-xs font-ui font-medium"
      >
        {{ t('zone.newBadge') }}
      </span>
      <!-- Cadenas si verrouillé -->
      <span
        v-else-if="isLocked"
        class="inline-flex items-center justify-center w-8 h-8 text-gray-500"
      >
        🔒
      </span>
    </div>
  </button>
</template>

<style scoped>
.zone-card:active {
  transform: scale(0.98);
}

.zone-card:disabled {
  cursor: not-allowed;
}
</style>
```

### Composant CheminLibre

```vue
<!-- frontend/app/components/feature/CheminLibre.vue -->
<script setup lang="ts">
import { mapZones } from '~/data/mapZones'

const emit = defineEmits<{
  close: []
  navigate: [route: string]
}>()

const { locale } = useI18n()
const router = useRouter()
const progressionStore = useProgressionStore()

function handleZoneSelect(zone: typeof mapZones[0]) {
  if (zone.id === 'contact' && !progressionStore.contactUnlocked) {
    return
  }

  const route = locale.value === 'fr' ? zone.route.fr : zone.route.en
  router.push(route)
  emit('navigate', route)
  emit('close')
}

function isVisited(zoneId: string): boolean {
  return zoneId !== 'contact' && progressionStore.visitedSections.includes(zoneId as any)
}

function isLocked(zoneId: string): boolean {
  return zoneId === 'contact' && !progressionStore.contactUnlocked
}

function isCurrent(zoneId: string): boolean {
  // Détecter basé sur la route actuelle
  const route = useRoute()
  const path = route.path.toLowerCase()

  const routeMap: Record<string, string[]> = {
    projets: ['projets', 'projects'],
    competences: ['competences', 'skills'],
    temoignages: ['temoignages', 'testimonials'],
    parcours: ['parcours', 'journey'],
    contact: ['contact'],
  }

  return routeMap[zoneId]?.some(segment => path.includes(segment)) ?? false
}
</script>

<template>
  <div class="chemin-libre h-full overflow-y-auto pb-safe">
    <!-- Header avec handle -->
    <div class="sticky top-0 bg-sky-dark-50 pt-4 pb-2 z-10">
      <div class="w-12 h-1 bg-sky-dark-100 rounded-full mx-auto mb-4"></div>
      <h2 class="text-xl font-ui font-bold text-sky-text text-center mb-4">
        {{ $t('cheminLibre.title') }}
      </h2>
    </div>

    <!-- Liste des zones avec ligne de connexion -->
    <div class="relative px-4 pb-8">
      <!-- Ligne de connexion verticale -->
      <div class="absolute left-12 top-8 bottom-8 w-0.5 bg-sky-dark-100"></div>

      <!-- Zones -->
      <div class="space-y-4 relative z-10">
        <div
          v-for="(zone, index) in mapZones"
          :key="zone.id"
          class="relative"
        >
          <!-- Point sur la ligne -->
          <div
            class="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 rounded-full border-2 z-10"
            :class="[
              isVisited(zone.id) ? 'bg-green-400 border-green-400' : '',
              isLocked(zone.id) ? 'bg-gray-500 border-gray-500' : '',
              !isVisited(zone.id) && !isLocked(zone.id) ? 'bg-sky-dark border-sky-accent' : '',
            ]"
          ></div>

          <!-- Card avec padding gauche pour la ligne -->
          <div class="pl-12">
            <ZoneCard
              :zone="zone"
              :is-visited="isVisited(zone.id)"
              :is-locked="isLocked(zone.id)"
              :is-current="isCurrent(zone.id)"
              @select="handleZoneSelect(zone)"
            />
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.pb-safe {
  padding-bottom: env(safe-area-inset-bottom, 1rem);
}
</style>
```

### Composant BottomBar

```vue
<!-- frontend/app/components/layout/BottomBar.vue -->
<script setup lang="ts">
const showCheminLibre = ref(false)
const showProgress = ref(false)
const showSettings = ref(false)

const progressionStore = useProgressionStore()
</script>

<template>
  <div class="bottom-bar-container md:hidden">
    <!-- Bottom Bar fixe -->
    <nav
      class="bottom-bar fixed bottom-0 inset-x-0 z-40 bg-sky-dark-50 border-t border-sky-dark-100 safe-bottom"
      style="--bottom-bar-height: 64px"
    >
      <div class="flex items-center justify-around h-16">
        <!-- Bouton Carte -->
        <button
          type="button"
          class="bottom-bar-btn flex flex-col items-center justify-center min-w-12 min-h-12 p-2 text-sky-text-muted hover:text-sky-accent transition-colors"
          :class="{ 'text-sky-accent': showCheminLibre }"
          :aria-label="$t('bottomBar.map')"
          @click="showCheminLibre = true"
        >
          <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
          </svg>
          <span class="text-xs font-ui mt-1">{{ $t('bottomBar.map') }}</span>
        </button>

        <!-- Bouton Progression -->
        <button
          type="button"
          class="bottom-bar-btn flex flex-col items-center justify-center min-w-12 min-h-12 p-2 text-sky-text-muted hover:text-sky-accent transition-colors"
          :class="{ 'text-sky-accent': showProgress }"
          :aria-label="$t('bottomBar.progress')"
          @click="showProgress = true"
        >
          <!-- Cercle de progression -->
          <div class="relative w-6 h-6">
            <svg class="w-full h-full -rotate-90" viewBox="0 0 36 36">
              <circle
                cx="18" cy="18" r="14"
                fill="none" stroke="currentColor" stroke-width="3"
                class="text-sky-dark-100"
              />
              <circle
                cx="18" cy="18" r="14"
                fill="none" stroke="currentColor" stroke-width="3"
                stroke-linecap="round"
                class="text-sky-accent"
                :stroke-dasharray="`${progressionStore.completionPercent}, 100`"
              />
            </svg>
            <span class="absolute inset-0 flex items-center justify-center text-[8px] font-ui font-bold">
              {{ progressionStore.completionPercent }}
            </span>
          </div>
          <span class="text-xs font-ui mt-1">{{ $t('bottomBar.progress') }}</span>
        </button>

        <!-- Bouton Paramètres -->
        <button
          type="button"
          class="bottom-bar-btn flex flex-col items-center justify-center min-w-12 min-h-12 p-2 text-sky-text-muted hover:text-sky-accent transition-colors"
          :class="{ 'text-sky-accent': showSettings }"
          :aria-label="$t('bottomBar.settings')"
          @click="showSettings = true"
        >
          <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
          </svg>
          <span class="text-xs font-ui mt-1">{{ $t('bottomBar.settings') }}</span>
        </button>
      </div>
    </nav>

    <!-- Drawer Chemin Libre -->
    <Teleport to="body">
      <Transition name="slide-up">
        <div
          v-if="showCheminLibre"
          class="fixed inset-x-0 bottom-16 top-0 z-50"
        >
          <!-- Overlay -->
          <div
            class="absolute inset-0 bg-black/50"
            @click="showCheminLibre = false"
          ></div>

          <!-- Drawer content -->
          <div class="absolute inset-x-0 bottom-0 max-h-[80vh] bg-sky-dark-50 rounded-t-2xl">
            <CheminLibre @close="showCheminLibre = false" />
          </div>
        </div>
      </Transition>

      <!-- Modal Progression -->
      <Transition name="fade">
        <div
          v-if="showProgress"
          class="fixed inset-0 z-50 flex items-end justify-center md:items-center"
        >
          <div
            class="absolute inset-0 bg-black/50"
            @click="showProgress = false"
          ></div>
          <div class="relative w-full max-w-sm bg-sky-dark-50 rounded-t-2xl md:rounded-2xl p-6 safe-bottom">
            <ProgressDetail @close="showProgress = false" />
          </div>
        </div>
      </Transition>

      <!-- Drawer Paramètres -->
      <Transition name="slide-up">
        <div
          v-if="showSettings"
          class="fixed inset-x-0 bottom-16 top-0 z-50"
        >
          <div
            class="absolute inset-0 bg-black/50"
            @click="showSettings = false"
          ></div>
          <div class="absolute inset-x-0 bottom-0 max-h-[60vh] bg-sky-dark-50 rounded-t-2xl p-6 safe-bottom">
            <SettingsDrawer @close="showSettings = false" />
          </div>
        </div>
      </Transition>
    </Teleport>
  </div>
</template>

<style scoped>
.safe-bottom {
  padding-bottom: max(env(safe-area-inset-bottom), 1rem);
}

.bottom-bar {
  height: var(--bottom-bar-height, 64px);
}

.bottom-bar-btn {
  min-width: 48px;
  min-height: 48px;
}

.slide-up-enter-active,
.slide-up-leave-active {
  transition: transform 0.3s ease, opacity 0.3s ease;
}

.slide-up-enter-from,
.slide-up-leave-to {
  transform: translateY(100%);
  opacity: 0;
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.2s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>
```

### Composant ProgressDetail (pour le modal)

```vue
<!-- frontend/app/components/feature/ProgressDetail.vue -->
<script setup lang="ts">
const emit = defineEmits<{
  close: []
}>()

const { t } = useI18n()
const progressionStore = useProgressionStore()

const sections = computed(() => [
  { key: 'projets', name: t('progress.sections.projects'), visited: progressionStore.visitedSections.includes('projets') },
  { key: 'competences', name: t('progress.sections.skills'), visited: progressionStore.visitedSections.includes('competences') },
  { key: 'temoignages', name: t('progress.sections.testimonials'), visited: progressionStore.visitedSections.includes('temoignages') },
  { key: 'parcours', name: t('progress.sections.journey'), visited: progressionStore.visitedSections.includes('parcours') },
])
</script>

<template>
  <div class="progress-detail">
    <!-- Handle -->
    <div class="w-12 h-1 bg-sky-dark-100 rounded-full mx-auto mb-4 md:hidden"></div>

    <h2 class="text-xl font-ui font-bold text-sky-text mb-4">
      {{ t('progress.title') }}
    </h2>

    <!-- Barre de progression -->
    <div class="mb-6">
      <ProgressBar
        :percent="progressionStore.completionPercent"
        :show-tooltip="false"
        compact
      />
    </div>

    <!-- Liste des sections -->
    <ul class="space-y-3 mb-6">
      <li
        v-for="section in sections"
        :key="section.key"
        class="flex items-center gap-3"
      >
        <span
          class="shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-sm"
          :class="section.visited ? 'bg-green-500/20 text-green-400' : 'bg-sky-dark-100 text-sky-text-muted'"
        >
          {{ section.visited ? '✓' : '○' }}
        </span>
        <span :class="section.visited ? 'text-sky-text' : 'text-sky-text-muted'">
          {{ section.name }}
        </span>
      </li>
    </ul>

    <!-- Bouton fermer -->
    <button
      type="button"
      class="w-full py-3 bg-sky-dark-100 rounded-lg text-sky-text font-ui font-medium"
      @click="emit('close')"
    >
      {{ t('common.close') }}
    </button>
  </div>
</template>
```

### Composant SettingsDrawer

```vue
<!-- frontend/app/components/feature/SettingsDrawer.vue -->
<script setup lang="ts">
const emit = defineEmits<{
  close: []
}>()

const { locale, setLocale, t } = useI18n()
const progressionStore = useProgressionStore()
const consentStore = useConsentStore()

function toggleLanguage() {
  setLocale(locale.value === 'fr' ? 'en' : 'fr')
}

function toggleExpressMode() {
  progressionStore.setExpressMode(!progressionStore.expressMode)
}

function resetProgress() {
  if (confirm(t('settings.confirmReset'))) {
    progressionStore.reset()
    localStorage.removeItem('skycel_progression')
  }
}
</script>

<template>
  <div class="settings-drawer">
    <!-- Handle -->
    <div class="w-12 h-1 bg-sky-dark-100 rounded-full mx-auto mb-4"></div>

    <h2 class="text-xl font-ui font-bold text-sky-text mb-6">
      {{ t('settings.title') }}
    </h2>

    <div class="space-y-4">
      <!-- Langue -->
      <div class="flex items-center justify-between py-3 border-b border-sky-dark-100">
        <span class="text-sky-text">{{ t('settings.language') }}</span>
        <button
          type="button"
          class="px-4 py-2 bg-sky-dark-100 rounded-lg text-sky-text font-ui"
          @click="toggleLanguage"
        >
          {{ locale === 'fr' ? 'English' : 'Français' }}
        </button>
      </div>

      <!-- Mode Express -->
      <div class="flex items-center justify-between py-3 border-b border-sky-dark-100">
        <div>
          <span class="text-sky-text block">{{ t('settings.expressMode') }}</span>
          <span class="text-xs text-sky-text-muted">{{ t('settings.expressModeDesc') }}</span>
        </div>
        <button
          type="button"
          class="w-12 h-6 rounded-full transition-colors"
          :class="progressionStore.expressMode ? 'bg-sky-accent' : 'bg-sky-dark-100'"
          @click="toggleExpressMode"
        >
          <span
            class="block w-5 h-5 rounded-full bg-white shadow transition-transform"
            :class="progressionStore.expressMode ? 'translate-x-6' : 'translate-x-0.5'"
          ></span>
        </button>
      </div>

      <!-- RGPD -->
      <div class="flex items-center justify-between py-3 border-b border-sky-dark-100">
        <div>
          <span class="text-sky-text block">{{ t('settings.saveProgress') }}</span>
          <span class="text-xs text-sky-text-muted">{{ t('settings.saveProgressDesc') }}</span>
        </div>
        <button
          type="button"
          class="w-12 h-6 rounded-full transition-colors"
          :class="consentStore.hasConsent ? 'bg-sky-accent' : 'bg-sky-dark-100'"
          @click="consentStore.hasConsent ? consentStore.revokeConsent() : consentStore.giveConsent()"
        >
          <span
            class="block w-5 h-5 rounded-full bg-white shadow transition-transform"
            :class="consentStore.hasConsent ? 'translate-x-6' : 'translate-x-0.5'"
          ></span>
        </button>
      </div>

      <!-- Réinitialiser -->
      <button
        type="button"
        class="w-full py-3 bg-red-500/20 text-red-400 rounded-lg font-ui font-medium mt-4"
        @click="resetProgress"
      >
        {{ t('settings.reset') }}
      </button>
    </div>

    <!-- Fermer -->
    <button
      type="button"
      class="w-full py-3 bg-sky-dark-100 rounded-lg text-sky-text font-ui font-medium mt-6"
      @click="emit('close')"
    >
      {{ t('common.close') }}
    </button>
  </div>
</template>
```

### Clés i18n

**fr.json :**
```json
{
  "zone": {
    "locked": "Verrouillé",
    "visited": "Visité",
    "new": "À découvrir",
    "newBadge": "Nouveau"
  },
  "cheminLibre": {
    "title": "Chemin Libre"
  },
  "bottomBar": {
    "map": "Carte",
    "progress": "Progression",
    "settings": "Options"
  },
  "settings": {
    "title": "Paramètres",
    "language": "Langue",
    "expressMode": "Mode Express",
    "expressModeDesc": "Navigation rapide sans aventure",
    "saveProgress": "Sauvegarder ma progression",
    "saveProgressDesc": "Permet de reprendre là où vous vous êtes arrêté",
    "reset": "Réinitialiser ma progression",
    "confirmReset": "Êtes-vous sûr de vouloir réinitialiser votre progression ?"
  }
}
```

**en.json :**
```json
{
  "zone": {
    "locked": "Locked",
    "visited": "Visited",
    "new": "To discover",
    "newBadge": "New"
  },
  "cheminLibre": {
    "title": "Free Path"
  },
  "bottomBar": {
    "map": "Map",
    "progress": "Progress",
    "settings": "Settings"
  },
  "settings": {
    "title": "Settings",
    "language": "Language",
    "expressMode": "Express Mode",
    "expressModeDesc": "Quick navigation without adventure",
    "saveProgress": "Save my progress",
    "saveProgressDesc": "Allows you to resume where you left off",
    "reset": "Reset my progress",
    "confirmReset": "Are you sure you want to reset your progress?"
  }
}
```

### Variable CSS pour la Bottom Bar

```css
/* frontend/app/assets/css/main.css ou variables.css */
:root {
  --bottom-bar-height: 64px;
}

/* Padding bottom pour le contenu principal sur mobile */
@media (max-width: 767px) {
  .main-content {
    padding-bottom: calc(var(--bottom-bar-height) + 1rem);
  }
}
```

### Dépendances

**Cette story nécessite :**
- Story 3.4 : ProgressBar composant
- Story 3.5 : Store de progression
- Story 3.2 : NarratorBubble (pour le positionnement)

**Cette story prépare pour :**
- Story 4.2 : Intro narrative (navigation mobile)
- Epic 4 : Chemins narratifs (utilise la navigation)

### Project Structure Notes

**Fichiers à créer :**
```
frontend/app/components/
├── feature/
│   ├── ZoneCard.vue                     # CRÉER
│   ├── CheminLibre.vue                  # CRÉER
│   ├── ProgressDetail.vue               # CRÉER
│   └── SettingsDrawer.vue               # CRÉER
└── layout/
    └── BottomBar.vue                    # CRÉER
```

**Fichiers à modifier :**
```
frontend/app/layouts/default.vue         # AJOUTER BottomBar
frontend/app/assets/css/main.css         # AJOUTER variables CSS
frontend/i18n/fr.json                    # AJOUTER traductions
frontend/i18n/en.json                    # AJOUTER traductions
```

### References

- [Source: docs/planning-artifacts/epics.md#Story-3.7]
- [Source: docs/planning-artifacts/ux-design-specification.md#Mobile-Navigation]
- [Source: docs/planning-artifacts/ux-design-specification.md#Bottom-Bar]

### Technical Requirements

| Requirement | Value | Source |
|-------------|-------|--------|
| Breakpoint mobile | < 768px | Epics |
| Touch targets | 48x48px minimum | WCAG |
| Bottom bar height | 64px | Décision technique |
| Safe area | env(safe-area-inset-bottom) | iOS |

## Dev Agent Record

### Agent Model Used

Claude Opus 4.5 (claude-opus-4-5-20251101)

### Debug Log References

- Build validé sans erreurs bloquantes
- Warning connu sur HeroType import dupliqué (non bloquant)

### Completion Notes List

- ZoneCard utilise `emoji` de MapZone (pas `icon` comme dans la spec originale)
- SettingsDrawer utilise un lien vers /resume au lieu d'un toggle expressMode
- ProgressDetail réutilise ProgressBar avec mode compact
- Toutes les transitions respectent prefers-reduced-motion
- Safe-area-inset géré pour iOS devices

### Change Log
| Date | Change | Author |
|------|--------|--------|
| 2026-02-04 | Story créée avec contexte complet | SM Agent |
| 2026-02-07 | Implémentation complète | Dev Agent |

### File List

**Fichiers créés :**
- `frontend/app/components/feature/ZoneCard.vue`
- `frontend/app/components/feature/CheminLibre.vue`
- `frontend/app/components/feature/ProgressDetail.vue`
- `frontend/app/components/feature/SettingsDrawer.vue`
- `frontend/app/components/layout/BottomBar.vue`

**Fichiers modifiés :**
- `frontend/app/layouts/default.vue` - Ajout BottomBar et padding mobile
- `frontend/app/layouts/adventure.vue` - Ajout BottomBar et padding mobile
- `frontend/app/assets/css/main.css` - Variable CSS --bottom-bar-height
- `frontend/i18n/fr.json` - Traductions zone, cheminLibre, bottomBar, settings
- `frontend/i18n/en.json` - Traductions zone, cheminLibre, bottomBar, settings
