diff --git a/src/lib/components/DevNavBar.svelte b/src/lib/components/DevNavBar.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..00e3f5cb3c115e5d3edda7bcd45d2758961d16ed
--- /dev/null
+++ b/src/lib/components/DevNavBar.svelte
@@ -0,0 +1,199 @@
+<script lang="ts">
+  import { page, session } from "$app/stores"
+
+  const menuItems = [
+    { href: "/", label: "Accueil" },
+    { href: "/about", label: "À propos" },
+    { href: "/calculations", label: "Calculs" },
+    { href: "/entities", label: "Entités" },
+    { href: "/parameters", label: "Paramètres" },
+    { href: "/storybook", label: "Storybook" },
+    { href: "/variables", label: "Variables" },
+  ]
+  let open = false
+  let openUserMenu = false
+
+  $: pageUrlPath = $page.path.replace(/\/+$/, "") || "/"
+
+  $: title = $session.title
+
+  function activeMenuItem(href: string) {
+    return pageUrlPath === href || pageUrlPath.startsWith(href + "/")
+  }
+</script>
+
+<nav class="bg-gray-800">
+  <div class="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
+    <div class="relative flex items-center justify-between h-16">
+      <div class="absolute inset-y-0 left-0 flex items-center sm:hidden">
+        <!-- Mobile menu button-->
+        <button
+          aria-controls="mobile-menu"
+          aria-expanded={open}
+          class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
+          on:click={() => (open = !open)}
+          type="button"
+        >
+          <span class="sr-only">Open main menu</span>
+          {#if open}
+            <!-- Heroicon name: outline/x -->
+            <svg
+              class="block h-6 stroke-current w-6"
+              xmlns="http://www.w3.org/2000/svg"
+              fill="none"
+              viewBox="0 0 24 24"
+              aria-hidden="true"
+            >
+              <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                stroke-width="2"
+                d="M6 18L18 6M6 6l12 12"
+              />
+            </svg>
+          {:else}
+            <!-- Heroicon name: outline/menu -->
+            <svg
+              class="block h-6 stroke-current w-6"
+              xmlns="http://www.w3.org/2000/svg"
+              fill="none"
+              viewBox="0 0 24 24"
+              aria-hidden="true"
+            >
+              <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                stroke-width="2"
+                d="M4 6h16M4 12h16M4 18h16"
+              />
+            </svg>
+          {/if}
+        </button>
+      </div>
+      <div
+        class="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start"
+      >
+        <div class="flex-shrink-0 flex items-center">
+          <img
+            alt={title}
+            class="block lg:hidden h-8 w-auto"
+            src="/logo_100x100.png"
+          />
+          <img
+            alt={title}
+            class="hidden lg:block h-8 w-auto"
+            src="/logo_100x100.png"
+          />
+        </div>
+        <div class="hidden sm:block sm:ml-6">
+          <div class="flex space-x-4">
+            {#each menuItems as { label, href }}
+              <a
+                aria-current="page"
+                class="{activeMenuItem(href)
+                  ? 'bg-gray-900 text-white'
+                  : 'text-gray-300 hover:bg-gray-700 hover:text-white'} px-3 py-2 rounded-md text-sm font-medium"
+                {href}>{label}</a
+              >
+            {/each}
+          </div>
+        </div>
+      </div>
+      <div
+        class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0"
+      >
+        <button
+          class="bg-gray-800 p-1 rounded-full text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
+        >
+          <span class="sr-only">View notifications</span>
+          <!-- Heroicon name: outline/bell -->
+          <svg
+            class="h-6 stroke-current w-6"
+            xmlns="http://www.w3.org/2000/svg"
+            fill="none"
+            viewBox="0 0 24 24"
+            aria-hidden="true"
+          >
+            <path
+              stroke-linecap="round"
+              stroke-linejoin="round"
+              stroke-width="2"
+              d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
+            />
+          </svg>
+        </button>
+
+        <!-- Profile dropdown -->
+        <div class="ml-3 relative">
+          <div>
+            <button
+              aria-expanded={openUserMenu}
+              aria-haspopup="true"
+              class="bg-gray-800 flex text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
+              id="user-menu"
+              on:click={() => (openUserMenu = !openUserMenu)}
+              type="button"
+            >
+              <span class="sr-only">Open user menu</span>
+              <img
+                class="h-8 w-8 rounded-full"
+                src="/logo_100x100.png"
+                alt=""
+              />
+            </button>
+          </div>
+          {#if openUserMenu}
+            <!--
+              Dropdown menu
+
+              Entering: "transition ease-out duration-100"
+                From: "transform opacity-0 scale-95"
+                To: "transform opacity-100 scale-100"
+              Leaving: "transition ease-in duration-75"
+                From: "transform opacity-100 scale-100"
+                To: "transform opacity-0 scale-95"
+            -->
+            <div
+              class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"
+              role="menu"
+              aria-orientation="vertical"
+              aria-labelledby="user-menu"
+            >
+              <a
+                href="profile"
+                class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
+                role="menuitem">Your Profile</a
+              >
+              <a
+                href="settings"
+                class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
+                role="menuitem">Settings</a
+              >
+              <a
+                href="sign_out"
+                class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
+                role="menuitem">Sign out</a
+              >
+            </div>
+          {/if}
+        </div>
+      </div>
+    </div>
+  </div>
+
+  {#if open}
+    <div class="sm:hidden" id="mobile-menu">
+      <div class="px-2 pt-2 pb-3 space-y-1">
+        {#each menuItems as { label, href }}
+          <a
+            aria-current="page"
+            class="{activeMenuItem(href)
+              ? 'bg-gray-900 text-white'
+              : 'text-gray-300 hover:bg-gray-700 hover:text-white'} block px-3 py-2 rounded-md text-base font-medium"
+            {href}>{label}</a
+          >
+        {/each}
+      </div>
+    </div>
+  {/if}
+</nav>
diff --git a/src/lib/components/NavBar.svelte b/src/lib/components/NavBar.svelte
index 00e3f5cb3c115e5d3edda7bcd45d2758961d16ed..8d7bcb68225f5ee8d2f0cba7742c1ff7937bc430 100644
--- a/src/lib/components/NavBar.svelte
+++ b/src/lib/components/NavBar.svelte
@@ -2,12 +2,9 @@
   import { page, session } from "$app/stores"
 
   const menuItems = [
-    { href: "/", label: "Accueil" },
+    { href: "/", label: "Simulation" },
     { href: "/about", label: "À propos" },
-    { href: "/calculations", label: "Calculs" },
-    { href: "/entities", label: "Entités" },
     { href: "/parameters", label: "Paramètres" },
-    { href: "/storybook", label: "Storybook" },
     { href: "/variables", label: "Variables" },
   ]
   let open = false
diff --git a/src/routes/dev.svelte b/src/routes/dev.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..2eadf70625ec4e31d05028d36e9aa6f5de7b19e3
--- /dev/null
+++ b/src/routes/dev.svelte
@@ -0,0 +1,31 @@
+<script lang="ts">
+  import { session } from "$app/stores"
+
+  $: title = $session.title
+</script>
+
+<svelte:head>
+  <title>{title}</title>
+</svelte:head>
+
+<main>
+  <h1>{title}</h1>
+
+  <h2>Tests de l'API OpenFisca</h2>
+  <ul>
+    <li><a class="link" href="/calculations">Calculs</a></li>
+    <li><a class="link" href="/entities">Entités</a></li>
+    <li><a class="link" href="/parameters">Paramètres</a></li>
+    <li><a class="link" href="/variables">Variables</a></li>
+  </ul>
+
+  <h2>Documentation</h2>
+  <ul>
+    <li><a class="link" href="/spec">Documentation de l'API</a></li>
+    <li>
+      <a class="link" href="/storybook"
+        >Storybook (design des éléments visuels)</a
+      >
+    </li>
+  </ul>
+</main>
diff --git a/src/routes/dev/__layout.reset.svelte b/src/routes/dev/__layout.reset.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..87600dc80a0f83dc7581957723ceb4d1422d7261
--- /dev/null
+++ b/src/routes/dev/__layout.reset.svelte
@@ -0,0 +1,27 @@
+<script lang="ts">
+  import "@fontsource/lato/index.css"
+  import "@fontsource/lora/index.css"
+
+  import "../../global.css"
+
+  import { setContext } from "svelte"
+  import { writable } from "svelte/store"
+
+  import DevNavBar from "$lib/components/DevNavBar.svelte"
+
+  const reform = writable({})
+  setContext("reform", reform)
+
+  const simulationRequested = writable(false)
+  setContext("simulationRequested", simulationRequested)
+
+  const situationComplement = writable({})
+  setContext("situationComplement", situationComplement)
+
+  const situationCore = writable({})
+  setContext("situationCore", situationCore)
+</script>
+
+<DevNavBar />
+
+<slot />
diff --git a/src/routes/calculations/index.svelte b/src/routes/dev/calculations.svelte
similarity index 100%
rename from src/routes/calculations/index.svelte
rename to src/routes/dev/calculations.svelte
diff --git a/src/routes/entities/index.svelte b/src/routes/dev/entities.svelte
similarity index 100%
rename from src/routes/entities/index.svelte
rename to src/routes/dev/entities.svelte
diff --git a/src/routes/dev/index.svelte b/src/routes/dev/index.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..b6378b8e0e9ffe302bdcb92a67b1467dc11ada70
--- /dev/null
+++ b/src/routes/dev/index.svelte
@@ -0,0 +1,46 @@
+<script lang="ts">
+  import { session } from "$app/stores"
+
+  $: title = $session.title
+</script>
+
+<svelte:head>
+  <title>{title}</title>
+</svelte:head>
+
+<main>
+  <h1 class="text-2xl">{title}</h1>
+
+  <section class="mt-8">
+    <h1 class="mb-4 text-xl">Pages de "<em>production</em>"</h1>
+
+    <ul>
+      <li><a class="link" href="/">Page d'accueil (simulation)</a></li>
+    </ul>
+  </section>
+
+  <section class="mt-8">
+    <h1 class="mb-4 text-xl">Pages de développement et de test</h1>
+
+    <ul>
+      <li><a class="link" href="/dev">Cette page d'accueil</a></li>
+      <li><a class="link" href="/calculations">Calculs</a></li>
+      <li><a class="link" href="/entities">Entités</a></li>
+      <li><a class="link" href="/parameters">Paramètres</a></li>
+      <li><a class="link" href="/variables">Variables</a></li>
+    </ul>
+  </section>
+
+  <section class="mt-8">
+    <h1 class="mb-4 text-xl">Documentation</h1>
+
+    <ul>
+      <li><a class="link" href="/spec">Documentation de l'API</a></li>
+      <li>
+        <a class="link" href="/storybook"
+          >Storybook (design des éléments visuels)</a
+        >
+      </li>
+    </ul>
+  </section>
+</main>
diff --git a/src/routes/dev/parameters/[parameter].svelte b/src/routes/dev/parameters/[parameter].svelte
new file mode 100644
index 0000000000000000000000000000000000000000..1878b303c0bb5ef9c938ff4d0b769b9bf6961d9b
--- /dev/null
+++ b/src/routes/dev/parameters/[parameter].svelte
@@ -0,0 +1,48 @@
+<script context="module" lang="ts">
+  import type { LoadInput, LoadOutput } from "@sveltejs/kit/types/page"
+
+  import { improveParameterWithAncestors } from "$lib/parameters"
+
+  export async function load({
+    fetch,
+    page,
+    session,
+  }: LoadInput): Promise<LoadOutput> {
+    const { parameter: name } = page.params
+    const url = new URL(`parameters/${name}`, session.apiBaseUrl).toString()
+    const res = await fetch(url)
+    if (!res.ok) {
+      return {
+        status: res.status,
+        error: new Error(`Could not load ${url}`),
+      }
+    }
+    const parameterWithAncestors = await res.json()
+    return {
+      props: {
+        parameter: improveParameterWithAncestors(parameterWithAncestors),
+      },
+    }
+  }
+</script>
+
+<script lang="ts">
+  import { setContext } from "svelte"
+
+  import { session } from "$app/stores"
+  import ParameterView from "$lib/components/parameters/ParameterView.svelte"
+  import type { AnyParameter } from "$lib/parameters"
+  import { newSelfTargetAProps } from "$lib/urls"
+
+  export let parameter: AnyParameter
+
+  setContext("newSelfTargetAProps", newSelfTargetAProps)
+</script>
+
+<svelte:head>
+  <title>{parameter.name} | Paramètres | {$session.title}</title>
+</svelte:head>
+
+<main>
+  <ParameterView editable={true} {parameter} />
+</main>
diff --git a/src/routes/dev/parameters/index.svelte b/src/routes/dev/parameters/index.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..1784427605a87e8395f1a339576dc2b485f6c408
--- /dev/null
+++ b/src/routes/dev/parameters/index.svelte
@@ -0,0 +1,87 @@
+<script context="module" lang="ts">
+  import type { LoadInput, LoadOutput } from "@sveltejs/kit/types/page"
+
+  import type { AnyParameter, ParameterNode } from "$lib/parameters"
+  import { improveParameter } from "$lib/parameters"
+
+  export async function load({
+    fetch,
+    session,
+  }: LoadInput): Promise<LoadOutput> {
+    const url = new URL("parameters/", session.apiBaseUrl).toString()
+    const res = await fetch(url)
+    if (!res.ok) {
+      return {
+        status: res.status,
+        error: new Error(`Could not load ${url}`),
+      }
+    }
+    const rootParameter = await res.json()
+    improveParameter(null, rootParameter)
+    return {
+      props: {
+        rootParameter,
+      },
+    }
+  }
+</script>
+
+<script lang="ts">
+  import { goto } from "$app/navigation"
+  import { page, session } from "$app/stores"
+  import ParametersSearch from "$lib/components/parameters/ParametersSearch.svelte"
+
+  export let rootParameter: ParameterNode
+
+  let initialTerm: string | undefined = undefined
+
+  $: term = $page.query.get("q") ?? ""
+
+  function searchTermChanged({ detail }: { detail: string }) {
+    if (initialTerm === undefined) {
+      initialTerm = term
+    }
+    term = detail
+    history.replaceState(
+      null,
+      "",
+      `${$page.path}${term ? `?q=${encodeURIComponent(term)}` : ""}`,
+    )
+  }
+
+  async function parameterClicked({
+    detail: parameter,
+  }: {
+    detail: AnyParameter
+  }) {
+    if (initialTerm !== undefined) {
+      // Restore the initial term in browser history.
+      await goto(
+        `${$page.path}${
+          initialTerm ? `?q=${encodeURIComponent(initialTerm)}` : ""
+        }`,
+        { replaceState: true },
+      )
+      // Push the current term.
+      await goto(`${$page.path}${term ? `?q=${encodeURIComponent(term)}` : ""}`)
+    }
+    // Go to parameter page.
+    await goto(`/parameters/${parameter.name}`)
+  }
+</script>
+
+<svelte:head>
+  <title>Paramètres{term ? ` « ${term} »` : " "} | {$session.title}</title>
+</svelte:head>
+
+<main>
+  <h1>Paramètres</h1>
+
+  <ParametersSearch
+    dispatchItemClick={true}
+    on:change={searchTermChanged}
+    on:itemClick={parameterClicked}
+    {rootParameter}
+    {term}
+  />
+</main>
diff --git a/src/routes/dev/variables/[variable]/index.svelte b/src/routes/dev/variables/[variable]/index.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..a161cb81412992ca50033e75a31f6a5d0dc0b534
--- /dev/null
+++ b/src/routes/dev/variables/[variable]/index.svelte
@@ -0,0 +1,45 @@
+<script context="module" lang="ts">
+  import type { LoadInput, LoadOutput } from "@sveltejs/kit/types/page"
+
+  export async function load({
+    fetch,
+    page,
+    session,
+  }: LoadInput): Promise<LoadOutput> {
+    const { variable: name } = page.params
+    const url = new URL(`variables/${name}`, session.apiBaseUrl).toString()
+    const res = await fetch(url)
+    if (!res.ok) {
+      return {
+        status: res.status,
+        error: new Error(`Could not load ${url}`),
+      }
+    }
+    return {
+      props: {
+        variable: await res.json(),
+      },
+    }
+  }
+</script>
+
+<script lang="ts">
+  import { setContext } from "svelte"
+
+  import { session } from "$app/stores"
+  import VariableView from "$lib/components/variables/VariableView.svelte"
+  import { newSelfTargetAProps } from "$lib/urls"
+  import type { Variable } from "$lib/variables"
+
+  export let variable: Variable
+
+  setContext("newSelfTargetAProps", newSelfTargetAProps)
+</script>
+
+<svelte:head>
+  <title>{variable.name} | Variables | {$session.title}</title>
+</svelte:head>
+
+<main>
+  <VariableView editable={true} {variable} />
+</main>
diff --git a/src/routes/dev/variables/[variable]/inputs/[date].svelte b/src/routes/dev/variables/[variable]/inputs/[date].svelte
new file mode 100644
index 0000000000000000000000000000000000000000..5aabc155a082778b78f3d87b94f5876a1c130cec
--- /dev/null
+++ b/src/routes/dev/variables/[variable]/inputs/[date].svelte
@@ -0,0 +1,89 @@
+<script context="module" lang="ts">
+  import type { LoadInput, LoadOutput } from "@sveltejs/kit/types/page"
+
+  export async function load({
+    fetch,
+    page,
+    session,
+  }: LoadInput): Promise<LoadOutput> {
+    const { date, variable: name } = page.params
+    const results = await Promise.all([
+      (async () => {
+        const url = new URL(`variables/${name}`, session.apiBaseUrl).toString()
+        const res = await fetch(url)
+        if (!res.ok) {
+          return {
+            status: res.status,
+            error: new Error(`Could not load ${url}`),
+          }
+        }
+        return {
+          props: {
+            variable: await res.json(),
+          },
+        }
+      })(),
+      (async () => {
+        const url = new URL(
+          `variables/${name}/inputs/${date}`,
+          session.apiBaseUrl,
+        ).toString()
+        const res = await fetch(url)
+        if (!res.ok) {
+          return {
+            status: res.status,
+            error: new Error(`Could not load ${url}`),
+          }
+        }
+        return {
+          props: {
+            inputs: await res.json(),
+          },
+        }
+      })(),
+    ])
+    const firstResultWithError = results.find(
+      ({ error }) => error !== undefined,
+    )
+    if (firstResultWithError !== undefined) {
+      return firstResultWithError
+    }
+    return {
+      props: {
+        ...Object.fromEntries(
+          [].concat(...results.map(({ props }) => Object.entries(props))),
+        ),
+      },
+    }
+  }
+</script>
+
+<script lang="ts">
+  import { setContext } from "svelte"
+
+  import { page, session } from "$app/stores"
+  import VariableReferredInputs from "$lib/components/variables/VariableReferredInputs.svelte"
+  import { newSelfTargetAProps } from "$lib/urls"
+  import type { Variable } from "$lib/variables"
+
+  export let variable: Variable
+  export let inputs: Variable[]
+
+  setContext("newSelfTargetAProps", newSelfTargetAProps)
+
+  $: params = $page.params
+
+  $: date = params.date
+
+  $: name = params.variable
+</script>
+
+<svelte:head>
+  <title
+    >Variables d'entrée au {date} | {name} | Variables | {$session.title}</title
+  >
+</svelte:head>
+
+<main>
+  <VariableReferredInputs {inputs} {variable} />
+</main>
diff --git a/src/routes/dev/variables/[variable]/parameters/[date].svelte b/src/routes/dev/variables/[variable]/parameters/[date].svelte
new file mode 100644
index 0000000000000000000000000000000000000000..0736fc0b9d4936926385d71354506a5fc6a8291b
--- /dev/null
+++ b/src/routes/dev/variables/[variable]/parameters/[date].svelte
@@ -0,0 +1,95 @@
+<script context="module" lang="ts">
+  import type { LoadInput, LoadOutput } from "@sveltejs/kit/types/page"
+
+  import { improveParameterWithAncestors } from "$lib/parameters"
+
+  export async function load({
+    fetch,
+    page,
+    session,
+  }: LoadInput): Promise<LoadOutput> {
+    const { date, variable: name } = page.params
+    const results = await Promise.all([
+      (async () => {
+        const url = new URL(`variables/${name}`, session.apiBaseUrl).toString()
+        const res = await fetch(url)
+        if (!res.ok) {
+          return {
+            status: res.status,
+            error: new Error(`Could not load ${url}`),
+          }
+        }
+        return {
+          props: {
+            variable: await res.json(),
+          },
+        }
+      })(),
+      (async () => {
+        const url = new URL(
+          `variables/${name}/parameters/${date}`,
+          session.apiBaseUrl,
+        ).toString()
+        const res = await fetch(url)
+        if (!res.ok) {
+          return {
+            status: res.status,
+            error: new Error(`Could not load ${url}`),
+          }
+        }
+        const parametersWithAncestors = await res.json()
+        return {
+          props: {
+            parameters: parametersWithAncestors.map(
+              improveParameterWithAncestors,
+            ),
+          },
+        }
+      })(),
+    ])
+    const firstResultWithError = results.find(
+      ({ error }) => error !== undefined,
+    )
+    if (firstResultWithError !== undefined) {
+      return firstResultWithError
+    }
+    return {
+      props: {
+        ...Object.fromEntries(
+          [].concat(...results.map(({ props }) => Object.entries(props))),
+        ),
+      },
+    }
+  }
+</script>
+
+<script lang="ts">
+  import { setContext } from "svelte"
+
+  import { page, session } from "$app/stores"
+  import VariableReferredParameters from "$lib/components/variables/VariableReferredParameters.svelte"
+  import type { AnyParameter } from "$lib/parameters"
+  import { newSelfTargetAProps } from "$lib/urls"
+  import type { Variable } from "$lib/variables"
+
+  export let parameters: AnyParameter[]
+  export let variable: Variable
+
+  setContext("newSelfTargetAProps", newSelfTargetAProps)
+
+  $: params = $page.params
+
+  $: date = params.date
+
+  $: name = params.variable
+</script>
+
+<svelte:head>
+  <title
+    >Paramètres influant au {date} | {name} | Variables | {$session.title}</title
+  >
+</svelte:head>
+
+<main>
+  <VariableReferredParameters {date} {parameters} {variable} />
+</main>
diff --git a/src/routes/dev/variables/index.svelte b/src/routes/dev/variables/index.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..d60d4df8aded9f64dde3c7b6d0a3d6605f5880dc
--- /dev/null
+++ b/src/routes/dev/variables/index.svelte
@@ -0,0 +1,50 @@
+<script context="module" lang="ts">
+  import type { LoadInput, LoadOutput } from "@sveltejs/kit/types/page"
+
+  export async function load({
+    fetch,
+    session,
+  }: LoadInput): Promise<LoadOutput> {
+    const url = new URL("variables/", session.apiBaseUrl).toString()
+    const res = await fetch(url)
+    if (!res.ok) {
+      return {
+        status: res.status,
+        error: new Error(`Could not load ${url}`),
+      }
+    }
+    return {
+      props: {
+        variableByName: await res.json(),
+      },
+    }
+  }
+</script>
+
+<script lang="ts">
+  import { session } from "$app/stores"
+  import type { VariableSummaryByName } from "$lib/variables"
+
+  export let variableByName: VariableSummaryByName
+</script>
+
+<svelte:head>
+  <title>Variables | {$session.title}</title>
+</svelte:head>
+
+<main>
+  <h1>Variables</h1>
+
+  <ul class="list-disc list-inside">
+    {#each Object.entries(variableByName) as [name, variable]}
+      <li>
+        <a class="link" href="variables/{name}">
+          <var>{name}</var>
+          {#if variable.label !== undefined}
+            : {variable.label}
+          {/if}
+        </a>
+      </li>
+    {/each}
+  </ul>
+</main>
diff --git a/src/routes/index.svelte b/src/routes/index.svelte
index 2eadf70625ec4e31d05028d36e9aa6f5de7b19e3..f5f590a67e7ac9618ae4cafa9c0f0ba7415e5b48 100644
--- a/src/routes/index.svelte
+++ b/src/routes/index.svelte
@@ -1,31 +1,368 @@
 <script lang="ts">
-  import { session } from "$app/stores"
+  import { getContext, setContext } from "svelte"
+  import type { Writable } from "svelte/store"
+  import { writable } from "svelte/store"
+  import Sockette from "sockette"
 
-  $: title = $session.title
+  import { browser } from "$app/env"
+  import { page, session } from "$app/stores"
+  import { validateCalculationQuery } from "$lib/auditors/queries"
+  import type { ValidCalculationQuery } from "$lib/calculations"
+  import CalculationPane from "$lib/components/calculations/CalculationPane.svelte"
+  import type { Decomposition } from "$lib/decompositions"
+  import { decomposition as decompositionWithoutValue } from "$lib/decompositions"
+  import type { ReformChange } from "$lib/reforms"
+  import type { Axis, Situation, SituationComplement } from "$lib/situations"
+
+  let axes: Axis[][] = []
+  let deltaByCode: { [code: string]: number[] } = {}
+  let decomposition = writable(
+    updateDecompositionValues(
+      decompositionWithoutValue as Decomposition,
+      deltaByCode,
+      1, // vectorLength
+    ),
+  )
+  setContext("decomposition", decomposition)
+  const reform = getContext("reform") as Writable<ReformChange>
+  let showNulls = false
+  const simulationRequested = getContext(
+    "simulationRequested",
+  ) as Writable<boolean>
+  let situation: Situation | undefined = undefined
+  let situationComplement = getContext(
+    "situationComplement",
+  ) as Writable<SituationComplement>
+  let situationCore = getContext("situationCore") as Writable<Situation>
+  let vectorIndex = 0
+  let vectorLength = 1
+  let webSocket: Sockette | undefined = undefined
+  let webSocketOpen = false
+  let year = 2021
+
+  $: query = ensureValidQuery($page.query)
+
+  $: if (browser) {
+    openWebSocket()
+  }
+
+  $: if ($simulationRequested) {
+    $simulationRequested = false
+    submit()
+  }
+
+  function changeAxes({ detail }) {
+    axes = detail
+
+    vectorLength = 1
+    for (const parallelAxes of axes) {
+      // All the parallel axes have the same count.
+      const axis = parallelAxes[0]
+      vectorLength *= axis.count
+    }
+
+    $decomposition = updateDecompositionValues(
+      decompositionWithoutValue as Decomposition,
+      deltaByCode,
+      vectorLength,
+    )
+    if (webSocketOpen) {
+      submit()
+    }
+  }
+
+  function changeSituation({ detail }) {
+    situation = detail
+    $situationCore = situation
+    if (webSocketOpen) {
+      submit()
+    }
+  }
+
+  function changeVectorIndex({ detail }) {
+    vectorIndex = detail
+  }
+
+  function ensureValidQuery(query: URLSearchParams): ValidCalculationQuery {
+    const [validQuery, queryError] = validateCalculationQuery(query)
+    if (queryError !== null) {
+      console.warn(
+        `Query error at ${$page.path}: ${JSON.stringify(
+          queryError,
+          null,
+          2,
+        )}\n\n${JSON.stringify(validQuery, null, 2)}`,
+      )
+      return {}
+    }
+    return validQuery
+  }
+
+  function openWebSocket() {
+    webSocket = new Sockette(
+      new URL("simulations/calculate", $session.apiWebSocketBaseUrl).toString(),
+      {
+        // maxAttempts: 10,
+        onmessage: (event) => {
+          const result = JSON.parse(event.data)
+          if (result.error !== undefined) {
+            console.error("Error:", result)
+          } else {
+            deltaByCode = {
+              ...deltaByCode,
+              [result.code]: result.value,
+            }
+            $decomposition = updateDecompositionValues(
+              decompositionWithoutValue as Decomposition,
+              deltaByCode,
+              vectorLength,
+            )
+          }
+        },
+        // onopen: (event) => console.log("[WebSocket] Connected!", event),
+        onopen: () => {
+          webSocketOpen = true
+          submit()
+        },
+        // onreconnect: (event) =>
+        //   console.log("[WebSocket] Reconnecting...", event),
+        // onmaximum: (event) =>
+        //   console.log("[WebSocket] Stop Attempting!", event),
+        // onclose: (event) => console.log("[WebSocket] Closed!", event),
+        // onerror: (event) => console.log("[WebSocket] Error:", event),
+        // timeout: 5e3,
+      },
+    )
+  }
+
+  function submit() {
+    if (situation === undefined) {
+      return
+    }
+
+    let situationWithAxes = { ...situation }
+    for (const [entityPlural, entitySituationComplement] of Object.entries(
+      $situationComplement,
+    )) {
+      const entitySituation = (situationWithAxes[entityPlural] = {
+        ...situationWithAxes[entityPlural],
+      })
+      for (const [itemName, itemSituationComplement] of Object.entries(
+        entitySituationComplement,
+      )) {
+        const itemSituation = (entitySituation[itemName] = {
+          ...entitySituation[itemName],
+        })
+        for (const [variableName, variableValue] of Object.entries(
+          itemSituationComplement,
+        )) {
+          itemSituation[variableName] = { [year]: variableValue }
+        }
+      }
+    }
+
+    if (axes.length > 0) {
+      // Remove variables used as axes from situation (otherwise OpenFisca Core fails).
+      situationWithAxes = {
+        axes,
+        ...situationWithAxes,
+      }
+      for (const parallelAxes of axes) {
+        nextAxis: for (const axis of parallelAxes) {
+          const { name: code, index } = axis
+
+          let individuIndex = 0
+          for (let [name, individu] of Object.entries(
+            situationWithAxes.individus,
+          )) {
+            if (index === individuIndex && individu[code] !== undefined) {
+              individu = { ...individu }
+              delete individu[code]
+              situationWithAxes.individus = { ...situationWithAxes.individus }
+              situationWithAxes.individus[name] = individu // Preserve order of individu in individus.
+              continue nextAxis
+            }
+          }
+
+          let familleIndex = 0
+          for (let [name, famille] of Object.entries(
+            situationWithAxes.familles,
+          )) {
+            if (index === familleIndex && famille[code] !== undefined) {
+              famille = { ...famille }
+              delete famille[code]
+              situationWithAxes.familles = { ...situationWithAxes.familles }
+              situationWithAxes.familles[name] = famille // Preserve order of famille in familles.
+              continue nextAxis
+            }
+          }
+
+          let foyerFiscalIndex = 0
+          for (let [name, foyerFiscal] of Object.entries(
+            situationWithAxes.foyers_fiscaux,
+          )) {
+            if (index === foyerFiscalIndex && foyerFiscal[code] !== undefined) {
+              foyerFiscal = { ...foyerFiscal }
+              delete foyerFiscal[code]
+              situationWithAxes.foyers_fiscaux = {
+                ...situationWithAxes.foyers_fiscaux,
+              }
+              situationWithAxes.foyers_fiscaux[name] = foyerFiscal // Preserve order of foyerFiscal in foyersFiscaux.
+              continue nextAxis
+            }
+          }
+
+          let menageIndex = 0
+          for (let [name, menage] of Object.entries(
+            situationWithAxes.menages,
+          )) {
+            if (index === menageIndex && menage[code] !== undefined) {
+              menage = { ...menage }
+              delete menage[code]
+              situationWithAxes.menages = { ...situationWithAxes.menages }
+              situationWithAxes.menages[name] = menage // Preserve order of menage in menages.
+              continue nextAxis
+            }
+          }
+        }
+      }
+    }
+
+    webSocket.send(
+      JSON.stringify({
+        decomposition: $decomposition,
+        reform: $reform,
+        situation: situationWithAxes,
+        period: year.toString(),
+      }),
+    )
+    webSocket.send(
+      JSON.stringify({
+        calculate: true,
+      }),
+    )
+  }
+
+  function updateDecompositionValues(
+    node: Decomposition,
+    deltaByCode: { [code: string]: number[] },
+    vectorLength: number,
+    valuePrevious = undefined,
+  ): Decomposition {
+    if (valuePrevious === undefined) {
+      valuePrevious = new Array(vectorLength).fill(0)
+    }
+    let children = node.children
+    if (children !== undefined) {
+      children = []
+      let childValuePrevious = valuePrevious
+      for (let child of node.children) {
+        child = updateDecompositionValues(
+          child,
+          deltaByCode,
+          vectorLength,
+          childValuePrevious,
+        )
+        children.push(child)
+        childValuePrevious = child.values.map((itemValue) => itemValue[1])
+      }
+    }
+    let delta = deltaByCode[node.code]
+    if (delta === undefined) {
+      if (children === undefined) {
+        delta = new Array(vectorLength).fill(0)
+      } else {
+        const firstChildValues = children[0].values
+        const lastChildValues = children[children.length - 1].values
+        delta = lastChildValues.map(
+          (lastChildValue, index) =>
+            lastChildValue[1] - firstChildValues[index][0],
+        )
+      }
+    }
+    return {
+      ...node,
+      children,
+      delta,
+      values: valuePrevious.map((previousItemValue, index) => [
+        previousItemValue,
+        previousItemValue + delta[index],
+      ]),
+    }
+  }
 </script>
 
 <svelte:head>
-  <title>{title}</title>
+  <title>Calculs | {$session.title}</title>
 </svelte:head>
 
 <main>
-  <h1>{title}</h1>
-
-  <h2>Tests de l'API OpenFisca</h2>
-  <ul>
-    <li><a class="link" href="/calculations">Calculs</a></li>
-    <li><a class="link" href="/entities">Entités</a></li>
-    <li><a class="link" href="/parameters">Paramètres</a></li>
-    <li><a class="link" href="/variables">Variables</a></li>
-  </ul>
-
-  <h2>Documentation</h2>
-  <ul>
-    <li><a class="link" href="/spec">Documentation de l'API</a></li>
-    <li>
-      <a class="link" href="/storybook"
-        >Storybook (design des éléments visuels)</a
-      >
-    </li>
-  </ul>
+  <label class="block">
+    Année
+    <input max={2021} min={2013} step="1" type="number" bind:value={year} />
+  </label>
+
+  <div>
+    <button class="border rounded p-1" on:click={submit}>Simuler</button>
+  </div>
+
+  <div class="flex w-full">
+    <section class="overflow-auto relative w-1/3">
+      <CalculationPane
+        actions={query.pane1}
+        on:changeAxes={changeAxes}
+        on:changeSituation={changeSituation}
+        on:changeVectorIndex={changeVectorIndex}
+        pane="pane1"
+        {query}
+        {showNulls}
+        {vectorIndex}
+        {year}
+      />
+    </section>
+    <section class="overflow-auto relative w-2/3">
+      <CalculationPane
+        actions={query.pane2}
+        on:changeAxes={changeAxes}
+        on:changeSituation={changeSituation}
+        on:changeVectorIndex={changeVectorIndex}
+        pane="pane2"
+        {query}
+        {showNulls}
+        {vectorIndex}
+        {year}
+      />
+    </section>
+  </div>
+  <div class="flex w-full">
+    <section class="overflow-auto relative w-1/3">
+      <CalculationPane
+        actions={query.pane3}
+        on:changeAxes={changeAxes}
+        on:changeSituation={changeSituation}
+        on:changeVectorIndex={changeVectorIndex}
+        pane="pane3"
+        {query}
+        {showNulls}
+        {vectorIndex}
+        {year}
+      />
+    </section>
+    <section class="overflow-auto relative w-2/3">
+      <CalculationPane
+        actions={query.pane4}
+        on:changeAxes={changeAxes}
+        on:changeSituation={changeSituation}
+        on:changeVectorIndex={changeVectorIndex}
+        pane="pane4"
+        {query}
+        {showNulls}
+        {vectorIndex}
+        {year}
+      />
+    </section>
+  </div>
+  <label
+    ><input bind:checked={showNulls} type="checkbox" /> Montrer les montants nuls</label
+  >
 </main>