diff --git a/.gitignore b/.gitignore
index 0648667bdb50f03dbf8151eff52f71c863b4bcf7..ef6f322552824d51d4938c0b1be7ea2d68e4e69f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -95,43 +95,11 @@ local.properties
 *.ipr
 
 # User-specific stuff
-.idea/*
-.idea/**/workspace.xml
-.idea/**/tasks.xml
-.idea/**/usage.statistics.xml
-.idea/**/dictionaries
-.idea/**/shelf
-
-# Generated files
-.idea/**/contentModel.xml
-
-# Sensitive or high-churn files
-.idea/**/dataSources/
-.idea/**/dataSources.ids
-.idea/**/dataSources.local.xml
-.idea/**/sqlDataSources.xml
-.idea/**/dynamic.xml
-.idea/**/uiDesigner.xml
-.idea/**/dbnavigator.xml
-
-# Gradle
-.idea/**/gradle.xml
-.idea/**/libraries
-
-# Gradle and Maven with auto-import
-# When using Gradle or Maven with auto-import, you should exclude module files,
-# since they will be recreated, and may cause churn.  Uncomment if using
-# auto-import.
-# .idea/modules.xml
-# .idea/*.iml
-# .idea/modules
+.idea
 
 # CMake
 cmake-build-*/
 
-# Mongo Explorer plugin
-.idea/**/mongoSettings.xml
-
 # File-based project format
 *.iws
 
@@ -144,21 +112,12 @@ out/
 # JIRA plugin
 atlassian-ide-plugin.xml
 
-# Cursive Clojure plugin
-.idea/replstate.xml
-
 # Crashlytics plugin (for Android Studio and IntelliJ)
 com_crashlytics_export_strings.xml
 crashlytics.properties
 crashlytics-build.properties
 fabric.properties
 
-# Editor-based Rest Client
-.idea/httpRequests
-
-# Android studio 3.1+ serialized cache file
-.idea/caches/build_file_checksums.ser
-
 ### Intellij Patch ###
 # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
 
@@ -167,9 +126,6 @@ fabric.properties
 # .idea/misc.xml
 # *.ipr
 
-# Sonarlint plugin
-.idea/sonarlint
-
 ### Kotlin ###
 # Compiled class file
 *.class
@@ -270,3 +226,4 @@ gradle-app.setting
 
 # End of https://www.gitignore.io/api/gradle,eclipse,intellij,visualstudiocode,kotlin,git,macos,linux
 frontend/package-lock.json
+/web/node_modules/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b5511df7904d649e1cf5dfc50a6bd2c50ad4e3bc..7df3480415c742159af12bd1c3e36c778000c472 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -30,8 +30,6 @@ cache:
   key: "$CI_COMMIT_REF_NAME"
   paths:
     - ".gradle"
-    - "frontend/.gradle/"
-    - "frontend/node_modules/"
 
 # PRE-BUILD
 
@@ -62,20 +60,6 @@ build-loader-docker-image:
 # TESTS
 
 
-lint:
-  stage: test
-  tags:
-    - openstack
-  script: "./gradlew lint"
-  cache:
-    key: "$CI_COMMIT_REF_NAME"
-    policy: pull
-    paths:
-      - ".gradle"
-      - "frontend/.gradle/"
-      - "frontend/node_modules/"
-
-
 test-and-sonarqube:
   stage: test
   tags:
@@ -104,20 +88,19 @@ test-and-sonarqube:
     policy: pull-push
     paths:
       - ".gradle"
-      - "frontend/.gradle/"
-      - "frontend/node_modules/"
       - .sonar/cache
   script:
-    - ./gradlew :frontend:assemble --parallel
-    - ./gradlew :backend:test jacocoTestReport --parallel
-    - find /tmp/node/*/bin -name node -exec ln -s {} /tmp/node/node \;
-    - export PATH="/tmp/node/:$PATH"
-    - ./gradlew -s sonarqube -x test
+    - ./gradlew test jacocoTestReport --parallel
+    # disable sonarqube because it apparently needs node, but I don't know why, and it can't find it anymore now that
+    # there is no frontend project anymore, and it takes sooooo much time to complete anyway for results that nobody
+    # will ever look
+    # - find /tmp/node/*/bin -name node -exec ln -s {} /tmp/node/node \;
+    # - export PATH="/tmp/node/:$PATH"
+    # - ./gradlew -s sonarqube -x test
   artifacts:
     reports:
       junit:
         - ./backend/build/test-results/test/TEST-*.xml
-        # - ./frontend/karma-junit-tests-report/TEST*.xml
   only:
     refs:
       - merge_requests
@@ -144,8 +127,6 @@ build:
     policy: pull
     paths:
       - ".gradle"
-      - "frontend/.gradle/"
-      - "frontend/node_modules/"
   artifacts:
     paths:
       - "$JAR_PATH"
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index ca99a00edbda7ccc394869431fec04f9a5ac33a6..0000000000000000000000000000000000000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="CompilerConfiguration">
-    <annotationProcessing>
-      <profile name="Gradle Imported" enabled="true">
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="false">
-          <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-configuration-processor/2.1.2.RELEASE/db9671c321defb942a6700fae8a7700a137a25e/spring-boot-configuration-processor-2.1.2.RELEASE.jar" />
-        </processorPath>
-        <module name="faidare.backend.main" />
-      </profile>
-    </annotationProcessing>
-    <bytecodeTargetLevel target="1.8" />
-  </component>
-</project>
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
deleted file mode 100644
index 15a15b218a29e09c9190992732698d646e4d659a..0000000000000000000000000000000000000000
--- a/.idea/encodings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="Encoding" addBOMForNewFiles="with NO BOM" />
-</project>
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index ff8249e49968d1849269b0d8a2fe49a31c72a63f..0000000000000000000000000000000000000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="FrameworkDetectionExcludesConfiguration">
-    <file type="web" url="file://$PROJECT_DIR$" />
-  </component>
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
-    <output url="file://$PROJECT_DIR$/out" />
-  </component>
-</project>
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 3b2a562bc3214c46382cc1761196d85196a94d66..0000000000000000000000000000000000000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="ProjectModuleManager">
-    <modules>
-      <module fileurl="file://$PROJECT_DIR$/.idea/modules/faidare.iml" filepath="$PROJECT_DIR$/.idea/modules/faidare.iml" />
-      <module fileurl="file://$PROJECT_DIR$/.idea/modules/backend/faidare.backend.iml" filepath="$PROJECT_DIR$/.idea/modules/backend/faidare.backend.iml" />
-      <module fileurl="file://$PROJECT_DIR$/.idea/modules/backend/faidare.backend.main.iml" filepath="$PROJECT_DIR$/.idea/modules/backend/faidare.backend.main.iml" />
-      <module fileurl="file://$PROJECT_DIR$/.idea/modules/backend/faidare.backend.test.iml" filepath="$PROJECT_DIR$/.idea/modules/backend/faidare.backend.test.iml" />
-      <module fileurl="file://$PROJECT_DIR$/.idea/modules/frontend/faidare.frontend.iml" filepath="$PROJECT_DIR$/.idea/modules/frontend/faidare.frontend.iml" />
-      <module fileurl="file://$PROJECT_DIR$/backend/src/main/main.iml" filepath="$PROJECT_DIR$/backend/src/main/main.iml" />
-      <module fileurl="file://$PROJECT_DIR$/backend/src/test/test.iml" filepath="$PROJECT_DIR$/backend/src/test/test.iml" />
-    </modules>
-  </component>
-</project>
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7f4cb416c083d265558da75d457237d671..0000000000000000000000000000000000000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="VcsDirectoryMappings">
-    <mapping directory="$PROJECT_DIR$" vcs="Git" />
-  </component>
-</project>
\ No newline at end of file
diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts
index 9ff140544f08a54151ddf9ca56e164de76e37a35..64acb331b41fea7957dcb7ea7c0d7713e561dd43 100644
--- a/backend/build.gradle.kts
+++ b/backend/build.gradle.kts
@@ -1,11 +1,8 @@
 import org.gradle.api.tasks.testing.logging.TestExceptionFormat
 import org.springframework.boot.gradle.tasks.buildinfo.BuildInfo
-import org.springframework.boot.gradle.tasks.bundling.BootJar
-import org.springframework.boot.gradle.tasks.run.BootRun
 
 buildscript {
     repositories {
-        mavenLocal()
         mavenCentral()
     }
 }
@@ -20,7 +17,6 @@ plugins {
     id("org.owasp.dependencycheck") version "6.0.3"
 }
 
-
 java {
     sourceCompatibility = JavaVersion.VERSION_1_8
 }
@@ -40,7 +36,7 @@ tasks {
         options.compilerArgs.add("-parameters")
     }
 
-    getByName<Copy>("processResources") {
+    processResources {
         inputs.property("app", "gnpis")
 
         filesMatching("bootstrap.yml") {}
@@ -51,25 +47,44 @@ tasks {
     // but it's better to do that than using the bootInfo() method of the springBoot closure, because that
     // makes the test task out of date, which makes the build much longer.
     // See https://github.com/spring-projects/spring-boot/issues/13152
-    val buildInfo by creating(BuildInfo::class) {
+    val buildInfo by registering(BuildInfo::class) {
         destinationDir = file("$buildDir/buildInfo")
     }
 
-    val bootJar by getting(BootJar::class) {
+    bootJar {
         archiveName = "${rootProject.name}.jar"
-        dependsOn(":frontend:assemble")
         dependsOn(buildInfo)
-
-        into("BOOT-INF/classes/static") {
-            from("${project(":frontend").projectDir}/dist/frontend")
+        dependsOn(":web:assemble")
+
+        // replace the script.js and style.css file names referenced in main.html
+        // by their actual name, containing the content hash
+        filesMatching("**/layout/main.html") {
+            val webAssetsDir = project(":web").file("build/dist/assets/");
+            val scriptFileName = webAssetsDir.list().first { it.startsWith("script") && it.endsWith(".js") }
+            val styleFileName = webAssetsDir.list().first { it.startsWith("style") && it.endsWith(".css") }
+
+            filter { line ->
+                if (line.contains("script.js")) {
+                    line.replace("script.js", scriptFileName)
+                }
+                else if (line.contains("style.css")) {
+                    line.replace("style.css", styleFileName)
+                } else {
+                    line
+                }
+            }
         }
+
         into("BOOT-INF/classes/META-INF") {
-            from(buildInfo.destinationDir)
+            from(buildInfo.map { it.destinationDir })
+        }
+        into("BOOT-INF/classes/static") {
+            from(project(":web").file("build/dist"))
         }
         launchScript()
     }
 
-    val test by getting(Test::class) {
+    test {
         useJUnitPlatform()
         testLogging {
             exceptionFormat = TestExceptionFormat.FULL
@@ -77,7 +92,7 @@ tasks {
         outputs.dir(snippetsDir)
     }
 
-    val jacocoTestReport by getting(JacocoReport::class) {
+    jacocoTestReport {
         reports {
             xml.setEnabled(true)
             html.setEnabled(true)
@@ -98,6 +113,7 @@ dependencies {
     implementation("org.springframework.boot:spring-boot-starter-actuator")
     implementation("org.springframework.boot:spring-boot-starter-security")
     implementation("org.springframework.cloud:spring-cloud-starter-config")
+    implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
 
     // Elasticsearch
     implementation("org.elasticsearch:elasticsearch:6.6.2")
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/domain/xref/XRefDocumentSearchCriteria.java b/backend/src/main/java/fr/inra/urgi/faidare/domain/xref/XRefDocumentSearchCriteria.java
index 7ca7b26afe0f212fa6c997315c92214409f74b88..6dde515e2d0fa5fedf1d2b36d7d2b30de43dd5f5 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/domain/xref/XRefDocumentSearchCriteria.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/domain/xref/XRefDocumentSearchCriteria.java
@@ -4,6 +4,7 @@ import fr.inra.urgi.faidare.domain.criteria.base.PaginationCriteriaImpl;
 import fr.inra.urgi.faidare.elasticsearch.criteria.annotation.CriteriaForDocument;
 import fr.inra.urgi.faidare.elasticsearch.criteria.annotation.DocumentPath;
 
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -18,6 +19,12 @@ public class XRefDocumentSearchCriteria extends PaginationCriteriaImpl {
     @DocumentPath("linkedRessourcesID")
 	private List<String> linkedRessourcesID;
 
+    public static XRefDocumentSearchCriteria forXRefId(String resourceId) {
+        XRefDocumentSearchCriteria criteria = new XRefDocumentSearchCriteria();
+        criteria.setLinkedRessourcesID(Collections.singletonList(resourceId));
+        return criteria;
+    }
+
     public String getEntryType() {
         return entryType;
     }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/filter/AngularRouteFilter.java b/backend/src/main/java/fr/inra/urgi/faidare/filter/AngularRouteFilter.java
deleted file mode 100644
index 81978e5f3bfaca602e70eff0f109477d55e1466a..0000000000000000000000000000000000000000
--- a/backend/src/main/java/fr/inra/urgi/faidare/filter/AngularRouteFilter.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package fr.inra.urgi.faidare.filter;
-
-import com.google.common.base.Charsets;
-import com.google.common.io.ByteSource;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.core.io.ResourceLoader;
-import org.springframework.stereotype.Component;
-
-import javax.servlet.*;
-import javax.servlet.annotation.WebFilter;
-import javax.servlet.http.HttpServletRequest;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Arrays;
-
-/**
- * Filter that intercepts all request to potential Angular routes
- * (ex: /studies/ID) to send back the Angular `index.html` file with a correct
- * base href set to the spring server context path.
- *
- * Potential angular routes are devised by process of elimination:
- * - They should be GET requests
- * - They should not end with common static file suffixes {@link AngularRouteFilter#STATIC_SUFFIXES}
- * - They should not start with API prefixes {@link AngularRouteFilter#API_PREFIXES}
- *
- * <p>
- * Adapted from data-discovery
- *
- * @author gcornut
- */
-@Component
-@WebFilter("/*")
-public class AngularRouteFilter implements Filter {
-
-    private static final String[] API_PREFIXES = {
-        "/brapi/v1", "/faidare/v1", "/actuator", "/v2/api-docs", "/swagger-resources"
-    };
-
-    private static final String[] STATIC_SUFFIXES = {
-        ".html", ".js", ".css", ".ico", ".png", ".jpg", ".gif", ".eot", ".svg",
-        ".woff2", ".ttf", ".woff", ".md"
-    };
-
-    @Value("${server.servlet.context-path}")
-    private String serverContextPath;
-
-    private final ResourceLoader resourceLoader;
-
-    @Autowired
-    public AngularRouteFilter(ResourceLoader resourceLoader) {
-        this.resourceLoader = resourceLoader;
-    }
-
-    @Override
-    public void doFilter(
-        ServletRequest req,
-        ServletResponse response,
-        FilterChain chain
-    ) throws IOException, ServletException {
-        HttpServletRequest request = (HttpServletRequest) req;
-
-        if (isAngularRoute(request)) {
-            // Angular route
-            InputStream inputStream = resourceLoader.getResource("classpath:static/index.html").getInputStream();
-
-            ByteSource byteSource = new ByteSource() {
-                @Override
-                public InputStream openStream() {
-                    return inputStream;
-                }
-            };
-
-            String content = byteSource.asCharSource(Charsets.UTF_8).read();
-            String replacedContent = content.replace(
-                "<base href=\"./\">",
-                "<base href=\"" + serverContextPath + "/\">"
-            );
-            response.getWriter().write(replacedContent);
-            return;
-        }
-
-        // Otherwise nothing to do
-        chain.doFilter(request, response);
-    }
-
-    private boolean isAngularRoute(HttpServletRequest request) {
-        if (!request.getMethod().equals("GET")) {
-            return false;
-        }
-
-        String fullUri = request.getRequestURI();
-        String contextPath = request.getContextPath();
-        String uri = fullUri.substring(contextPath.length());
-
-        return !isApiOrStaticResource(uri);
-    }
-
-    private boolean isApiOrStaticResource(String relativePath) {
-        // Starts with API prefix
-        return Arrays.stream(API_PREFIXES).anyMatch(relativePath::startsWith)
-            // or has static file suffix
-            || Arrays.stream(STATIC_SUFFIXES).anyMatch(relativePath::endsWith);
-    }
-
-    @Override
-    public void init(FilterConfig filterConfig) {
-        // nothing to do
-    }
-
-    @Override
-    public void destroy() {
-        // nothing to do
-    }
-}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java b/backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java
new file mode 100644
index 0000000000000000000000000000000000000000..67fde44b8117296f13defc599adc6dcd0cd970a3
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java
@@ -0,0 +1,14 @@
+package fr.inra.urgi.faidare.utils;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * Utilities for sites
+ * @author JB Nizet
+ */
+public class Sites {
+    public static String siteIdToLocationId(String siteId) {
+        return Base64.getUrlEncoder().encodeToString(("urn:URGI/location/" + siteId).getBytes(StandardCharsets.US_ASCII));
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/HomeController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/HomeController.java
new file mode 100644
index 0000000000000000000000000000000000000000..6734b6aa97a2ae445af4eb61be86a07883b2a58a
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/HomeController.java
@@ -0,0 +1,19 @@
+package fr.inra.urgi.faidare.web;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ * Controller for the home page, which doesn't display much
+ * @author JB Nizet
+ */
+@Controller
+@RequestMapping("")
+public class HomeController {
+    @GetMapping
+    public ModelAndView home() {
+        return new ModelAndView("index");
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java
new file mode 100644
index 0000000000000000000000000000000000000000..cd24d06dc61698b217db1422c9585ed4b7b97b05
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java
@@ -0,0 +1,412 @@
+package fr.inra.urgi.faidare.web.germplasm;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import fr.inra.urgi.faidare.api.NotFoundException;
+import fr.inra.urgi.faidare.config.FaidareProperties;
+import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiGermplasmAttributeValue;
+import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiSibling;
+import fr.inra.urgi.faidare.domain.criteria.GermplasmAttributeCriteria;
+import fr.inra.urgi.faidare.domain.criteria.GermplasmGETSearchCriteria;
+import fr.inra.urgi.faidare.domain.data.germplasm.CollPopVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.DonorVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.GenealogyVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmAttributeValueVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmInstituteVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.InstituteVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.PedigreeVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.PhotoVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.PuiNameValueVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.SiblingVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.SimpleVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.SiteVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.TaxonSourceVO;
+import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+import fr.inra.urgi.faidare.repository.es.GermplasmAttributeRepository;
+import fr.inra.urgi.faidare.repository.es.GermplasmRepository;
+import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ * Controller used to display a germplasm card based on its ID.
+ * @author JB Nizet
+ */
+@Controller("webGermplasmController")
+@RequestMapping("/germplasms")
+public class GermplasmController {
+
+    private final GermplasmRepository germplasmRepository;
+    private final FaidareProperties faidareProperties;
+    private final XRefDocumentRepository xRefDocumentRepository;
+    private GermplasmAttributeRepository germplasmAttributeRepository;
+
+    public GermplasmController(GermplasmRepository germplasmRepository,
+                               FaidareProperties faidareProperties,
+                               XRefDocumentRepository xRefDocumentRepository,
+                               GermplasmAttributeRepository germplasmAttributeRepository) {
+        this.germplasmRepository = germplasmRepository;
+        this.faidareProperties = faidareProperties;
+        this.xRefDocumentRepository = xRefDocumentRepository;
+        this.germplasmAttributeRepository = germplasmAttributeRepository;
+    }
+
+    @GetMapping("/{germplasmId}")
+    public ModelAndView get(@PathVariable("germplasmId") String germplasmId) {
+        // GermplasmVO germplasm = germplasmRepository.getById(germplasmId);
+
+        // TODO replace this block by the above commented one
+        GermplasmVO germplasm = createGermplasm();
+
+        if (germplasm == null) {
+            throw new NotFoundException("Germplasm with ID " + germplasmId + " not found");
+        }
+
+        return toModelAndView(germplasm);
+    }
+
+    @GetMapping(params = "pui")
+    public ModelAndView getByPui(@RequestParam("pui") String pui) {
+        GermplasmGETSearchCriteria criteria = new GermplasmGETSearchCriteria();
+        criteria.setGermplasmPUI(Collections.singletonList(pui));
+        List<GermplasmVO> germplasms = germplasmRepository.find(criteria);
+        if (germplasms.size() != 1) {
+            throw new NotFoundException("Germplasm with PUI " + pui + " not found");
+        }
+
+        return toModelAndView(germplasms.get(0));
+    }
+
+    private ModelAndView toModelAndView(GermplasmVO germplasm) {
+        // List<BrapiGermplasmAttributeValue> attributes = getAttributes(germplasm);
+        // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find(
+        //     XRefDocumentSearchCriteria.forXRefId(site.getLocationDbId()));
+        // PedigreeVO pedigree = getPedigree(germplasm);
+        // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find(
+        //     XRefDocumentSearchCriteria.forXRefId(germplasm.getGermplasmDbId())
+        // );
+
+        // TODO replace this block by the above commented one
+        List<BrapiGermplasmAttributeValue> attributes = Arrays.asList(
+            createAttribute()
+        );
+        PedigreeVO pedigree = createPedigree();
+        List<XRefDocumentVO> crossReferences = Arrays.asList(
+            createXref("foobar"),
+            createXref("bazbing")
+        );
+
+        sortDonors(germplasm);
+        sortPopulations(germplasm);
+        sortCollections(germplasm);
+        sortPanels(germplasm);
+        return new ModelAndView("germplasm",
+                                "model",
+                                new GermplasmModel(
+                                    germplasm,
+                                    faidareProperties.getByUri(germplasm.getSourceUri()),
+                                    attributes,
+                                    pedigree,
+                                    crossReferences)
+        );
+    }
+
+    private void sortPopulations(GermplasmVO germplasm) {
+        if (germplasm.getPopulation() != null) {
+            germplasm.setPopulation(germplasm.getPopulation()
+                                             .stream()
+                                             .sorted(Comparator.comparing(
+                                                 CollPopVO::getName))
+                                             .collect(Collectors.toList()));
+        }
+    }
+
+    private void sortCollections(GermplasmVO germplasm) {
+        if (germplasm.getCollection() != null) {
+            germplasm.setCollection(germplasm.getCollection()
+                                             .stream()
+                                             .sorted(Comparator.comparing(CollPopVO::getName))
+                                             .collect(Collectors.toList()));
+        }
+    }
+
+    private void sortPanels(GermplasmVO germplasm) {
+        if (germplasm.getPanel() != null) {
+            germplasm.setPanel(germplasm.getPanel()
+                                        .stream()
+                                        .sorted(Comparator.comparing(CollPopVO::getName))
+                                        .collect(Collectors.toList()));
+        }
+    }
+
+    private void sortDonors(GermplasmVO germplasm) {
+        if (germplasm.getDonors() != null) {
+            germplasm.setDonors(germplasm.getDonors()
+                                         .stream()
+                                         .sorted(Comparator.comparing(donor -> donor.getDonorInstitute()
+                                                                                    .getInstituteName()))
+                                         .collect(Collectors.toList()));
+        }
+    }
+
+    private List<BrapiGermplasmAttributeValue> getAttributes(GermplasmVO germplasm) {
+        GermplasmAttributeCriteria criteria = new GermplasmAttributeCriteria();
+        criteria.setGermplasmDbId(germplasm.getGermplasmDbId());
+        return germplasmAttributeRepository.find(criteria)
+            .stream()
+            .flatMap(vo -> vo.getData().stream())
+            .sorted(Comparator.comparing(BrapiGermplasmAttributeValue::getAttributeName))
+            .collect(Collectors.toList());
+    }
+
+    private PedigreeVO getPedigree(GermplasmVO germplasm) {
+        return germplasmRepository.findPedigree(germplasm.getGermplasmDbId());
+    }
+
+    private BrapiGermplasmAttributeValue createAttribute() {
+        GermplasmAttributeValueVO result = new GermplasmAttributeValueVO();
+        result.setAttributeName("A1");
+        result.setValue("V1");
+        return result;
+    }
+
+    private GermplasmVO createGermplasm() {
+        GermplasmVO result = new GermplasmVO();
+
+        result.setGermplasmName("BLE BARBU DU ROUSSILLON");
+        result.setAccessionNumber("1408");
+        result.setSynonyms(Arrays.asList("BLE DU ROUSSILLON", "FRA051:1699", "ROUSSILLON"));
+        PhotoVO photo = new PhotoVO();
+        photo.setPhotoName("Blé du roussillon");
+        photo.setCopyright("INRA, Emmanuelle BOULAT/Lionel BARDY 2012");
+        photo.setThumbnailFile("https://urgi.versailles.inrae.fr/files/siregal/images/accession/CEREALS/thumbnails/thumb_1408_R09_S.jpg");
+        photo.setFile("https://urgi.versailles.inrae.fr/files/siregal/images/accession/CEREALS/1408_R09_S.jpg");
+        result.setPhoto(photo);
+
+        InstituteVO holdingGenBank = new InstituteVO();
+        holdingGenBank.setLogo("https://urgi.versailles.inra.fr/files/siregal/images/grc/inra_brc_en.png");
+        holdingGenBank.setInstituteName("INRA BRC");
+        holdingGenBank.setWebSite("http://google.fr");
+        result.setHoldingGenbank(holdingGenBank);
+
+        result.setBiologicalStatusOfAccessionCode("Traditional cultivar/landrace ");
+        result.setPedigree("LV");
+        SiteVO originSite = new SiteVO();
+        originSite.setSiteId("1234");
+        originSite.setSiteName("Le Moulon");
+        originSite.setSiteType("Origin site");
+        originSite.setLatitude(47.0F);
+        originSite.setLongitude(12.0F);
+        result.setOriginSite(originSite);
+
+        List<SiteVO> evaluationSites = new ArrayList<>();
+        for (int i = 0; i < 5; i++) {
+            SiteVO evaluationSite = new SiteVO();
+            evaluationSite.setSiteId(Integer.toString(12347 + i));
+            evaluationSite.setSiteType("Evaluation site");
+            evaluationSite.setSiteName("Site " + i);
+            evaluationSite.setLatitude(46.0F + i);
+            evaluationSite.setLongitude(13.0F + i);
+            evaluationSites.add(evaluationSite);
+        }
+        result.setEvaluationSites(evaluationSites);
+
+        result.setGenus("Genus 1");
+        result.setSpecies("Species 1");
+        result.setSpeciesAuthority("Species Auth");
+        result.setSourceUri("https://urgi.versailles.inrae.fr/gnpis");
+        result.setSubtaxa("Subtaxa 1");
+        result.setGenusSpeciesSubtaxa("Triticum aestivum subsp. aestivum");
+        result.setSubtaxaAuthority("INRAE");
+        result.setTaxonIds(Arrays.asList(createTaxonId(), createTaxonId()));
+        result.setTaxonComment("C'est bon le blé");
+        result.setTaxonCommonNames(Arrays.asList("Blé tendre", "Bread wheat", "Soft wheat"));
+        result.setTaxonSynonyms(Arrays.asList("Blé tendre1", "Bread wheat1", "Soft wheat1"));
+
+        InstituteVO holdingInstitute = new InstituteVO();
+        holdingInstitute.setInstituteName("GDEC - UMR Génétique, Diversité et Ecophysiologie des Céréales");
+        holdingInstitute.setLogo("https://urgi.versailles.inra.fr/files/siregal/images/grc/inra_brc_en.png");
+        holdingInstitute.setWebSite("https://google.fr/q=qsdqsdqsdslqlsdnqlsdqlsdlqskdlqdqlsdqsdqsdqd");
+        holdingInstitute.setInstituteCode("GDEC");
+        holdingInstitute.setInstituteType("Type1");
+        holdingInstitute.setAcronym("G.D.E.C");
+        holdingInstitute.setAddress("Lyon");
+        holdingInstitute.setOrganisation("SAS");
+        result.setHoldingInstitute(holdingInstitute);
+
+        result.setPresenceStatus("Maintained");
+
+        GermplasmInstituteVO collector = new GermplasmInstituteVO();
+        collector.setMaterialType("Fork");
+        collector.setCollectors("Joe, Jack, William, Averell");
+        InstituteVO collectingInstitute = new InstituteVO();
+        collectingInstitute.setInstituteName("Ninja Squad");
+        collector.setInstitute(collectingInstitute);
+        collector.setAccessionNumber("567");
+        result.setCollector(collector);
+
+        SiteVO collectingSite = new SiteVO();
+        collectingSite.setSiteId("1235");
+        collectingSite.setSiteName("St Just");
+        collectingSite.setSiteType("Collecting site");
+        collectingSite.setLatitude(48.0F);
+        collectingSite.setLongitude(13.0F);
+        result.setCollectingSite(collectingSite);
+        result.setAcquisitionDate("In the summer");
+
+        GermplasmInstituteVO breeder = new GermplasmInstituteVO();
+        InstituteVO breedingInstitute = new InstituteVO();
+        breedingInstitute.setInstituteName("Microsoft");
+        breeder.setInstitute(breedingInstitute);
+        breeder.setAccessionCreationDate(2015);
+        breeder.setAccessionNumber("678");
+        breeder.setRegistrationYear(2016);
+        breeder.setDeregistrationYear(2019);
+        result.setBreeder(breeder);
+
+        result.setDonors(Arrays.asList(
+            createDonor()
+        ));
+
+        result.setDistributors(Arrays.asList(
+            createDistributor()
+        ));
+
+        result.setChildren(Arrays.asList(createChild(), createChild()));
+
+        result.setGermplasmPUI("germplasmPUI");
+        result.setPopulation(Arrays.asList(createPopulation1(), createPopulation2(), createPopulation3()));
+
+        result.setCollection(Arrays.asList(createCollection()));
+
+        result.setPanel(Arrays.asList(createPanel()));
+
+        return result;
+    }
+
+    private DonorVO createDonor() {
+        DonorVO result = new DonorVO();
+        result.setDonorGermplasmPUI("PUI1");
+        result.setDonationDate(2017);
+        result.setDonorAccessionNumber("3456");
+        result.setDonorInstituteCode("GD46U");
+        InstituteVO institute = new InstituteVO();
+        institute.setInstituteName("Hello");
+        result.setDonorInstitute(institute);
+        return result;
+    }
+
+    private GermplasmInstituteVO createDistributor() {
+        GermplasmInstituteVO result = new GermplasmInstituteVO();
+        InstituteVO institute = new InstituteVO();
+        institute.setInstituteName("Microsoft");
+        result.setInstitute(institute);
+        result.setAccessionNumber("678");
+        result.setDistributionStatus("OK");
+        return result;
+    }
+
+    private PedigreeVO createPedigree() {
+        PedigreeVO result = new PedigreeVO();
+        result.setPedigree("Pedigree 1");
+        result.setParent1DbId("12345");
+        result.setParent1Name("Parent 1");
+        result.setParent1Type("P1");
+        result.setParent2DbId("12346");
+        result.setParent2Name("Parent 2");
+        result.setParent2Type("P2");
+        result.setCrossingPlan("crossing plan 1");
+        result.setCrossingYear("2012");
+        result.setSiblings(Arrays.asList(createBrapiSibling()));
+        return result;
+    }
+
+    private BrapiSibling createBrapiSibling() {
+        SiblingVO sibling = new SiblingVO();
+        sibling.setGermplasmDbId("5678");
+        sibling.setDefaultDisplayName("Sibling 5678");
+        return sibling;
+    }
+
+    private GenealogyVO createChild() {
+        GenealogyVO result = new GenealogyVO();
+        result.setFirstParentName("CP1");
+        result.setSecondParentName("CP2");
+        result.setSibblings(Arrays.asList(createPuiNameValueVO(), createPuiNameValueVO()));
+        return result;
+    }
+
+    private PuiNameValueVO createPuiNameValueVO() {
+        PuiNameValueVO result = new PuiNameValueVO();
+        result.setName("Child 1");
+        result.setPui("pui1");
+        return result;
+    }
+
+    private CollPopVO createPopulation1() {
+        CollPopVO result = new CollPopVO();
+        result.setName("Population 1");
+        result.setType("Pop Type 1");
+        result.setGermplasmCount(3);
+        result.setGermplasmRef(createPuiNameValueVO());
+        return result;
+    }
+
+    private CollPopVO createPopulation2() {
+        CollPopVO result = new CollPopVO();
+        result.setName("Population 2");
+        result.setGermplasmCount(3);
+        PuiNameValueVO puiNameValueVO = createPuiNameValueVO();
+        puiNameValueVO.setPui("germplasmPUI");
+        result.setGermplasmRef(puiNameValueVO);
+        return result;
+    }
+
+    private CollPopVO createPopulation3() {
+        CollPopVO result = new CollPopVO();
+        result.setName("Population 3");
+        result.setGermplasmCount(5);
+        return result;
+    }
+
+    private CollPopVO createCollection() {
+        CollPopVO result = new CollPopVO();
+        result.setName("Collection 1");
+        result.setGermplasmCount(7);
+        return result;
+    }
+
+    private CollPopVO createPanel() {
+        CollPopVO result = new CollPopVO();
+        result.setName("The_panel_1");
+        result.setGermplasmCount(2);
+        return result;
+    }
+
+    private TaxonSourceVO createTaxonId() {
+        TaxonSourceVO result = new TaxonSourceVO();
+        result.setTaxonId("taxon1");
+        result.setSourceName("ThePlantList");
+        return result;
+    }
+
+    private XRefDocumentVO createXref(String name) {
+        XRefDocumentVO xref = new XRefDocumentVO();
+        xref.setName(name);
+        xref.setDescription("A very large description for the xref " + name + " which has way more than 120 characters bla bla bla bla bla bla bla bla bla bla bla bla");
+        xref.setDatabaseName("db_" + name);
+        xref.setUrl("https://google.com");
+        xref.setEntryType("type " + name);
+        return xref;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..ffa24336f395b2d00817babacc09b38981318178
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java
@@ -0,0 +1,157 @@
+package fr.inra.urgi.faidare.web.germplasm;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiGermplasmAttributeValue;
+import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmInstituteVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.PedigreeVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.SiteVO;
+import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource;
+import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+import fr.inra.urgi.faidare.web.site.MapLocation;
+import org.apache.logging.log4j.util.Strings;
+
+/**
+ * The model used by the germplasm page
+ * @author JB Nizet
+ */
+public final class GermplasmModel {
+    private final GermplasmVO germplasm;
+    private final DataSource source;
+    private final List<BrapiGermplasmAttributeValue> attributes;
+    private final PedigreeVO pedigree;
+    private final List<XRefDocumentVO> crossReferences;
+
+    public GermplasmModel(GermplasmVO germplasm,
+                          DataSource source,
+                          List<BrapiGermplasmAttributeValue> attributes,
+                          PedigreeVO pedigree,
+                          List<XRefDocumentVO> crossReferences) {
+        this.germplasm = germplasm;
+        this.source = source;
+        this.attributes = attributes;
+        this.pedigree = pedigree;
+        this.crossReferences = crossReferences;
+    }
+
+    public GermplasmVO getGermplasm() {
+        return germplasm;
+    }
+
+    public DataSource getSource() {
+        return source;
+    }
+
+    public List<BrapiGermplasmAttributeValue> getAttributes() {
+        return attributes;
+    }
+
+    public PedigreeVO getPedigree() {
+        return pedigree;
+    }
+
+    public List<XRefDocumentVO> getCrossReferences() {
+        return crossReferences;
+    }
+
+    public String getTaxon() {
+        if (Strings.isNotBlank(this.germplasm.getGenusSpeciesSubtaxa())) {
+            return this.germplasm.getGenusSpeciesSubtaxa();
+        } else if (Strings.isNotBlank(this.germplasm.getGenusSpecies())) {
+            return this.germplasm.getGenusSpecies();
+        } else if (Strings.isNotBlank(this.germplasm.getSubtaxa())) {
+            return this.germplasm.getGenus() + " " + this.germplasm.getSpecies() + " " + this.germplasm.getSubtaxa();
+        } else if (Strings.isNotBlank(this.germplasm.getSpecies())) {
+            return this.germplasm.getGenus() + " " + this.germplasm.getSpecies();
+        } else {
+            return this.germplasm.getGenus();
+        }
+    }
+
+    public String getTaxonAuthor() {
+        if (Strings.isNotBlank(this.germplasm.getGenusSpeciesSubtaxa())) {
+            return this.germplasm.getSubtaxaAuthority();
+        } else if (Strings.isNotBlank(this.germplasm.getGenusSpecies())) {
+            return this.germplasm.getSpeciesAuthority();
+        } else if (Strings.isNotBlank(this.germplasm.getSubtaxa())) {
+            return this.germplasm.getSubtaxaAuthority();
+        } else if (Strings.isNotBlank(this.germplasm.getSpecies())) {
+            return this.germplasm.getSpeciesAuthority();
+        } else {
+            return null;
+        }
+    }
+
+    public boolean isCollecting() {
+        return this.isCollectingSitePresent()
+            || this.isCollectorInstitutePresent()
+            || this.isCollectorIntituteFieldPresent();
+    }
+
+    private boolean isCollectingSitePresent() {
+        return this.germplasm.getCollectingSite() != null && Strings.isNotBlank(this.germplasm.getCollectingSite().getSiteName());
+    }
+
+    private boolean isCollectorInstitutePresent() {
+        return this.germplasm.getCollector() != null &&
+            this.germplasm.getCollector().getInstitute() != null &&
+            Strings.isNotBlank(this.germplasm.getCollector().getInstitute().getInstituteName());
+    }
+
+    private boolean isCollectorIntituteFieldPresent() {
+        GermplasmInstituteVO collector = this.germplasm.getCollector();
+        return (collector != null) &&
+            (Strings.isNotBlank(collector.getAccessionNumber())
+                || collector.getAccessionCreationDate() != null
+                || Strings.isNotBlank(collector.getMaterialType())
+                || Strings.isNotBlank(collector.getCollectors())
+                || collector.getRegistrationYear() != null
+                || collector.getDeregistrationYear() != null
+                || Strings.isNotBlank(collector.getDistributionStatus())
+            );
+    }
+
+    public boolean isBreeding() {
+        GermplasmInstituteVO breeder = this.germplasm.getBreeder();
+        return breeder != null &&
+            ((breeder.getInstitute() != null && Strings.isNotBlank(breeder.getInstitute().getInstituteName())) ||
+                breeder.getAccessionCreationDate() != null ||
+                Strings.isNotBlank(breeder.getAccessionNumber()) ||
+                breeder.getRegistrationYear() != null ||
+                breeder.getDeregistrationYear() != null);
+    }
+
+    public boolean isGenealogyPresent() {
+        return isPedigreePresent() || isProgenyPresent();
+    }
+
+    private boolean isProgenyPresent() {
+        return germplasm.getChildren() != null && !germplasm.getChildren().isEmpty();
+    }
+
+    private boolean isPedigreePresent() {
+        return this.pedigree != null &&
+            (Strings.isNotBlank(this.pedigree.getParent1Name())
+            || Strings.isNotBlank(this.pedigree.getParent2Name())
+            || Strings.isNotBlank(this.pedigree.getCrossingPlan())
+            || Strings.isNotBlank(this.pedigree.getCrossingYear())
+            || Strings.isNotBlank(this.pedigree.getFamilyCode()));
+    }
+
+    public List<MapLocation> getMapLocations() {
+        List<SiteVO> sites = new ArrayList<>();
+        if (germplasm.getCollectingSite() != null) {
+            sites.add(germplasm.getCollectingSite());
+        }
+        if (germplasm.getOriginSite() != null) {
+            sites.add(germplasm.getOriginSite());
+        }
+        if (germplasm.getEvaluationSites() != null) {
+            sites.addAll(germplasm.getEvaluationSites());
+        }
+
+        return MapLocation.sitesToDisplayableMapLocations(sites);
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java
new file mode 100644
index 0000000000000000000000000000000000000000..3b096853cfff4a467d7a277f9fb4b41e65b2ad01
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java
@@ -0,0 +1,82 @@
+package fr.inra.urgi.faidare.web.site;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import fr.inra.urgi.faidare.domain.data.LocationVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.SiteVO;
+import fr.inra.urgi.faidare.utils.Sites;
+
+/**
+ * An object that can be serialized to JSON to serve as a map marker.
+ * @author JB Nizet
+ */
+public final class MapLocation {
+    private final String locationDbId;
+    private final String locationType;
+    private final String locationName;
+    private final double latitude;
+    private final double longitude;
+
+    public MapLocation(String locationDbId,
+                       String locationType,
+                       String locationName,
+                       double latitude,
+                       double longitude) {
+        this.locationDbId = locationDbId;
+        this.locationType = locationType;
+        this.locationName = locationName;
+        this.latitude = latitude;
+        this.longitude = longitude;
+    }
+
+    public MapLocation(LocationVO site) {
+        this(site.getLocationDbId(),
+             site.getLocationType(),
+             site.getLocationName(),
+             site.getLatitude(),
+             site.getLongitude());
+    }
+
+    public MapLocation(SiteVO site) {
+        this(Sites.siteIdToLocationId(site.getSiteId()),
+             site.getSiteType(),
+             site.getSiteName(),
+             site.getLatitude(),
+             site.getLongitude());
+    }
+
+    public static List<MapLocation> locationsToDisplayableMapLocations(List<LocationVO> locations) {
+        return locations.stream()
+                        .filter(location -> location.getLatitude() != null && location.getLongitude() != null)
+                        .map(MapLocation::new)
+                        .collect(Collectors.toList());
+    }
+
+    public static List<MapLocation> sitesToDisplayableMapLocations(List<SiteVO> sites) {
+        return sites.stream()
+                    .filter(site -> site.getLatitude() != null && site.getLongitude() != null)
+                    .map(MapLocation::new)
+                    .collect(Collectors.toList());
+    }
+
+    public String getLocationDbId() {
+        return locationDbId;
+    }
+
+    public String getLocationType() {
+        return locationType;
+    }
+
+    public String getLocationName() {
+        return locationName;
+    }
+
+    public double getLatitude() {
+        return latitude;
+    }
+
+    public double getLongitude() {
+        return longitude;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java
new file mode 100644
index 0000000000000000000000000000000000000000..151da527d4393d1f388de8d6098b52d4219d98b2
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java
@@ -0,0 +1,94 @@
+package fr.inra.urgi.faidare.web.site;
+
+import java.util.Arrays;
+import java.util.List;
+
+import fr.inra.urgi.faidare.api.NotFoundException;
+import fr.inra.urgi.faidare.config.FaidareProperties;
+import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiAdditionalInfo;
+import fr.inra.urgi.faidare.domain.data.LocationVO;
+import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+import fr.inra.urgi.faidare.repository.es.LocationRepository;
+import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ * Controller used to display a site card based on its ID.
+ * @author JB Nizet
+ */
+@Controller("webSiteController")
+@RequestMapping("/sites")
+public class SiteController {
+
+    private final LocationRepository locationRepository;
+    private final FaidareProperties faidareProperties;
+    private final XRefDocumentRepository xRefDocumentRepository;
+
+    public SiteController(LocationRepository locationRepository,
+                          FaidareProperties faidareProperties,
+                          XRefDocumentRepository xRefDocumentRepository) {
+        this.locationRepository = locationRepository;
+        this.faidareProperties = faidareProperties;
+        this.xRefDocumentRepository = xRefDocumentRepository;
+    }
+
+    @GetMapping("/{siteId}")
+    public ModelAndView get(@PathVariable("siteId") String siteId) {
+        LocationVO site = locationRepository.getById(siteId);
+
+        // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find(
+        //     XRefDocumentSearchCriteria.forXRefId(site.getLocationDbId()));
+        List<XRefDocumentVO> crossReferences = Arrays.asList(
+            createXref("foobar"),
+            createXref("bazbing")
+        );
+
+        // LocationVO site = createSite();
+
+        if (site == null) {
+            throw new NotFoundException("Site with ID " + siteId + " not found");
+        }
+
+
+        return new ModelAndView("site",
+                                "model",
+                                new SiteModel(
+                                    site,
+                                    faidareProperties.getByUri(site.getSourceUri()),
+                                    crossReferences
+                                )
+        );
+    }
+
+    private LocationVO createSite() {
+        LocationVO site = new LocationVO();
+        site.setLocationName("France");
+        site.setSourceUri("https://urgi.versailles.inrae.fr/gnpis");
+        site.setUri("Test URI");
+        site.setUrl("https://google.com");
+        site.setLatitude(45.65);
+        site.setLongitude(1.34);
+        BrapiAdditionalInfo additionalInfo = new BrapiAdditionalInfo();
+        additionalInfo.addProperty("Slope", 4.32);
+        additionalInfo.addProperty("Distance to city", "3 km");
+        additionalInfo.addProperty("foo", "bar");
+        additionalInfo.addProperty("baz", "zing");
+        additionalInfo.addProperty("blob", null);
+        site.setAdditionalInfo(additionalInfo);
+        return site;
+    }
+
+    private XRefDocumentVO createXref(String name) {
+        XRefDocumentVO xref = new XRefDocumentVO();
+        xref.setName(name);
+        xref.setDescription("A very large description for the xref " + name + " which has way more than 120 characters bla bla bla bla bla bla bla bla bla bla bla bla");
+        xref.setDatabaseName("db_" + name);
+        xref.setUrl("https://google.com");
+        xref.setEntryType("type " + name);
+        return xref;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..0d7e1c0dcef28b8182a3a5ac8f4aa160f067255c
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
@@ -0,0 +1,118 @@
+package fr.inra.urgi.faidare.web.site;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import fr.inra.urgi.faidare.domain.data.LocationVO;
+import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource;
+import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+
+/**
+ * The model used py the site page
+ * @author JB Nizet
+ */
+public final class SiteModel {
+    private static final Set<String> IGNORED_PROPERTIES =
+        new HashSet<>(Arrays.asList("Site status",
+                                    "Coordinates precision",
+                                    "Slope",
+                                    "Exposure",
+                                    "Geographical location",
+                                    "Distance to city",
+                                    "Direction from city",
+                                    "Environment type",
+                                    "Topography",
+                                    "Comment"));
+
+    private final LocationVO site;
+    private final DataSource source;
+    private final Map<String, Object> additionalInfo;
+    private final List<XRefDocumentVO> crossReferences;
+    private final List<Map.Entry<String, Object>> additionalInfoProperties;
+
+    public SiteModel(LocationVO site,
+                     DataSource source,
+                     List<XRefDocumentVO> crossReferences) {
+        this.site = site;
+        this.source = source;
+        this.additionalInfo = site.getAdditionalInfo() == null ? Collections.emptyMap() : site.getAdditionalInfo().getProperties();
+        this.crossReferences = crossReferences;
+        this.additionalInfoProperties =
+            this.additionalInfo
+                .entrySet()
+                .stream()
+                .filter(entry -> !IGNORED_PROPERTIES.contains(entry.getKey()))
+                .filter(entry -> entry.getValue() != null && !entry.getValue().toString().isEmpty())
+                .sorted(Map.Entry.comparingByKey())
+                .collect(Collectors.toList());
+    }
+
+    public LocationVO getSite() {
+        return site;
+    }
+
+    public DataSource getSource() {
+        return source;
+    }
+
+    public Map<String, Object> getAdditionalInfo() {
+        return this.additionalInfo;
+    }
+
+    public Object getSiteStatus() {
+        return this.additionalInfo.get("Site status");
+    }
+
+    public Object getCoordinatesPrecision() {
+        return this.additionalInfo.get("Coordinates precision");
+    }
+
+    public Object getGeographicalLocation() {
+        return this.additionalInfo.get("Geographical location");
+    }
+
+    public Object getSlope() {
+        return this.additionalInfo.get("Slope");
+    }
+
+    public Object getExposure() {
+        return this.additionalInfo.get("Exposure");
+    }
+
+    public Object getTopography() {
+        return this.additionalInfo.get("Topography");
+    }
+
+    public Object getEnvironmentType() {
+        return this.additionalInfo.get("Environment type");
+    }
+
+    public Object getDistanceToCity() {
+        return this.additionalInfo.get("Distance to city");
+    }
+
+    public Object getDirectionFromCity() {
+        return this.additionalInfo.get("Direction from city");
+    }
+
+    public Object getComment() {
+        return this.additionalInfo.get("Comment");
+    }
+
+    public List<Map.Entry<String, Object>> getAdditionalInfoProperties() {
+        return additionalInfoProperties;
+    }
+
+    public List<XRefDocumentVO> getCrossReferences() {
+        return crossReferences;
+    }
+
+    public List<MapLocation> getMapLocations() {
+        return MapLocation.locationsToDisplayableMapLocations(Collections.singletonList(this.site));
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
new file mode 100644
index 0000000000000000000000000000000000000000..833c45471061a5ef02d7f5fc57319fc9afc8a12b
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
@@ -0,0 +1,159 @@
+package fr.inra.urgi.faidare.web.study;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.google.common.collect.Lists;
+import fr.inra.urgi.faidare.api.NotFoundException;
+import fr.inra.urgi.faidare.config.FaidareProperties;
+import fr.inra.urgi.faidare.domain.criteria.GermplasmPOSTSearchCriteria;
+import fr.inra.urgi.faidare.domain.data.LocationVO;
+import fr.inra.urgi.faidare.domain.data.TrialVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO;
+import fr.inra.urgi.faidare.domain.data.study.StudyDetailVO;
+import fr.inra.urgi.faidare.domain.data.variable.ObservationVariableVO;
+import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+import fr.inra.urgi.faidare.repository.es.GermplasmRepository;
+import fr.inra.urgi.faidare.repository.es.LocationRepository;
+import fr.inra.urgi.faidare.repository.es.StudyRepository;
+import fr.inra.urgi.faidare.repository.es.TrialRepository;
+import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository;
+import fr.inra.urgi.faidare.repository.file.CropOntologyRepository;
+import fr.inra.urgi.faidare.web.site.MapLocation;
+import org.apache.logging.log4j.util.Strings;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ * Controller used to display a study card based on its ID.
+ * @author JB Nizet
+ */
+@Controller("webStudyController")
+@RequestMapping("/studies")
+public class StudyController {
+
+    private final StudyRepository studyRepository;
+    private final FaidareProperties faidareProperties;
+    private final XRefDocumentRepository xRefDocumentRepository;
+    private final GermplasmRepository germplasmRepository;
+    private final CropOntologyRepository cropOntologyRepository;
+    private final TrialRepository trialRepository;
+    private final LocationRepository locationRepository;
+
+    public StudyController(StudyRepository studyRepository,
+                           FaidareProperties faidareProperties,
+                           XRefDocumentRepository xRefDocumentRepository,
+                           GermplasmRepository germplasmRepository,
+                           CropOntologyRepository cropOntologyRepository,
+                           TrialRepository trialRepository,
+                           LocationRepository locationRepository) {
+        this.studyRepository = studyRepository;
+        this.faidareProperties = faidareProperties;
+        this.xRefDocumentRepository = xRefDocumentRepository;
+        this.germplasmRepository = germplasmRepository;
+        this.cropOntologyRepository = cropOntologyRepository;
+        this.trialRepository = trialRepository;
+        this.locationRepository = locationRepository;
+    }
+
+    @GetMapping("/{studyId}")
+    public ModelAndView get(@PathVariable("studyId") String studyId) {
+        StudyDetailVO study = studyRepository.getById(studyId);
+
+        // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find(
+        //     XRefDocumentSearchCriteria.forXRefId(site.getLocationDbId()));
+        List<XRefDocumentVO> crossReferences = Arrays.asList(
+            createXref("foobar"),
+            createXref("bazbing")
+        );
+
+        // LocationVO site = createSite();
+
+        if (study == null) {
+            throw new NotFoundException("Study with ID " + studyId + " not found");
+        }
+
+        List<GermplasmVO> germplasms = getGermplasms(study);
+        List<ObservationVariableVO>variables = getVariables(study);
+        List<TrialVO> trials = getTrials(study);
+        LocationVO location = getLocation(study);
+
+        // TODO remove this
+        location.setLatitude(34.0);
+        location.setLongitude(14.0);
+
+        return new ModelAndView("study",
+                                "model",
+                                new StudyModel(
+                                    study,
+                                    faidareProperties.getByUri(study.getSourceUri()),
+                                    germplasms,
+                                    variables,
+                                    trials,
+                                    crossReferences,
+                                    location
+                                )
+        );
+    }
+
+    private LocationVO getLocation(StudyDetailVO study) {
+        if (Strings.isBlank(study.getLocationDbId())) {
+            return null;
+        }
+        return locationRepository.getById(study.getLocationDbId());
+    }
+
+    private List<GermplasmVO> getGermplasms(StudyDetailVO study) {
+        if (study.getGermplasmDbIds() == null || study.getGermplasmDbIds().isEmpty()) {
+            return Collections.emptyList();
+        } else {
+            GermplasmPOSTSearchCriteria germplasmCriteria = new GermplasmPOSTSearchCriteria();
+            germplasmCriteria.setGermplasmDbIds(Lists.newArrayList(study.getGermplasmDbIds()));
+            return germplasmRepository.find(germplasmCriteria)
+                .stream()
+                .sorted(Comparator.comparing(GermplasmVO::getGermplasmName))
+                .collect(Collectors.toList());
+        }
+    }
+
+    private List<ObservationVariableVO> getVariables(StudyDetailVO study) {
+        Set<String> variableIds = studyRepository.getVariableIds(study.getStudyDbId());
+        return cropOntologyRepository.getVariableByIds(variableIds)
+            .stream()
+            .sorted(Comparator.comparing(ObservationVariableVO::getObservationVariableDbId))
+            .collect(Collectors.toList());
+    }
+
+    private List<TrialVO> getTrials(StudyDetailVO study) {
+        if (study.getTrialDbIds() == null || study.getTrialDbIds().isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        return study.getTrialDbIds()
+                    .stream()
+                    .sorted(Comparator.naturalOrder())
+                    .map(trialRepository::getById)
+                    .filter(Objects::nonNull)
+                    .collect(Collectors.toList());
+    }
+
+
+
+    private XRefDocumentVO createXref(String name) {
+        XRefDocumentVO xref = new XRefDocumentVO();
+        xref.setName(name);
+        xref.setDescription("A very large description for the xref " + name + " which has way more than 120 characters bla bla bla bla bla bla bla bla bla bla bla bla");
+        xref.setDatabaseName("db_" + name);
+        xref.setUrl("https://google.com");
+        xref.setEntryType("type " + name);
+        return xref;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..bc77dfc3d9b9fdb7da63dd7b47afb15ab1e3bff6
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java
@@ -0,0 +1,90 @@
+package fr.inra.urgi.faidare.web.study;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import fr.inra.urgi.faidare.domain.data.LocationVO;
+import fr.inra.urgi.faidare.domain.data.TrialVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO;
+import fr.inra.urgi.faidare.domain.data.study.StudyDetailVO;
+import fr.inra.urgi.faidare.domain.data.variable.ObservationVariableVO;
+import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource;
+import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+import fr.inra.urgi.faidare.web.site.MapLocation;
+
+/**
+ * The model used by the study page
+ * @author JB Nizet
+ */
+public final class StudyModel {
+    private final StudyDetailVO study;
+    private final DataSource source;
+    private final List<GermplasmVO> germplasms;
+    private final List<ObservationVariableVO> variables;
+    private final List<TrialVO> trials;
+    private final List<XRefDocumentVO> crossReferences;
+    private final LocationVO location;
+    private final List<Map.Entry<String, Object>> additionalInfoProperties;
+
+    public StudyModel(StudyDetailVO study,
+                      DataSource source,
+                      List<GermplasmVO> germplasms,
+                      List<ObservationVariableVO> variables,
+                      List<TrialVO> trials,
+                      List<XRefDocumentVO> crossReferences,
+                      LocationVO location) {
+        this.study = study;
+        this.source = source;
+        this.germplasms = germplasms;
+        this.variables = variables;
+        this.trials = trials;
+        this.crossReferences = crossReferences;
+        this.location = location;
+
+        Map<String, Object> additionalInfo =
+            study.getAdditionalInfo() == null ? Collections.emptyMap() : study.getAdditionalInfo().getProperties();
+        this.additionalInfoProperties =
+            additionalInfo.entrySet()
+                          .stream()
+                          .filter(entry -> entry.getValue() != null && !entry.getValue().toString().isEmpty())
+                          .sorted(Map.Entry.comparingByKey())
+                          .collect(Collectors.toList());
+    }
+
+    public StudyDetailVO getStudy() {
+        return study;
+    }
+
+    public DataSource getSource() {
+        return source;
+    }
+
+    public List<XRefDocumentVO> getCrossReferences() {
+        return crossReferences;
+    }
+
+    public List<GermplasmVO> getGermplasms() {
+        return germplasms;
+    }
+
+    public List<ObservationVariableVO> getVariables() {
+        return variables;
+    }
+
+    public List<TrialVO> getTrials() {
+        return trials;
+    }
+
+    public List<Map.Entry<String, Object>> getAdditionalInfoProperties() {
+        return additionalInfoProperties;
+    }
+
+    public List<MapLocation> getMapLocations() {
+        if (this.location == null) {
+            return Collections.emptyList();
+        }
+        return MapLocation.locationsToDisplayableMapLocations(Collections.singletonList(this.location));
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/Coordinates.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/Coordinates.java
new file mode 100644
index 0000000000000000000000000000000000000000..554929b11d88e103093f808be0ba969118c47772
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/Coordinates.java
@@ -0,0 +1,71 @@
+package fr.inra.urgi.faidare.web.thymeleaf;
+
+import java.text.DecimalFormat;
+import java.util.Locale;
+
+/**
+ * The actual object offering Coordinates helper methods to thymeleaf
+ * @author JB Nizet
+ */
+public class Coordinates {
+    private final Locale locale;
+
+    public Coordinates(Locale locale) {
+        this.locale = locale;
+    }
+
+    public String format(Double value) {
+        if (value == null) {
+            return "";
+        }
+        return DecimalFormat.getInstance(locale).format(value);
+    }
+
+    public String formatLatitude(Double value) {
+        if (value == null) {
+            return "";
+        }
+        return this.format(value) + " — " + this.toLatitudeDegrees(value);
+    }
+
+    public String formatLongitude(Double value) {
+        if (value == null) {
+            return "";
+        }
+        return this.format(value) + " — " + this.toLongitudeDegrees(value);
+    }
+
+    public String toLatitudeDegrees(Double latitude) {
+        if (latitude == null) {
+            return "";
+        }
+
+        return toDegrees(latitude) + " " + ((latitude < 0) ? "S" : "N");
+    }
+
+    public String toLongitudeDegrees(Double longitude) {
+        if (longitude == null) {
+            return "";
+        }
+
+        return toDegrees(longitude) + " " + ((longitude < 0) ? "W" : "E");
+    }
+
+    private String toDegrees(double value) {
+        double absoluteDegrees = Math.abs(value);
+        int fullDegrees = (int) absoluteDegrees;
+        double remainingMinutes = (absoluteDegrees - fullDegrees) * 60;
+        int minutes = (int) remainingMinutes;
+        double remainingSeconds = (remainingMinutes - minutes) * 60;
+        int seconds = (int) remainingSeconds;
+        if (seconds == 60) {
+            minutes += 1;
+            seconds = 0;
+        }
+        if (minutes == 60) {
+            fullDegrees += 1;
+            minutes = 0;
+        }
+        return fullDegrees + "°" + minutes + "'" + seconds;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesDialect.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesDialect.java
new file mode 100644
index 0000000000000000000000000000000000000000..d443e589ca1c8e82c00960faeb9f15733db0e102
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesDialect.java
@@ -0,0 +1,28 @@
+package fr.inra.urgi.faidare.web.thymeleaf;
+
+import org.springframework.stereotype.Component;
+import org.thymeleaf.dialect.AbstractDialect;
+import org.thymeleaf.dialect.IDialect;
+import org.thymeleaf.dialect.IExpressionObjectDialect;
+import org.thymeleaf.expression.IExpressionObjectFactory;
+import org.thymeleaf.extras.java8time.dialect.Java8TimeExpressionFactory;
+
+/**
+ * A thymeleaf dialect allowing to transform coordinates (latitude and longitude)
+ * to degrees.
+ * @author JB Nizet
+ */
+@Component
+public class CoordinatesDialect extends AbstractDialect implements IExpressionObjectDialect {
+
+    private final IExpressionObjectFactory COORDINATES_EXPRESSION_OBJECTS_FACTORY = new CoordinatesExpressionFactory();
+
+    protected CoordinatesDialect() {
+        super("coordinates");
+    }
+
+    @Override
+    public IExpressionObjectFactory getExpressionObjectFactory() {
+        return COORDINATES_EXPRESSION_OBJECTS_FACTORY;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesExpressionFactory.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesExpressionFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..e3c25cc8577aae0cac73a894915d6c33edd6d8ca
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesExpressionFactory.java
@@ -0,0 +1,36 @@
+package fr.inra.urgi.faidare.web.thymeleaf;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.thymeleaf.context.IExpressionContext;
+import org.thymeleaf.expression.IExpressionObjectFactory;
+import org.thymeleaf.extras.java8time.expression.Temporals;
+
+/**
+ * The object factory for the {@link CoordinatesDialect}
+ * @author JB Nizet
+ */
+public class CoordinatesExpressionFactory implements IExpressionObjectFactory {
+    private static final String COORDINATES_EVALUATION_VARIABLE_NAME = "coordinates";
+
+    private static final Set<String> ALL_EXPRESSION_OBJECT_NAMES =
+        Collections.singleton(COORDINATES_EVALUATION_VARIABLE_NAME);
+
+    @Override
+    public Set<String> getAllExpressionObjectNames() {
+        return ALL_EXPRESSION_OBJECT_NAMES;
+    }
+
+    @Override
+    public Object buildObject(IExpressionContext context, String expressionObjectName) {
+        return new Coordinates(context.getLocale());
+    }
+
+    @Override
+    public boolean isCacheable(String expressionObjectName) {
+        return true;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareDialect.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareDialect.java
new file mode 100644
index 0000000000000000000000000000000000000000..2a02a842a28ba411f8f6d16d6c5254e41a53524b
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareDialect.java
@@ -0,0 +1,25 @@
+package fr.inra.urgi.faidare.web.thymeleaf;
+
+import org.springframework.stereotype.Component;
+import org.thymeleaf.dialect.AbstractDialect;
+import org.thymeleaf.dialect.IExpressionObjectDialect;
+import org.thymeleaf.expression.IExpressionObjectFactory;
+
+/**
+ * A thymeleaf dialect allowing to perform various tasks in the template related to Faidare
+ * @author JB Nizet
+ */
+@Component
+public class FaidareDialect extends AbstractDialect implements IExpressionObjectDialect {
+
+    private final IExpressionObjectFactory FAIDARE_EXPRESSION_OBJECTS_FACTORY = new FaidareExpressionFactory();
+
+    protected FaidareDialect() {
+        super("faidare");
+    }
+
+    @Override
+    public IExpressionObjectFactory getExpressionObjectFactory() {
+        return FAIDARE_EXPRESSION_OBJECTS_FACTORY;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressionFactory.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressionFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..e873375c1a7681d2837c0bf6591174d1aa557e3d
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressionFactory.java
@@ -0,0 +1,33 @@
+package fr.inra.urgi.faidare.web.thymeleaf;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.thymeleaf.context.IExpressionContext;
+import org.thymeleaf.expression.IExpressionObjectFactory;
+
+/**
+ * The object factory for the {@link FaidareDialect}
+ * @author JB Nizet
+ */
+public class FaidareExpressionFactory implements IExpressionObjectFactory {
+    private static final String FAIDARE_EVALUATION_VARIABLE_NAME = "faidare";
+
+    private static final Set<String> ALL_EXPRESSION_OBJECT_NAMES =
+        Collections.singleton(FAIDARE_EVALUATION_VARIABLE_NAME);
+
+    @Override
+    public Set<String> getAllExpressionObjectNames() {
+        return ALL_EXPRESSION_OBJECT_NAMES;
+    }
+
+    @Override
+    public Object buildObject(IExpressionContext context, String expressionObjectName) {
+        return new FaidareExpressions(context.getLocale());
+    }
+
+    @Override
+    public boolean isCacheable(String expressionObjectName) {
+        return true;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java
new file mode 100644
index 0000000000000000000000000000000000000000..9ba16d342d94e9097e720c5b3ad2e370d564fe6d
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java
@@ -0,0 +1,78 @@
+package fr.inra.urgi.faidare.web.thymeleaf;
+
+import java.nio.charset.StandardCharsets;
+import java.text.DecimalFormat;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Function;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import fr.inra.urgi.faidare.domain.data.germplasm.CollPopVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.TaxonSourceVO;
+import fr.inra.urgi.faidare.utils.Sites;
+import org.apache.logging.log4j.util.Strings;
+
+/**
+ * The actual object offering Faidare helper methods to thymeleaf
+ * @author JB Nizet
+ */
+public class FaidareExpressions {
+
+    private static final Map<String, Function<String, String>> TAXON_ID_URL_FACTORIES_BY_SOURCE_NAME =
+        createTaxonIdUrlFactories();
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
+    private static Map<String, Function<String, String>> createTaxonIdUrlFactories() {
+        Map<String, Function<String, String>> result = new HashMap<>();
+        result.put("NCBI", s -> "https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=Info&id=" + s);
+        result.put("ThePlantList", s -> "http://www.theplantlist.org/tpl1.1/record/" + s);
+        result.put("TAXREF", s -> "https://inpn.mnhn.fr/espece/cd_nom/" + s);
+        result.put("CatalogueOfLife", s -> "http://www.catalogueoflife.org/col/details/species/id/" + s);
+        return Collections.unmodifiableMap(result);
+    }
+
+    private final Locale locale;
+
+    public FaidareExpressions(Locale locale) {
+        this.locale = locale;
+    }
+
+    public String toSiteParam(String siteId) {
+        return Sites.siteIdToLocationId(siteId);
+    }
+
+    public String collPopTitle(CollPopVO collPopVO) {
+        return collPopTitle(collPopVO, Function.identity());
+    }
+
+    public String collPopTitleWithoutUnderscores(CollPopVO collPopVO) {
+        return collPopTitle(collPopVO, s -> s.replace('_', ' '));
+    }
+
+    public String taxonIdUrl(TaxonSourceVO taxonSource) {
+        Function<String, String> urlFactory =
+            TAXON_ID_URL_FACTORIES_BY_SOURCE_NAME.get(taxonSource.getSourceName());
+        return urlFactory != null ? urlFactory.apply(taxonSource.getTaxonId()) : null;
+    }
+
+    public String toJson(Object value) {
+        try {
+            return objectMapper.writeValueAsString(value);
+        }
+        catch (JsonProcessingException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    private String collPopTitle(CollPopVO collPopVO, Function<String, String> nameTransformer) {
+        if (Strings.isBlank(collPopVO.getType())) {
+            return nameTransformer.apply(collPopVO.getName());
+        } else {
+            return nameTransformer.apply(collPopVO.getName()) + " (" + collPopVO.getType() + ")";
+        }
+    }
+}
diff --git a/backend/src/main/main.iml b/backend/src/main/main.iml
deleted file mode 100644
index 50f3d6bc79c58fa92df31bb59ab229d1e7c0c12e..0000000000000000000000000000000000000000
--- a/backend/src/main/main.iml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<module type="JAVA_MODULE" version="4">
-  <component name="NewModuleRootManager" inherit-compiler-output="true">
-    <exclude-output />
-    <content url="file://$MODULE_DIR$">
-      <sourceFolder url="file://$MODULE_DIR$/java" isTestSource="false" />
-    </content>
-    <orderEntry type="inheritedJdk" />
-    <orderEntry type="sourceFolder" forTests="false" />
-    <orderEntry type="library" name="Gradle: io.swagger:swagger-annotations:1.5.21" level="project" />
-    <orderEntry type="library" name="Gradle: org.springframework:spring-beans:5.1.4.RELEASE" level="project" />
-    <orderEntry type="library" name="Gradle: org.springframework:spring-web:5.1.4.RELEASE" level="project" />
-    <orderEntry type="library" name="Gradle: io.springfox:springfox-core:2.9.2" level="project" />
-    <orderEntry type="library" name="Gradle: io.springfox:springfox-spring-web:2.9.2" level="project" />
-    <orderEntry type="library" name="Gradle: javax.validation:validation-api:2.0.1.Final" level="project" />
-    <orderEntry type="library" name="Gradle: com.google.guava:guava:27.0.1-jre" level="project" />
-    <orderEntry type="library" name="Gradle: commons-collections:commons-collections:3.2.2" level="project" />
-    <orderEntry type="library" name="Gradle: org.apache.httpcomponents:httpcore:4.4.10" level="project" />
-    <orderEntry type="library" name="Gradle: org.elasticsearch.client:elasticsearch-rest-client:6.4.3" level="project" />
-    <orderEntry type="library" name="Gradle: org.apache.httpcomponents:httpcore-nio:4.4.10" level="project" />
-    <orderEntry type="library" name="Gradle: org.springframework.boot:spring-boot-autoconfigure:2.1.2.RELEASE" level="project" />
-    <orderEntry type="library" name="Gradle: org.springframework:spring-context:5.1.4.RELEASE" level="project" />
-    <orderEntry type="library" name="Gradle: com.fasterxml.jackson.core:jackson-databind:2.9.8" level="project" />
-    <orderEntry type="library" name="Gradle: org.springframework.boot:spring-boot:2.1.2.RELEASE" level="project" />
-    <orderEntry type="library" name="Gradle: org.springframework.security:spring-security-config:5.1.3.RELEASE" level="project" />
-    <orderEntry type="library" name="Gradle: io.springfox:springfox-spi:2.9.2" level="project" />
-    <orderEntry type="library" name="Gradle: io.springfox:springfox-swagger2:2.9.2" level="project" />
-    <orderEntry type="library" name="Gradle: org.elasticsearch:elasticsearch:6.5.4" level="project" />
-    <orderEntry type="library" name="Gradle: org.slf4j:slf4j-api:1.7.25" level="project" />
-    <orderEntry type="library" name="Gradle: org.elasticsearch.client:elasticsearch-rest-high-level-client:6.5.4" level="project" />
-    <orderEntry type="library" name="Gradle: org.elasticsearch:elasticsearch-core:6.5.4" level="project" />
-    <orderEntry type="library" name="Gradle: org.springframework:spring-core:5.1.4.RELEASE" level="project" />
-    <orderEntry type="library" name="Gradle: org.apache.tomcat.embed:tomcat-embed-core:9.0.14" level="project" />
-    <orderEntry type="library" name="Gradle: com.opencsv:opencsv:4.4" level="project" />
-    <orderEntry type="library" name="Gradle: org.apache.commons:commons-lang3:3.8.1" level="project" />
-    <orderEntry type="library" name="Gradle: org.apache.lucene:lucene-join:7.5.0" level="project" />
-    <orderEntry type="library" name="Gradle: com.fasterxml.jackson.core:jackson-annotations:2.9.0" level="project" />
-  </component>
-</module>
\ No newline at end of file
diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml
index 26c55fc3839132b3f6aca48b40f00c061a6b180f..2b684f600c3e548456c9e68ef473116839440929 100644
--- a/backend/src/main/resources/application.yml
+++ b/backend/src/main/resources/application.yml
@@ -90,3 +90,11 @@ server:
   servlet:
     context-path: /faidare-dev
 
+---
+spring:
+  profiles:
+    dev
+  resources:
+    static-locations:
+      - classpath:/static/
+      - file:./web/build/dist/
diff --git a/backend/src/main/resources/static/assets/images/favicon.ico b/backend/src/main/resources/static/assets/images/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..7fc066f132eb58eeaf30b6259da59f5ed92aca8e
Binary files /dev/null and b/backend/src/main/resources/static/assets/images/favicon.ico differ
diff --git a/backend/src/main/resources/static/assets/images/logo.png b/backend/src/main/resources/static/assets/images/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..86b3ab108693a19cca5a05c95faebd8573e23389
Binary files /dev/null and b/backend/src/main/resources/static/assets/images/logo.png differ
diff --git a/backend/src/main/resources/static/assets/images/marker-icon-blue.png b/backend/src/main/resources/static/assets/images/marker-icon-blue.png
new file mode 100644
index 0000000000000000000000000000000000000000..e2e9f757f515ded172e6f72c3ce55bbe15579649
Binary files /dev/null and b/backend/src/main/resources/static/assets/images/marker-icon-blue.png differ
diff --git a/backend/src/main/resources/static/assets/images/marker-icon-green.png b/backend/src/main/resources/static/assets/images/marker-icon-green.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b4fb278f611f802d0c4e9cf88edad80e1e1c975
Binary files /dev/null and b/backend/src/main/resources/static/assets/images/marker-icon-green.png differ
diff --git a/backend/src/main/resources/static/assets/images/marker-icon-purple.png b/backend/src/main/resources/static/assets/images/marker-icon-purple.png
new file mode 100644
index 0000000000000000000000000000000000000000..63e423d250842ad5c9666507a4dd843a8f6a2b93
Binary files /dev/null and b/backend/src/main/resources/static/assets/images/marker-icon-purple.png differ
diff --git a/backend/src/main/resources/static/assets/images/marker-icon-red.png b/backend/src/main/resources/static/assets/images/marker-icon-red.png
new file mode 100644
index 0000000000000000000000000000000000000000..e3c0026ef246271f89aad81b6cf47b2ff63596d9
Binary files /dev/null and b/backend/src/main/resources/static/assets/images/marker-icon-red.png differ
diff --git a/backend/src/main/resources/templates/fragments/institute.html b/backend/src/main/resources/templates/fragments/institute.html
new file mode 100644
index 0000000000000000000000000000000000000000..b2e8da3ad2ef0e0d12bccaabfed479d893a2631c
--- /dev/null
+++ b/backend/src/main/resources/templates/fragments/institute.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+
+<html xmlns:th="http://www.thymeleaf.org">
+
+<body>
+
+<!--
+Reusable fragment displaying the content of an institute popover.
+Its unique argument (institute) is an InstituteVO
+-->
+
+<th:block th:fragment="institute(institute)">
+  <div class="text-center py-2" th:if="${institute.logo}">
+    <img class="img-fluid" th:src="${institute.logo}" />
+  </div>
+  <div th:replace="fragments/row::text-row(label='Code', text=${institute.instituteCode})"></div>
+  <div th:replace="fragments/row::text-row(label='Acronym', text=${institute.acronym})"></div>
+  <div th:replace="fragments/row::text-row(label='Organization', text=${institute.organisation})"></div>
+  <div th:replace="fragments/row::text-row(label='Type', text=${institute.instituteType})"></div>
+  <div th:replace="fragments/row::text-row(label='Address', text=${institute.address})"></div>
+
+  <th:block th:if="${institute.webSite}">
+    <div th:replace="fragments/row::row(label='Website', content=~{::.institute-website})">
+      <a class="institute-website"
+         target="_blank"
+         th:href="${institute.webSite}"
+         th:text="${#strings.abbreviate(institute.webSite, 25)}"></a>
+    </div>
+  </th:block>
+</th:block>
+
+</body>
+
+</html>
diff --git a/backend/src/main/resources/templates/fragments/link.html b/backend/src/main/resources/templates/fragments/link.html
new file mode 100644
index 0000000000000000000000000000000000000000..d7c43bbdb422c31c5bce647d302c803270672669
--- /dev/null
+++ b/backend/src/main/resources/templates/fragments/link.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+
+<html xmlns:th="http://www.thymeleaf.org">
+
+<body>
+<!--
+Reusable fragment displaying a link with a label if the provided url is not
+empty, or a span with the label if the provided url is empty.
+Both arguments are strings.
+-->
+<th:block th:fragment="link(label, url)">
+  <a th:unless="${#strings.isEmpty(url)}"
+     th:href="${url}"
+     th:text="${label}"></a>
+  <span th:if="${#strings.isEmpty(url)}" th:text="${label}"></span>
+</th:block>
+
+</body>
+
+</html>
diff --git a/backend/src/main/resources/templates/fragments/map.html b/backend/src/main/resources/templates/fragments/map.html
new file mode 100644
index 0000000000000000000000000000000000000000..35ebed333c5c5eaf55160f400e5ebde01313cf24
--- /dev/null
+++ b/backend/src/main/resources/templates/fragments/map.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+
+<html xmlns:th="http://www.thymeleaf.org">
+
+<body>
+<!--
+Reusable fragment displaying a map and its legend.
+The map is initially hidden. The JavaScript displays it if there are locations
+to display
+-->
+<div th:fragment="map" id="map-container" class="d-none">
+  <div id="map" class="border rounded"></div>
+  <div class="map-legend mt-1 small">
+    <img th:src="@{/assets/images/marker-icon-red.png}" id="red"/>
+    <label for="red" class="me-2">Origin site</label>
+    <img th:src="@{/assets/images/marker-icon-blue.png}" id="blue"/>
+    <label for="blue" class="me-2">Collecting site</label>
+    <img th:src="@{/assets/images/marker-icon-green.png}" id="green"/>
+    <label for="green" class="me-2">Evaluation site</label>
+    <img th:src="@{/assets/images/marker-icon-purple.png}" id="purple"/>
+    <label for="purple">Multi-purpose site</label>
+  </div>
+</div>
diff --git a/backend/src/main/resources/templates/fragments/row.html b/backend/src/main/resources/templates/fragments/row.html
new file mode 100644
index 0000000000000000000000000000000000000000..8676b5906b77ae97d6e8c2ca826839450ce28842
--- /dev/null
+++ b/backend/src/main/resources/templates/fragments/row.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+
+<html xmlns:th="http://www.thymeleaf.org">
+
+<body>
+
+<!--
+Reusable fragment displaying a responsive row containing a label and a content.
+The label argument is a string.
+The content argument is a fragment which is displayed at the right of the label.
+
+Note that `th:if` is not evaluated when th:replace is used. So if this row must
+be displayed only if some condition is true, the fragment should be enclosed
+into a block with the condition:
+  <th:block th:if="${someCondition}">
+    <div th:replace="fragments/row::row(label='Some label', content=~{::#some-content-id})">
+      <span id="some-content-id">the content here</span>
+    </div>
+  </th:block>
+-->
+
+<div th:fragment="row(label, content)" class="row f-row">
+  <div class="col-md-4 label pb-1 pb-md-0" th:text="${label}"></div>
+  <div class="col">
+    <th:block th:replace="${content}" />
+  </div>
+</div>
+
+<!--
+Reusable fragment displaying a responsive row containing a label and a textual content.
+The label argument is a string.
+The text argument is a string which is displayed at the right of the label.
+The whole row is omitted if the textual content is empty, so the caller does not
+need to test that condition.
+
+Note that `th:if` is not evaluated when th:replace is used. So if this row must
+be displayed only if some other condition is true, the fragment should be enclosed
+into a block with the condition:
+  <th:block th:if="${someCondition}">
+    <div th:replace="fragments/row::text-row(label='Some label', text=${someTextExpression})"></div>
+  </th:block>
+-->
+<div th:fragment="text-row(label, text)" th:unless="${#strings.isEmpty(text)}" class="row f-row">
+  <div class="col-md-4 label pb-1 pb-md-0" th:text="${label}"></div>
+  <div class="col" th:text="${text}"></div>
+</div>
+
+</body>
+
+</html>
diff --git a/backend/src/main/resources/templates/fragments/source.html b/backend/src/main/resources/templates/fragments/source.html
new file mode 100644
index 0000000000000000000000000000000000000000..b09638810885559be62d0c5448adecd617dbe2fb
--- /dev/null
+++ b/backend/src/main/resources/templates/fragments/source.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+
+<html xmlns:th="http://www.thymeleaf.org">
+
+<body>
+
+<!--
+Reusable fragment displaying the source and the data links of an entity (site, study or germplasm).
+The source argument is a DataSource.
+The url argument is a string, which is the URL of the entity.
+The entityType argument is a string, which is used in the message
+"Link to this <entityType>".
+-->
+
+<th:block th:fragment="source(source, url, entityType)">
+  <th:block th:if="${source != null}">
+    <div th:replace="fragments/row::row(label='Source', content=~{::.source})">
+      <a class="source" target="_blank" th:href="${source.url}">
+        <img class="img-fluid" style="max-height: 60px;" th:src="${source.image}" th:alt="${source.name} + ' logo'" />
+      </a>
+    </div>
+  </th:block>
+
+  <th:block th:if="${url != null && source != null}">
+    <div th:replace="fragments/row::row(label='Data link', content=~{::.source-url})">
+      <a class="source-url" target="_blank" th:href="${url}">
+        Link to this <span th:text="${entityType}"></span> on <th:block th:text="${source.name}" />
+      </a>
+    </div>
+  </th:block>
+</th:block>
+
+</body>
+
+</html>
diff --git a/backend/src/main/resources/templates/fragments/xrefs.html b/backend/src/main/resources/templates/fragments/xrefs.html
new file mode 100644
index 0000000000000000000000000000000000000000..c60310b69c5d852b7a636d9ab7392d0a52d2b553
--- /dev/null
+++ b/backend/src/main/resources/templates/fragments/xrefs.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+
+<html xmlns:th="http://www.thymeleaf.org">
+
+<body>
+
+<!--
+Reusable fragment displaying a cross references section, with its title.
+The unique argument (crossReferences) is a List<XRefDocumentVO>
+-->
+
+<div class="f-card" th:fragment="xrefs(crossReferences)" th:if="${!#lists.isEmpty(crossReferences)}">
+  <h2>Cross references</h2>
+  <div class="f-card-body">
+    <div class="scroll-table-container scroll-table-container-big">
+      <table class="table table-sm table-striped table-sticky table-responsive-sm">
+        <thead>
+          <tr>
+            <th scope="col">Name</th>
+            <th scope="col">Source</th>
+            <th scope="col">Type</th>
+            <th scope="col">Description</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr th:each="crossRef : ${crossReferences}">
+            <td><a th:href="${crossRef.url}" target="_blank" th:text="${crossRef.name}"></a></td>
+            <td th:text="${crossRef.databaseName}"></td>
+            <td th:text="${crossRef.entryType}"></td>
+            <td style="min-width: 30rem;" th:text="${#strings.abbreviate(crossRef.description, 120)}"></td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+</div>
+
+</body>
+
+</html>
diff --git a/backend/src/main/resources/templates/germplasm.html b/backend/src/main/resources/templates/germplasm.html
new file mode 100644
index 0000000000000000000000000000000000000000..f5c6a4ce2fdfa2f12bd1e2826f01dfea0c0fa48c
--- /dev/null
+++ b/backend/src/main/resources/templates/germplasm.html
@@ -0,0 +1,452 @@
+<!DOCTYPE html>
+
+<html
+  xmlns:th="http://www.thymeleaf.org"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}"
+>
+<head>
+  <title>Germplasm: <th:block th:text="${model.germplasm.germplasmName}" /></title>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+</head>
+
+<body>
+<main>
+  <div class="d-flex">
+    <h1 class="flex-grow-1">Germplasm: <th:block th:text="${model.germplasm.germplasmName}" /></h1>
+    <div th:if="${model.germplasm.holdingGenbank != null && model.germplasm.holdingGenbank.logo != null}">
+      <img th:src="${model.germplasm.holdingGenbank.logo}" th:alt="${model.germplasm.holdingGenbank.instituteName}" />
+    </div>
+  </div>
+
+  <div th:replace="fragments/map::map"></div>
+
+  <div class="row align-items-center justify-content-center mt-4">
+    <div class="col-auto field" th:if="${model.germplasm.photo != null && model.germplasm.photo.thumbnailFile != null}">
+      <template id="photo-popover">
+        <div class="card">
+          <img th:src="${model.germplasm.photo.file}" class="card-img-top" alt="">
+          <div class="card-body">
+            <div th:replace="fragments/row::text-row(label='Accession name', text=${model.germplasm.germplasmName})"></div>
+            <div th:replace="fragments/row::text-row(label='Photo name', text=${model.germplasm.photo.photoName})"></div>
+            <div th:replace="fragments/row::text-row(label='Description', text=${model.germplasm.photo.description})"></div>
+            <div th:replace="fragments/row::text-row(label='Copyright', text=${model.germplasm.photo.copyright})"></div>
+          </div>
+        </div>
+      </template>
+
+      <a role="button"
+         class="d-flex flex-column align-items-center"
+         data-bs-toggle="popover"
+         tabindex="0"
+         th:data-bs-title="${model.germplasm.photo.photoName}"
+         data-bs-element="#photo-popover"
+         data-bs-container="body"
+         data-bs-trigger="focus">
+        <img th:src="${model.germplasm.photo.thumbnailFile}" class="img-fluid" />
+
+        <figcaption class="figure-caption">
+          © <span th:text="${model.germplasm.photo.copyright}"></span>
+        </figcaption>
+      </a>
+    </div>
+
+    <div class="col-12 col-lg">
+      <div class="f-card">
+        <h2>Identification</h2>
+
+        <div class="f-card-body">
+          <div th:replace="fragments/row::text-row(label='Germplasm name', text=${model.germplasm.germplasmName})"></div>
+          <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.accessionNumber})"></div>
+
+          <div th:replace="fragments/source::source(source=${model.source}, url=${model.germplasm.url}, entityType='germplasm')"></div>
+
+          <th:block th:unless="${#lists.isEmpty(model.germplasm.synonyms)}">
+            <div th:replace="fragments/row::row(label='Accession synonyms', content=~{::#accession-synonyms})">
+              <div id="accession-synonyms" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.synonyms, ', ')}"></div>
+            </div>
+          </th:block>
+
+          <th:block th:unless="${#strings.isEmpty(model.taxon)}">
+            <div th:replace="fragments/row::row(label='Taxon', content=~{::#taxon})">
+              <div id="taxon">
+                <template id="taxon-popover">
+                  <th:block th:unless="${#strings.isEmpty(model.germplasm.genus)}">
+                    <div th:replace="fragments/row::row(label='Genus', content=~{::#taxon-genus})">
+                      <em id="taxon-genus" th:text="${model.germplasm.genus}"></em>
+                    </div>
+                  </th:block>
+                  <th:block th:unless="${#strings.isEmpty(model.germplasm.species)}">
+                    <div th:replace="fragments/row::row(label='Species', content=~{::#taxon-species})">
+                      <span id="taxon-species">
+                        <em th:text="${model.germplasm.species}"></em>
+                        <span th:unless="${#strings.isEmpty(model.germplasm.speciesAuthority)}"
+                              th:text="${'(' + model.germplasm.speciesAuthority + ')'}"></span>
+                      </span>
+                    </div>
+                  </th:block>
+                  <th:block th:unless="${#strings.isEmpty(model.germplasm.subtaxa)}">
+                    <div th:replace="fragments/row::row(label='Subtaxa', content=~{::#taxon-subtaxa})">
+                      <span id="taxon-subtaxa">
+                        <em th:text="${model.germplasm.subtaxa}"></em>
+                        <span th:unless="${#strings.isEmpty(model.germplasm.subtaxaAuthority)}"
+                              th:text="${'(' + model.germplasm.subtaxaAuthority + ')'}"></span>
+                      </span>
+                    </div>
+                  </th:block>
+
+                  <div th:replace="fragments/row::text-row(label='Authority', text=${model.taxonAuthor})"></div>
+
+                  <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonIds)}">
+                    <div th:replace="fragments/row::row(label='Taxon IDs', content=~{::#taxon-ids})">
+                      <div id="taxon-ids">
+                        <div th:each="taxonId : ${model.germplasm.taxonIds}" class="row">
+                          <div class="col-6 text-nowrap" th:text="${taxonId.sourceName}"></div>
+                          <div class="col-6">
+                            <span class="taxon-id"
+                                  th:replace="fragments/link::link(label=${taxonId.taxonId}, url=${#faidare.taxonIdUrl(taxonId)})"></span>
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                  </th:block>
+
+                  <div th:replace="fragments/row::text-row(label='Comment', text=${model.germplasm.taxonComment})"></div>
+                  <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonCommonNames)}">
+                    <div th:replace="fragments/row::row(label='Taxon common names', content=~{::#taxon-common-names})">
+                      <div id="taxon-common-names" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.taxonCommonNames, ', ')}"></div>
+                    </div>
+                  </th:block>
+                  <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonSynonyms)}">
+                    <div th:replace="fragments/row::row(label='Taxon common names', content=~{::#taxon-synonyms})">
+                      <div id="taxon-synonyms" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.taxonSynonyms, ', ')}"></div>
+                    </div>
+                  </th:block>
+                </template>
+                <a role="button"
+                   tabindex="0"
+                   data-bs-toggle="popover"
+                   th:data-bs-title="${model.taxon}"
+                   data-bs-element="#taxon-popover"
+                   data-bs-container="body"
+                   data-bs-trigger="focus">
+                  <em th:text="${model.taxon}"></em>
+                  <th:block th:unless="${#strings.isEmpty(model.taxonAuthor)}">(<span th:text="${model.taxonAuthor}"></span>)</th:block>
+                </a>
+              </div>
+            </div>
+          </th:block>
+
+          <div th:replace="fragments/row::text-row(label='Biological status', text=${model.germplasm.biologicalStatusOfAccessionCode})"></div>
+          <div th:replace="fragments/row::text-row(label='Genetic nature', text=${model.germplasm.geneticNature})"></div>
+          <div th:replace="fragments/row::text-row(label='Seed source', text=${model.germplasm.seedSource})"></div>
+          <div th:replace="fragments/row::text-row(label='Pedigree', text=${model.germplasm.pedigree})"></div>
+          <div th:replace="fragments/row::text-row(label='Comments', text=${model.germplasm.comment})"></div>
+
+          <th:block th:if="${model.germplasm.originSite != null && !#strings.isEmpty(model.germplasm.originSite.siteName)}">
+            <div th:replace="fragments/row::row(label='Origin site', content=~{::#origin-site})">
+              <a id="origin-site" th:href="@{/sites/{siteId}(siteId=${#faidare.toSiteParam(model.germplasm.originSite.siteId)})}" th:text="${model.germplasm.originSite.siteName}"></a>
+            </div>
+          </th:block>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <div class="f-card" th:if="${model.germplasm.holdingInstitute}">
+    <h2>Depositary</h2>
+    <div class="f-card-body">
+      <template id="holding-institute-popover">
+        <div th:replace="fragments/institute::institute(institute=${model.germplasm.holdingInstitute})"></div>
+      </template>
+      <div th:replace="fragments/row::row(label='Institution', content=~{::#institution})">
+        <a id="institution"
+           role="button"
+           tabindex="0"
+           data-bs-toggle="popover"
+           th:data-bs-title="${model.germplasm.holdingInstitute.instituteName}"
+           data-bs-element="#holding-institute-popover"
+           data-bs-container="body"
+           data-bs-trigger="focus"
+           th:text="${model.germplasm.holdingInstitute.instituteName}"></a>
+      </div>
+
+      <th:block th:if="${model.germplasm.holdingGenbank != null && !#strings.isEmpty(model.germplasm.holdingGenbank.instituteName) && !#strings.isEmpty(model.germplasm.holdingGenbank.webSite)}">
+        <div th:replace="fragments/row::row(label='Stock center name', content=~{::#stock-center-name})">
+          <a id="stock-center-name"
+             target="_blank"
+             th:href="${model.germplasm.holdingGenbank.webSite}"
+             th:text="${model.germplasm.holdingGenbank.instituteName}"></a>
+        </div>
+      </th:block>
+
+      <div th:replace="fragments/row::text-row(label='Presence status', text=${model.germplasm.presenceStatus})"></div>
+    </div>
+  </div>
+
+  <div class="f-card" th:if="${model.collecting}">
+    <h2>Collector</h2>
+    <div class="f-card-body">
+      <th:block th:if="${model.germplasm.collectingSite != null && !#strings.isEmpty(model.germplasm.collectingSite.siteName)}">
+        <div th:replace="fragments/row::row(label='Collecting site', content=~{::#collecting-site})">
+          <a id="collecting-site"
+             th:href="@{/sites/{siteId}(siteId=${#faidare.toSiteParam(model.germplasm.collectingSite.siteId)})}"
+             th:text="${model.germplasm.collectingSite.siteName}"
+          ></a>
+        </div>
+      </th:block>
+
+      <div th:replace="fragments/row::text-row(label='Material type', text=${model.germplasm.collector.materialType})"></div>
+      <div th:replace="fragments/row::text-row(label='Collectors', text=${model.germplasm.collector.collectors})"></div>
+
+      <th:block th:if="${!#strings.isEmpty(model.germplasm.acquisitionDate) && model.germplasm.collector.accessionCreationDate == null}">
+        <div th:replace="fragments/row::text-row(label='Acquisition / Creation date', text=${model.germplasm.acquisitionDate})"></div>
+      </th:block>
+
+      <th:block th:if="${model.germplasm.collector.institute != null && !#strings.isEmpty(model.germplasm.collector.institute.instituteName)}">
+        <template id="collector-institute-popover">
+          <div th:replace="fragments/institute::institute(institute=${model.germplasm.collector.institute})"></div>
+        </template>
+        <div th:replace="fragments/row::row(label='Institution', content=~{::#collecting-institution})">
+          <a id="collecting-institution"
+             role="button"
+             tabindex="0"
+             data-bs-toggle="popover"
+             th:data-bs-title="${model.germplasm.collector.institute.instituteName}"
+             data-bs-element="#collector-institute-popover"
+             data-bs-container="body"
+             data-bs-trigger="focus"
+             th:text="${model.germplasm.collector.institute.instituteName}"></a>
+        </div>
+      </th:block>
+
+      <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.collector.accessionNumber})"></div>
+    </div>
+  </div>
+
+  <div class="f-card" th:if="${model.breeding}">
+    <h2>Breeder</h2>
+    <div class="f-card-body">
+      <th:block th:if="${model.germplasm.breeder.institute != null && !#strings.isEmpty(model.germplasm.breeder.institute.instituteName)}">
+        <template id="breeder-institute-popover">
+          <div th:replace="fragments/institute::institute(institute=${model.germplasm.breeder.institute})"></div>
+        </template>
+        <div th:replace="fragments/row::row(label='Institute', content=~{::#breeding-institution})">
+          <a id="breeding-institution"
+             role="button"
+             tabindex="0"
+             data-bs-toggle="popover"
+             th:data-bs-title="${model.germplasm.breeder.institute.instituteName}"
+             data-bs-element="#breeder-institute-popover"
+             data-bs-container="body"
+             data-bs-trigger="focus"
+             th:text="${model.germplasm.breeder.institute.instituteName}"></a>
+        </div>
+      </th:block>
+
+      <div th:replace="fragments/row::text-row(label='Accession creation year', text=${model.germplasm.breeder.accessionCreationDate})"></div>
+      <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.breeder.accessionNumber})"></div>
+      <div th:replace="fragments/row::text-row(label='Catalog registration year', text=${model.germplasm.breeder.registrationYear})"></div>
+      <div th:replace="fragments/row::text-row(label='Catalog deregistration year', text=${model.germplasm.breeder.deregistrationYear})"></div>
+    </div>
+  </div>
+
+  <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.donors)}">
+    <h2>Donors</h2>
+    <div class="f-card-body">
+      <div class="scroll-table-container">
+        <table class="table table-sm table-striped table-sticky table-responsive-sm">
+          <thead>
+            <tr>
+              <th scope="col">Institute name</th>
+              <th scope="col">Institute code</th>
+              <th scope="col">Donation date</th>
+              <th scope="col">Accession number</th>
+              <th scope="col">Accession PUI</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="row, donorIterStat : ${model.germplasm.donors}">
+              <td>
+                <template th:id="${'donor-institute-popover-' + donorIterStat.index}">
+                  <div th:replace="fragments/institute::institute(institute=${row.donorInstitute})"></div>
+                </template>
+                <a role="button"
+                   tabindex="0"
+                   data-bs-toggle="popover"
+                   th:data-bs-title="${row.donorInstitute.instituteName}"
+                   th:data-bs-element="${'#donor-institute-popover-' + donorIterStat.index}"
+                   data-bs-container="body"
+                   data-bs-trigger="focus"
+                   th:text="${row.donorInstitute.instituteName}"></a>
+              </td>
+              <td th:text="${row.donorInstituteCode}"></td>
+              <td th:text="${row.donationDate}"></td>
+              <td th:text="${row.donorAccessionNumber}"></td>
+              <td th:text="${row.donorGermplasmPUI}"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+
+  <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.distributors)}">
+    <h2>Distributors</h2>
+    <div class="f-card-body">
+      <div class="scroll-table-container">
+        <table class="table table-sm table-striped table-sticky table-responsive-sm">
+          <thead>
+            <tr>
+              <th scope="col">Institute</th>
+              <th scope="col">Accession number</th>
+              <th scope="col">Distribution status</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="row, distributorIterStat : ${model.germplasm.distributors}">
+              <td>
+                <template th:id="${'distributor-institute-popover-' + distributorIterStat.index}">
+                  <div th:replace="fragments/institute::institute(institute=${row.institute})"></div>
+                </template>
+                <a role="button"
+                   tabindex="0"
+                   th:data-bs-title="${row.institute.instituteName}"
+                   th:data-bs-element="${'#distributor-institute-popover-' + distributorIterStat.index}"
+                   data-bs-container="body"
+                   data-bs-trigger="focus"
+                   th:text="${row.institute.instituteName}"></a>
+              </td>
+              <td th:text="${row.accessionNumber}"></td>
+              <td th:text="${row.distributionStatus}"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+
+  <div class="f-card" th:unless="${#lists.isEmpty(model.attributes)}">
+    <h2>Evaluation Data</h2>
+    <div class="f-card-body">
+      <th:block th:each="descriptor : ${model.attributes}">
+        <div th:replace="fragments/row::text-row(label=${descriptor.attributeName}, text=${descriptor.value})"></div>
+      </th:block>
+    </div>
+  </div>
+
+  <div class="f-card" th:if="${model.genealogyPresent}">
+    <h2>Genealogy</h2>
+    <div class="f-card-body">
+      <th:block th:if="${model.pedigree != null}">
+        <div th:replace="fragments/row::text-row(label='Crossing plan', text=${model.pedigree.crossingPlan})"></div>
+        <div th:replace="fragments/row::text-row(label='Crossing year', text=${model.pedigree.crossingYear})"></div>
+        <div th:replace="fragments/row::text-row(label='Family code', text=${model.pedigree.familyCode})"></div>
+        <th:block th:unless="${#strings.isEmpty(model.pedigree.parent1Name)}">
+          <div th:replace="fragments/row::row(label='Parent accessions', content=~{::#parent-accessions})">
+            <div id="parent-accessions">
+              <th:block th:if="${model.pedigree.parent1DbId}">
+                <div th:replace="fragments/row::row(label=${model.pedigree.parent1Type}, content=~{::#parent1-link})">
+                  <a id="parent1-link" th:href="@{/germplasms/{germplasmId}(germplasmId=${model.pedigree.parent1DbId})}" th:text="${model.pedigree.parent1Name}"></a>
+                </div>
+              </th:block>
+
+              <th:block th:if="${model.pedigree.parent2DbId}">
+                <div th:replace="fragments/row::row(label=${model.pedigree.parent2Type}, content=~{::#parent2-link})">
+                  <a id="parent2-link" th:href="@{/germplasms/{germplasmId}(germplasmId=${model.pedigree.parent2DbId})}" th:text="${model.pedigree.parent2Name}"></a>
+                </div>
+              </th:block>
+            </div>
+          </div>
+        </th:block>
+
+        <th:block th:unless="${#lists.isEmpty(model.pedigree.siblings)}">
+          <div th:replace="fragments/row::row(label='Sibling accessions', content=~{::#sibling-accessions})">
+            <div id="sibling-accessions" class="content-overflow">
+              <a th:each="sibling : ${model.pedigree.siblings}"
+                 th:href="@{/germplasms/{germplasmId}(germplasmId=${sibling.germplasmDbId})}"
+                 th:text="${sibling.defaultDisplayName}"></a>
+            </div>
+          </div>
+        </th:block>
+      </th:block>
+
+      <th:block th:unless="${#lists.isEmpty(model.germplasm.children)}">
+        <div th:replace="fragments/row::row(label='Descendants', content=~{::#descendants})">
+          <div id="descendants" class="content-overflow content-overflow-big">
+            <th:block th:each="child : ${model.germplasm.children}">
+              <div th:replace="fragments/row::row(label=${#strings.isEmpty(child.secondParentName) ? ('children of ' + child.firstParentName) : ('children of ' + child.firstParentName + ' x ' + child.secondParentName) }, content=~{::.descendant-child})">
+                <div class="descendant-child">
+                  <th:block th:each="sibling, siblingIterStat : ${child.sibblings}">
+                    <a th:href="@{/germplasms(pui=${sibling.pui})}"
+                       th:text="${sibling.name}"></a><th:block th:unless="${siblingIterStat.last}">, </th:block>
+                  </th:block>
+                </div>
+              </div>
+            </th:block>
+          </div>
+        </div>
+      </th:block>
+    </div>
+  </div>
+
+  <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.population)}">
+    <h2>Population</h2>
+    <div class="f-card-body">
+      <th:block th:each="population : ${model.germplasm.population}">
+
+        <th:block th:if="${population.germplasmRef != null}">
+          <th:block th:unless="${#strings.isEmpty(population.germplasmRef.pui)}">
+            <div th:replace="fragments/row::row(label=${#faidare.collPopTitle(population)}, content=~{::.population-1})">
+              <div class="population-1">
+                <a th:if="${population.germplasmRef.pui != model.germplasm.germplasmPUI}"
+                   th:href="@{/germplasms(pui=${population.germplasmRef.pui})}"
+                   th:text="${population.germplasmRef.name}"></a>
+                <span th:if="${population.germplasmRef.pui == model.germplasm.germplasmPUI}"
+                      th:text="${population.germplasmRef.name}"></span>
+                is composed by <span th:text="${population.germplasmCount}"></span> accession(s)
+                <!-- TODO there was a link pointing at a search here -->
+              </div>
+            </div>
+          </th:block>
+        </th:block>
+
+        <th:block th:if="${population.germplasmRef == null}">
+          <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitle(population)}, text=${population.germplasmCount + ' accession(s)'})"></div>
+          <!-- TODO there was a link pointing at a search here -->
+        </th:block>
+      </th:block>
+    </div>
+  </div>
+
+  <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.collection)}">
+    <h2>Collection</h2>
+    <div class="f-card-body">
+      <th:block th:each="collection : ${model.germplasm.collection}">
+        <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitle(collection)}, text=${collection.germplasmCount + ' accession(s)'})"></div>
+        <!-- TODO there was a link pointing at a search here -->
+      </th:block>
+    </div>
+  </div>
+
+  <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.panel)}">
+    <h2>Panel</h2>
+    <div class="f-card-body">
+      <th:block th:each="panel : ${model.germplasm.panel}">
+        <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitleWithoutUnderscores(panel)}, text=${panel.germplasmCount + ' accession(s)'})"></div>
+        <!-- TODO there was a link pointing at a search here -->
+      </th:block>
+    </div>
+  </div>
+
+  <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
+</main>
+
+<script th:inline="javascript">
+  faidare.initializePopovers();
+  faidare.initializeMap({
+    contextPath: [[${#request.getContextPath()}]],
+    locations: /*[[${model.mapLocations}]]*/ []
+  });
+</script>
+</body>
+</html>
diff --git a/backend/src/main/resources/templates/index.html b/backend/src/main/resources/templates/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..49f401b0284647d171f51ec5643987e27d836a05
--- /dev/null
+++ b/backend/src/main/resources/templates/index.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+
+<html
+  xmlns:th="http://www.thymeleaf.org"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
+>
+<head>
+  <title>Faidare</title>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+</head>
+
+<body>
+<main>
+  <h1>Welcome to Faidare</h1>
+</main>
+</body>
+</html>
diff --git a/backend/src/main/resources/templates/layout/main.html b/backend/src/main/resources/templates/layout/main.html
new file mode 100644
index 0000000000000000000000000000000000000000..22c36a5efb6dfe860a20b41f520ba826d160e96b
--- /dev/null
+++ b/backend/src/main/resources/templates/layout/main.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="fr" th:fragment="layout (title, content, script)" xmlns:th="http://www.thymeleaf.org">
+  <head>
+    <title th:replace="${title}">Layout Title</title>
+
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <meta content="width=device-width, initial-scale=1" name="viewport" />
+
+    <link th:href="@{/assets/style.css}" rel="stylesheet">
+
+    <link rel="shortcut icon" th:href="@{/static/assets/images/favicon.ico}" type="image/x-icon" />
+  </head>
+
+  <body>
+    <nav class="navbar navbar-expand-lg navbar-light bg-light">
+      <div class="container">
+        <span class="navbar-brand py-0">
+          <img th:src="@{/assets/images/logo.png}" style="height: 40px"/>
+        </span>
+      </div>
+    </nav>
+    <div class="container mt-3">
+      <div th:replace="${content}">
+        <p>Layout content</p>
+      </div>
+    </div>
+    <script type="text/javascript" th:src="@{/assets/script.js}"></script>
+    <script type="text/javascript" th:replace="${script}"></script>
+  </body>
+</html>
diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html
new file mode 100644
index 0000000000000000000000000000000000000000..69deac41e56ee63e84e22618db267da846f230f1
--- /dev/null
+++ b/backend/src/main/resources/templates/site.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+
+<html
+  xmlns:th="http://www.thymeleaf.org"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}"
+>
+<head>
+  <title>Site <th:block th:text="${model.site.locationName}" /></title>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+</head>
+
+<body>
+<main>
+  <h1>Site <th:block th:text="${model.site.locationName}" /></h1>
+
+  <div th:replace="fragments/map::map"></div>
+
+  <div class="f-card mt-4">
+    <h2>Details</h2>
+    <div class="f-card-body">
+      <th:block th:if="${model.site.uri != null && !model.site.uri.startsWith('urn:')}">
+        <div th:replace="fragments/row::text-row(label='Permanent unique identifier', text=${model.site.uri})"></div>
+      </th:block>
+
+      <div th:replace="fragments/source::source(source=${model.source}, url=${model.site.url}, entityType='site')"></div>
+
+      <div th:replace="fragments/row::text-row(label='Abbreviation', text=${model.site.abbreviation})"></div>
+      <div th:replace="fragments/row::text-row(label='Type', text=${model.site.locationType})"></div>
+      <div th:replace="fragments/row::text-row(label='Status', text=${model.siteStatus})"></div>
+      <div th:replace="fragments/row::text-row(label='Institution/Landowner', text=${model.site.instituteName})"></div>
+      <div th:replace="fragments/row::text-row(label='Institution address', text=${model.site.instituteAddress})"></div>
+      <div th:replace="fragments/row::text-row(label='Coordinates precision', text=${model.coordinatesPrecision})"></div>
+      <th:block th:if="${model.site.latitude}">
+        <div th:replace="fragments/row::text-row(label='Latitude', text=${#coordinates.formatLatitude(model.site.latitude)})"></div>
+      </th:block>
+      <th:block th:if="${model.site.longitude}">
+        <div th:replace="fragments/row::text-row(label='Longitude', text=${#coordinates.formatLongitude(model.site.longitude)})"></div>
+      </th:block>
+      <div th:replace="fragments/row::text-row(label='Geographical location', text=${model.geographicalLocation})"></div>
+      <th:block th:if="${model.site.countryName != null && model.geographicalLocation == null}">
+        <div th:replace="fragments/row::text-row(label='Country name', text=${model.site.countryName})"></div>
+      </th:block>
+
+      <th:block th:if="${model.site.countryCode != null && model.geographicalLocation == null}">
+        <div th:replace="fragments/row::text-row(label='Country code', text=${model.site.countryName})"></div>
+      </th:block>
+
+      <div th:replace="fragments/row::text-row(label='Altitude', text=${model.site.altitude})"></div>
+      <div th:replace="fragments/row::text-row(label='Slope', text=${model.slope})"></div>
+      <div th:replace="fragments/row::text-row(label='Exposure', text=${model.exposure})"></div>
+      <div th:replace="fragments/row::text-row(label='Topography', text=${model.topography})"></div>
+      <div th:replace="fragments/row::text-row(label='Environment type', text=${model.environmentType})"></div>
+      <div th:replace="fragments/row::text-row(label='Distance to city', text=${model.distanceToCity})"></div>
+      <div th:replace="fragments/row::text-row(label='Direction from city', text=${model.directionFromCity})"></div>
+      <div th:replace="fragments/row::text-row(label='Comment', text=${model.comment})"></div>
+    </div>
+  </div>
+
+  <div class="f-card" th:unless="${#lists.isEmpty(model.additionalInfoProperties)}">
+    <h2>Additional info</h2>
+    <div class="f-card-body">
+      <th:block th:each="prop : ${model.additionalInfoProperties}">
+        <div th:replace="fragments/row::text-row(label=${prop.key}, text=${prop.value})"></div>
+      </th:block>
+    </div>
+  </div>
+
+  <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
+</main>
+
+<script th:inline="javascript">
+  faidare.initializeMap({
+    contextPath: [[${#request.getContextPath()}]],
+    locations: /*[[${model.mapLocations}]]*/ []
+  });
+</script>
+</body>
+</html>
diff --git a/backend/src/main/resources/templates/study.html b/backend/src/main/resources/templates/study.html
new file mode 100644
index 0000000000000000000000000000000000000000..5d26f8340dd27dd0bb2d1d850043ad556cd29aec
--- /dev/null
+++ b/backend/src/main/resources/templates/study.html
@@ -0,0 +1,203 @@
+<!DOCTYPE html>
+
+<html
+  xmlns:th="http://www.thymeleaf.org"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}"
+>
+<head>
+  <title>Study <th:block th:text="${model.study.studyType}" />: <th:block th:text="${model.study.studyName}" /></title>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+</head>
+
+<body>
+<main>
+  <h1>Study <th:block th:text="${model.study.studyType}" />: <th:block th:text="${model.study.studyName}" /></h1>
+
+  <div th:replace="fragments/map::map"></div>
+
+  <div class="f-card mt-4">
+    <h2>Identification</h2>
+    <div class="f-card-body">
+      <div th:replace="fragments/row::text-row(label='Name', text=${model.study.studyName})"></div>
+      <div th:replace="fragments/row::text-row(label='Identifier', text=${model.study.studyDbId})"></div>
+
+      <div th:replace="fragments/source::source(source=${model.source}, url=${model.study.url}, entityType='study')"></div>
+
+      <div th:replace="fragments/row::text-row(label='Project name', text=${model.study.programName})"></div>
+      <div th:replace="fragments/row::text-row(label='Description', text=${model.study.studyDescription})"></div>
+      <th:block th:if="${model.study.active != null}">
+        <div th:replace="fragments/row::text-row(label='Active', text=${model.study.active ? 'Yes' : 'No'})"></div>
+      </th:block>
+
+      <th:block th:unless="${#lists.isEmpty(model.study.seasons)}">
+        <div th:replace="fragments/row::text-row(label='Seasons', text=${#strings.listJoin(model.study.seasons, ',')})"></div>
+      </th:block>
+      <th:block th:if="${model.study.startDate != null && model.study.endDate != null}">
+        <div th:replace="fragments/row::text-row(label='Date', text=${'From ' + #dates.format(model.study.startDate, 'yyyy-MM-dd') + ' to ' + #dates.format(model.study.endDate, 'yyyy-MM-dd') })"></div>
+      </th:block>
+      <th:block th:if="${model.study.startDate != null && model.study.endDate == null}">
+        <div th:replace="fragments/row::text-row(label='Date', text=${'Started on ' + #dates.format(model.study.startDate, 'yyyy-MM-dd')})"></div>
+      </th:block>
+
+      <th:block th:if="${model.study.locationDbId}">
+        <div th:replace="fragments/row::row(label='Location name', content=~{::#location})">
+          <a id="location" th:href="@{/sites/{siteId}(siteId=${model.study.locationDbId})}" th:text="${model.study.locationName}"></a>
+        </div>
+      </th:block>
+
+      <th:block th:unless="${#lists.isEmpty(model.study.dataLinks)}">
+        <div th:replace="fragments/row::row(label='Data files', content=~{::#data-files})">
+          <ul id="data-files" class="list-unstyled">
+            <li th:each="dataLink : ${model.study.dataLinks}">
+              <a target="_blank" th:href="${dataLink.url}" th:text="${dataLink.name}"></a>
+            </li>
+          </ul>
+        </div>
+      </th:block>
+    </div>
+  </div>
+
+  <div class="f-card" th:unles="${#lists.isEmpty(model.germplasms)}">
+    <h2>Genotype</h2>
+    <div class="f-card-body">
+      <div class="scroll-table-container scroll-table-container-big">
+        <table class="table table-sm table-striped table-sticky table-responsive-sm">
+          <thead>
+            <tr>
+              <th scope="col">Accession number</th>
+              <th scope="col">Name</th>
+              <th scope="col">Taxon</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="row : ${model.germplasms}">
+              <td>
+                <a th:href="@{/germplasms/{germplasmId}(germplasmId=${row.germplasmDbId})}" th:text="${row.accessionNumber}"></a>
+              </td>
+              <td th:text="${row.germplasmName}"></td>
+              <td th:text="${(row.genus == null ? '' : row.genus) + ' ' + (row.species == null ? '' : row.species)+ ' ' + (row.subtaxa == null ? '' : row.subtaxa) }"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+
+  <div class="f-card" th:unless="${#lists.isEmpty(model.variables)}">
+    <h2>Variables</h2>
+    <div class="f-card-body">
+      <div class="scroll-table-container">
+        <table class="table table-sm table-striped table-sticky table-responsive-sm">
+          <thead>
+            <tr>
+              <th scope="col">Variable ID</th>
+              <th scope="col">Variable short name</th>
+              <th scope="col">Variable long name</th>
+              <th scope="col">Ontology name</th>
+              <th scope="col">Trait description</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="row : ${model.variables}">
+              <td>
+                <a th:unless="${#strings.isEmpty(row.documentationURL)}" th:href="${row.documentationURL}" th:text="${row.observationVariableDbId}" target="_blank" ></a>
+                <span th:if="${#strings.isEmpty(row.documentationURL)}" th:text="${row.observationVariableDbId}"></span>
+              </td>
+              <td th:text="${row.name}"></td>
+              <td th:text="${#lists.isEmpty(row.synonyms) ? '' : row.synonyms[0]}"></td>
+              <td th:text="${row.ontologyName}"></td>
+              <td th:text="${row.trait.description}"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+
+  <div class="f-card" th:unless="${#lists.isEmpty(model.trials)}">
+    <h2>Data Set</h2>
+    <div class="f-card-body">
+      <div class="scroll-table-container scroll-table-container-big">
+        <table class="table table-sm table-striped table-sticky table-responsive-sm">
+          <thead>
+            <tr>
+              <th scope="col">Name</th>
+              <th scope="col">Type</th>
+              <th scope="col">Linked studies identifier</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="row : ${model.trials}">
+              <td>
+                <a th:unless="${#strings.isEmpty(row.documentationURL)}" th:href="${row.documentationURL}" th:text="${row.trialName}" target="_blank" ></a>
+                <span th:if="${#strings.isEmpty(row.documentationURL)}" th:text="${row.trialName}"></span>
+              </td>
+              <td th:text="${row.trialType}"></td>
+              <td style="width: 60%">
+                <th:block th:each="trialStudy, iterStat : ${row.studies}"
+                          th:if="${trialStudy.studyDbId != model.study.studyDbId}">
+                  <a th:href="@{/studies/{studyId}(studyId=${trialStudy.studyDbId})}"
+                     th:text="${trialStudy.studyName.trim()}">
+                  </a><th:block th:if="${iterStat.last}">; </th:block>
+                </th:block>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+
+  <div class="f-card" th:unless="${#lists.isEmpty(model.study.contacts)}">
+    <h2>Contact</h2>
+    <div class="f-card-body">
+      <div class="scroll-table-container">
+        <table class="table table-sm table-striped table-sticky table-responsive-sm">
+          <thead>
+            <tr>
+              <th scope="col">Role</th>
+              <th scope="col">Name</th>
+              <th scope="col">Email</th>
+              <th scope="col">Institution</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="row : ${model.study.contacts}">
+              <td th:text="${row.type}"></td>
+              <td th:text="${row.name}"></td>
+              <td th:text="${row.email}"></td>
+              <td th:text="${row.institutionName}"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+
+  <div class="f-card" th:unless="${#lists.isEmpty(model.additionalInfoProperties)}">
+    <h2>Additional information</h2>
+    <div class="f-card-body">
+      <div class="scroll-table-container">
+        <table class="table table-sm">
+          <tbody>
+            <tr th:each="row : ${model.additionalInfoProperties}">
+              <th class="label" style="width: 33.33%" th:text="${row.key}" scope="row"></th>
+              <td th:text="${row.value}"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+
+  <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
+</main>
+
+<script th:inline="javascript">
+  faidare.initializeMap({
+    contextPath: [[${#request.getContextPath()}]],
+    locations: /*[[${model.mapLocations}]]*/ []
+  });
+</script>
+</body>
+</html>
diff --git a/backend/src/test/java/fr/inra/urgi/faidare/filter/AngularRouteFilterTest.java b/backend/src/test/java/fr/inra/urgi/faidare/filter/AngularRouteFilterTest.java
deleted file mode 100644
index b71e9e39509201d931b6e469ff747524f7a4ddd4..0000000000000000000000000000000000000000
--- a/backend/src/test/java/fr/inra/urgi/faidare/filter/AngularRouteFilterTest.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package fr.inra.urgi.faidare.filter;
-
-import fr.inra.urgi.faidare.Application;
-import fr.inra.urgi.faidare.config.SecurityConfig;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-import org.springframework.context.annotation.Import;
-import org.springframework.core.io.Resource;
-import org.springframework.core.io.ResourceLoader;
-import org.springframework.test.context.junit.jupiter.SpringExtension;
-import org.springframework.test.util.ReflectionTestUtils;
-import org.springframework.test.web.servlet.MockMvc;
-import org.springframework.test.web.servlet.setup.MockMvcBuilders;
-import org.springframework.web.context.WebApplicationContext;
-
-import java.io.ByteArrayInputStream;
-
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl;
-
-/**
- * Unit tests for {@link AngularRouteFilter}
- *
- * @author gcornut
- */
-@ExtendWith(SpringExtension.class)
-@Import(SecurityConfig.class)
-@SpringBootTest(classes = Application.class)
-class AngularRouteFilterTest {
-
-    @Autowired
-    private WebApplicationContext context;
-
-    @MockBean
-    private ResourceLoader resourceLoader;
-
-    private MockMvc mockMvc;
-
-    private AngularRouteFilter filter;
-
-    @BeforeEach
-    void setUp() {
-        filter = new AngularRouteFilter(resourceLoader);
-        mockMvc = MockMvcBuilders.webAppContextSetup(context)
-            .addFilter(filter, "/*")
-            .build();
-    }
-
-    @ParameterizedTest
-    @ValueSource(strings = {
-        // Static files
-        "/index.html",
-        "/script.js",
-        "/style.css",
-        "/image.gif",
-        "/icon.ico",
-        "/image.png",
-        "/image.jpg",
-        "/font.woff",
-        "/font.ttf",
-        // APIs
-        "/brapi/v1/studies",
-        "/faidare/v1/datadiscovery/suggest",
-        "/actuator/info",
-    })
-    void shouldNotForward(String url) throws Exception {
-        mockMvc.perform(get(url)).andExpect(forwardedUrl(null));
-    }
-
-    @ParameterizedTest
-    @ValueSource(strings = {
-        "/home",
-        "/studies/foo",
-        "/germplasm/bar",
-    })
-    void shouldForward(String url) throws Exception {
-        String indexBefore = "<html>\n" +
-            "  <base href=\"./\">\n" +
-            "</html>";
-        String indexAfter = "<html>\n" +
-            "  <base href=\"/gnpis-test/faidare/\">\n" +
-            "</html>";
-
-        ReflectionTestUtils.setField(filter, "serverContextPath", "/gnpis-test/faidare");
-
-        Resource mockResource = mock(Resource.class);
-        when(mockResource.getInputStream())
-            .thenReturn(new ByteArrayInputStream(indexBefore.getBytes()));
-        when(resourceLoader.getResource(anyString()))
-            .thenReturn(mockResource);
-
-        mockMvc.perform(get(url))
-            .andExpect(content().string(indexAfter));
-    }
-
-}
diff --git a/backend/src/test/test.iml b/backend/src/test/test.iml
deleted file mode 100644
index 6e30bb1c6134aa08b8bcfb9660df0484e83585c6..0000000000000000000000000000000000000000
--- a/backend/src/test/test.iml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<module type="JAVA_MODULE" version="4">
-  <component name="NewModuleRootManager" inherit-compiler-output="true">
-    <exclude-output />
-    <content url="file://$MODULE_DIR$">
-      <sourceFolder url="file://$MODULE_DIR$/java" isTestSource="true" />
-    </content>
-    <orderEntry type="inheritedJdk" />
-    <orderEntry type="sourceFolder" forTests="false" />
-    <orderEntry type="module" module-name="main" />
-  </component>
-</module>
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 457aad0d98108420a977756b7145c93c8910b076..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 75b8c7c8c67a003599a36935d1c6a41519fc2207..ffed3a254e91df704a9acc0f2745c0e340d9b582 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index af6708ff229fda75da4f7cc4da4747217bac4d53..1b6c787337ffb79f0e3cf8b1e9f00f680a959de1 100755
--- a/gradlew
+++ b/gradlew
@@ -1,78 +1,129 @@
-#!/usr/bin/env sh
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
 
 ##############################################################################
-##
-##  Gradle start up script for UN*X
-##
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
 ##############################################################################
 
 # Attempt to set APP_HOME
+
 # Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
-    ls=`ls -ld "$PRG"`
-    link=`expr "$ls" : '.*-> \(.*\)$'`
-    if expr "$link" : '/.*' > /dev/null; then
-        PRG="$link"
-    else
-        PRG=`dirname "$PRG"`"/$link"
-    fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
 done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
 
 APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
+APP_BASE_NAME=${0##*/}
 
 # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m"'
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
 
 warn () {
     echo "$*"
-}
+} >&2
 
 die () {
     echo
     echo "$*"
     echo
     exit 1
-}
+} >&2
 
 # OS specific support (must be 'true' or 'false').
 cygwin=false
 msys=false
 darwin=false
 nonstop=false
-case "`uname`" in
-  CYGWIN* )
-    cygwin=true
-    ;;
-  Darwin* )
-    darwin=true
-    ;;
-  MINGW* )
-    msys=true
-    ;;
-  NONSTOP* )
-    nonstop=true
-    ;;
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
 esac
 
 CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
 
+
 # Determine the Java command to use to start the JVM.
 if [ -n "$JAVA_HOME" ] ; then
     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
         # IBM's JDK on AIX uses strange locations for the executables
-        JAVACMD="$JAVA_HOME/jre/sh/java"
+        JAVACMD=$JAVA_HOME/jre/sh/java
     else
-        JAVACMD="$JAVA_HOME/bin/java"
+        JAVACMD=$JAVA_HOME/bin/java
     fi
     if [ ! -x "$JAVACMD" ] ; then
         die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -81,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
 location of your Java installation."
     fi
 else
-    JAVACMD="java"
+    JAVACMD=java
     which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 
 Please set the JAVA_HOME variable in your environment to match the
@@ -89,84 +140,95 @@ location of your Java installation."
 fi
 
 # Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
-    MAX_FD_LIMIT=`ulimit -H -n`
-    if [ $? -eq 0 ] ; then
-        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
-            MAX_FD="$MAX_FD_LIMIT"
-        fi
-        ulimit -n $MAX_FD
-        if [ $? -ne 0 ] ; then
-            warn "Could not set maximum file descriptor limit: $MAX_FD"
-        fi
-    else
-        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
-    fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
 fi
 
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
-    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
 
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
-    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
-    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-    JAVACMD=`cygpath --unix "$JAVACMD"`
-
-    # We build the pattern for arguments to be converted via cygpath
-    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
-    SEP=""
-    for dir in $ROOTDIRSRAW ; do
-        ROOTDIRS="$ROOTDIRS$SEP$dir"
-        SEP="|"
-    done
-    OURCYGPATTERN="(^($ROOTDIRS))"
-    # Add a user-defined pattern to the cygpath arguments
-    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
-        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
-    fi
     # Now convert the arguments - kludge to limit ourselves to /bin/sh
-    i=0
-    for arg in "$@" ; do
-        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
-        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
-
-        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
-            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
-        else
-            eval `echo args$i`="\"$arg\""
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
         fi
-        i=$((i+1))
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
     done
-    case $i in
-        (0) set -- ;;
-        (1) set -- "$args0" ;;
-        (2) set -- "$args0" "$args1" ;;
-        (3) set -- "$args0" "$args1" "$args2" ;;
-        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
-        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
-        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
-        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
-        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
-        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
-    esac
 fi
 
-# Escape application args
-save () {
-    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
-    echo " "
-}
-APP_ARGS=$(save "$@")
-
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-
-# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
-if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
-  cd "$(dirname "$0")"
-fi
+# Collect all arguments for the java command;
+#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+#     shell script including quotes and variable substitutions, so put them in
+#     double quotes to make sure that they get re-expanded; and
+#   * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
 
 exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index 6d57edc706c93465988754383a2d7ff353d4e79f..107acd32c4e687021ef32db511e8a206129b88ec 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -1,3 +1,19 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
 @if "%DEBUG%" == "" @echo off
 @rem ##########################################################################
 @rem
@@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=.
 set APP_BASE_NAME=%~n0
 set APP_HOME=%DIRNAME%
 
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
 @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m"
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
 
 @rem Find java.exe
 if defined JAVA_HOME goto findJavaFromJavaHome
 
 set JAVA_EXE=java.exe
 %JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
+if "%ERRORLEVEL%" == "0" goto execute
 
 echo.
 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -35,7 +54,7 @@ goto fail
 set JAVA_HOME=%JAVA_HOME:"=%
 set JAVA_EXE=%JAVA_HOME%/bin/java.exe
 
-if exist "%JAVA_EXE%" goto init
+if exist "%JAVA_EXE%" goto execute
 
 echo.
 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -45,28 +64,14 @@ echo location of your Java installation.
 
 goto fail
 
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
 :execute
 @rem Setup the command line
 
 set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
 
+
 @rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
 
 :end
 @rem End local scope for the variables with windows NT shell
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 4bd5a482217fc2c6b3921cafc0bdb602ebd96641..b8faef34872a40284f74d8be029e0dd7861df5d7 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,2 +1,2 @@
 rootProject.name = "faidare"
-include("backend", "frontend")
+include("backend", "web")
diff --git a/web/build.gradle.kts b/web/build.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..44076adb023409ae78bfcc8bd986a412a52afc11
--- /dev/null
+++ b/web/build.gradle.kts
@@ -0,0 +1,41 @@
+import com.github.gradle.node.yarn.task.YarnInstallTask
+import com.github.gradle.node.yarn.task.YarnTask
+
+plugins {
+    base
+    id("com.github.node-gradle.node") version "3.0.1"
+}
+
+node {
+    version.set("14.17.0")
+    npmVersion.set("6.14.10")
+    yarnVersion.set("1.22.10")
+    download.set(true)
+}
+
+tasks {
+    npmInstall {
+        enabled = false
+    }
+
+    val prepare by registering {
+        dependsOn(YarnInstallTask.NAME)
+    }
+
+    // This is not a yarn_build task because the task to run is `yarn build:prod`
+    // and tasks with colons are not supported
+    val yarnBuildProd by registering(YarnTask::class) {
+        args.set(listOf("run", "build:prod"))
+        dependsOn(prepare)
+        inputs.file("webpack.config.js")
+        inputs.file("tsconfig.json")
+        inputs.file("package.json")
+        inputs.file("yarn.lock")
+        inputs.dir("src")
+        outputs.dir("$buildDir/dist")
+    }
+
+    assemble {
+        dependsOn(yarnBuildProd)
+    }
+}
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..4ce0986c188f17659ca176c2deb119cd63c73421
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,35 @@
+{
+  "name": "faidaire-web",
+  "version": "1.0.0",
+  "description": "",
+  "private": true,
+  "scripts": {
+    "build": "webpack --mode development",
+    "build:prod": "webpack --mode production",
+    "watch": "webpack --mode development --watch",
+    "watch:prod": "webpack --mode production --watch",
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "MIT",
+  "dependencies": {
+    "@types/leaflet": "1.7.4",
+    "bootstrap": "5.1.0",
+    "leaflet": "1.7.1"
+  },
+  "devDependencies": {
+    "@types/bootstrap": "5.1.2",
+    "@types/leaflet.markercluster": "1.4.5",
+    "clean-webpack-plugin": "4.0.0-alpha.0",
+    "css-loader": "6.2.0",
+    "css-minimizer-webpack-plugin": "3.0.2",
+    "leaflet.markercluster": "1.5.0",
+    "mini-css-extract-plugin": "2.2.0",
+    "sass": "1.38.1",
+    "sass-loader": "12.1.0",
+    "ts-loader": "9.2.5",
+    "typescript": "4.3.5",
+    "webpack": "5.51.1",
+    "webpack-cli": "4.8.0"
+  }
+}
diff --git a/web/src/bootstrap/popovers.ts b/web/src/bootstrap/popovers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..83b703de77469e545f992f1ad2396706db3621bd
--- /dev/null
+++ b/web/src/bootstrap/popovers.ts
@@ -0,0 +1,19 @@
+import { Popover } from 'bootstrap';
+
+export function initializePopovers() {
+    const popoverTriggerList: Array<HTMLElement> = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
+    popoverTriggerList.forEach(popoverTriggerEl => {
+        const options: Partial<Popover.Options> = {};
+        const contentSelector = popoverTriggerEl.dataset.bsElement;
+        if (contentSelector) {
+            const content = document.querySelector(contentSelector);
+            if (content) {
+                options.content = content.innerHTML;
+                options.html = true;
+            } else {
+                throw new Error('element with selector ' + contentSelector + ' not found');
+            }
+        }
+        return new Popover(popoverTriggerEl, options);
+    });
+}
diff --git a/web/src/index.ts b/web/src/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c906b28eb7d5cb4d78785bd6ea5bd01ff4b92fd2
--- /dev/null
+++ b/web/src/index.ts
@@ -0,0 +1,7 @@
+import { initializePopovers } from './bootstrap/popovers';
+import { initializeMap } from './map/map';
+
+(window as any).faidare = {
+    initializePopovers,
+    initializeMap
+}
diff --git a/web/src/map/map.ts b/web/src/map/map.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9fc2a96004bca2ea53eaf342371595c7ea3f5b29
--- /dev/null
+++ b/web/src/map/map.ts
@@ -0,0 +1,93 @@
+import * as L from 'leaflet';
+import 'leaflet.markercluster';
+
+interface MapLocation {
+    locationDbId: string;
+    locationType: 'Origin site' | 'Collecting site' | 'Evaluation site' | null;
+    locationName: string;
+    latitude: number;
+    longitude: number;
+}
+
+interface MapOptions {
+    contextPath: string;
+    locations: Array<MapLocation>;
+}
+
+function markerColor(location: MapLocation) {
+    switch (location.locationType) {
+        case 'Origin site':
+            return 'red';
+        case 'Collecting site':
+            return 'blue';
+        case 'Evaluation site':
+            return 'green';
+    }
+    return 'purple';
+}
+
+function markerIconUrl(contextPath: string, location: MapLocation) {
+    return `${contextPath}/assets/images/marker-icon-${markerColor(location)}.png`;
+}
+
+export function initializeMap(options: MapOptions) {
+    if (!options.locations.length) {
+        return;
+    }
+
+    const mapContainerElement = document.querySelector('#map-container');
+    mapContainerElement!.classList.remove("d-none");
+    const mapElement = document.querySelector('#map') as HTMLElement;
+    const map = L.map(mapElement);
+    L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', {
+        attribution: 'Tiles &copy; Esri &mdash; Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, ' +
+            'Esri China (Hong Kong), Esri (Thailand), TomTom, 2012'
+    }).addTo(map);
+
+    const firstLocation = options.locations[0];
+    map.setView([firstLocation.latitude, firstLocation.longitude], 5);
+
+    const markers = L.markerClusterGroup();
+    const mapMarkers: Array<L.Marker> = [];
+    for (const location of options.locations) {
+        const icon = L.icon({
+            iconUrl: markerIconUrl(options.contextPath, location),
+            iconAnchor: [12, 41], // point of the icon which will correspond to marker's location
+        });
+
+        const popupElement = document.createElement('div');
+
+        const titleElement = document.createElement('strong');
+        titleElement.innerText = location.locationName;
+        popupElement.appendChild(titleElement);
+        popupElement.appendChild(document.createElement('br'));
+
+        if (location.locationType) {
+            const typeElement = document.createElement('span');
+            typeElement.innerText = location.locationType;
+            popupElement.appendChild(typeElement);
+            popupElement.appendChild(document.createElement('br'));
+        }
+
+        const linkElement = document.createElement('a');
+        linkElement.innerText = 'Details';
+        linkElement.href = `${options.contextPath}/sites/${location.locationDbId}`;
+        popupElement.appendChild(linkElement);
+
+        const marker = L.marker(
+            [location.latitude, location.longitude],
+            { icon: icon }
+        );
+        markers.addLayer(marker.bindPopup(popupElement));
+        mapMarkers.push(marker);
+    }
+    const initialZoom = map.getZoom();
+
+    map.fitBounds(L.featureGroup(mapMarkers).getBounds());
+    const markerZoom = map.getZoom();
+
+    setTimeout(() => {
+        map.setZoom(Math.min(initialZoom, markerZoom));
+        map.addLayer(markers);
+    }, 100);
+}
diff --git a/web/src/style/_custom-bootstrap.scss b/web/src/style/_custom-bootstrap.scss
new file mode 100644
index 0000000000000000000000000000000000000000..9de7c3dce4b8fbea20fd88c39b1bc001c72b69a0
--- /dev/null
+++ b/web/src/style/_custom-bootstrap.scss
@@ -0,0 +1,53 @@
+/*!
+ * Bootstrap v5.1.0 (https://getbootstrap.com/)
+ * Copyright 2011-2021 The Bootstrap Authors
+ * Copyright 2011-2021 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */
+
+// scss-docs-start import-stack
+// Configuration
+@import '~bootstrap/scss/functions';
+@import '~bootstrap/scss/variables';
+@import '~bootstrap/scss/mixins';
+@import '~bootstrap/scss/utilities';
+
+// Layout & components
+@import '~bootstrap/scss/root';
+@import '~bootstrap/scss/reboot';
+@import '~bootstrap/scss/type';
+@import '~bootstrap/scss/images';
+@import '~bootstrap/scss/containers';
+@import '~bootstrap/scss/grid';
+@import '~bootstrap/scss/tables';
+// @import '~bootstrap/scss/forms';
+@import '~bootstrap/scss/buttons';
+@import '~bootstrap/scss/transitions';
+@import '~bootstrap/scss/dropdown';
+// @import '~bootstrap/scss/button-group';
+@import '~bootstrap/scss/nav';
+@import '~bootstrap/scss/navbar';
+@import '~bootstrap/scss/card';
+// @import '~bootstrap/scss/accordion';
+// @import '~bootstrap/scss/breadcrumb';
+// @import '~bootstrap/scss/pagination';
+// @import '~bootstrap/scss/badge';
+// @import '~bootstrap/scss/alert';
+// @import '~bootstrap/scss/progress';
+// @import '~bootstrap/scss/list-group';
+// @import '~bootstrap/scss/close';
+// @import '~bootstrap/scss/toasts';
+// @import '~bootstrap/scss/modal';
+// @import '~bootstrap/scss/tooltip';
+@import '~bootstrap/scss/popover';
+// @import '~bootstrap/scss/carousel';
+// @import '~bootstrap/scss/spinners';
+// @import '~bootstrap/scss/offcanvas';
+// @import '~bootstrap/scss/placeholders';
+
+// Helpers
+// @import '~bootstrap/scss/helpers';
+
+// Utilities
+@import '~bootstrap/scss/utilities/api';
+// scss-docs-end import-stack
diff --git a/web/src/style/style.scss b/web/src/style/style.scss
new file mode 100644
index 0000000000000000000000000000000000000000..42aa2bb3a883a5991772d6b9cbe4eddb0eb7d267
--- /dev/null
+++ b/web/src/style/style.scss
@@ -0,0 +1,91 @@
+$headings-color: #0f6191;
+$border-color: #0f6e9f;
+$link-color: #0f6fa1;
+$link-decoration: none;
+$link-hover-decoration: underline;
+$enable-shadows: true;
+$table-border-color: #dee2e6;
+$table-group-separator-color: $table-border-color;
+
+@import 'custom-bootstrap';
+@import '~leaflet/dist/leaflet.css';
+@import '~leaflet.markercluster/dist/MarkerCluster.css';
+@import '~leaflet.markercluster/dist/MarkerCluster.Default.css';
+
+a[role=button] {
+  color: $link-color !important;
+}
+
+.f-row {
+  border-top: 1px solid $gray-300;
+  padding: 0.5rem 0;
+  .label {
+    font-weight: 700;
+  }
+}
+
+.f-card {
+  border: 1px solid $border-color;
+  border-radius: 0.25rem;
+  margin: 0.5rem 0;
+  h2 {
+    font-size: $h4-font-size;
+    padding: 0.5rem 1rem;
+    background-image: repeating-linear-gradient(#0f96cd, #0f6191, #0f76a5);
+    color: $white;
+  }
+  .f-card-body {
+    padding: 0.25rem 1rem;
+    .f-row:first-of-type {
+      border-top: 0;
+    }
+  }
+}
+
+.popover {
+  max-width: min(80vw, 500px);
+}
+
+.popover-header {
+  font-weight: 700;
+}
+
+#map {
+  height: min(400px, 60vh);
+}
+
+.map-legend img {
+  height: 1.5rem;
+}
+
+.content-overflow {
+  max-height: 200px;
+  overflow-y: auto;
+  overflow-x: hidden;
+  &.content-overflow-big {
+    max-height: 275px;
+  }
+}
+
+.scroll-table-container {
+  max-height: 200px;
+  overflow-y: auto;
+  padding-top: 0;
+  &.scroll-table-container-big {
+    max-height: 500px;
+  }
+}
+
+.table-sticky {
+  width: 100%;
+  thead th {
+    position: sticky;
+    position: -webkit-sticky;
+    top: 0;
+    background-color: white;
+    border-top-width: 0;
+    th {
+      padding-top: 0;
+    }
+  }
+}
diff --git a/web/tsconfig.json b/web/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..714af2e552e4d168d08d85597be03eecb652ac23
--- /dev/null
+++ b/web/tsconfig.json
@@ -0,0 +1,72 @@
+{
+  "compilerOptions": {
+    /* Visit https://aka.ms/tsconfig.json to read more about this file */
+
+    /* Basic Options */
+    // "incremental": true,                         /* Enable incremental compilation */
+    "target": "es6",                                /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
+    "module": "es2015",                             /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
+    // "lib": [],                                   /* Specify library files to be included in the compilation. */
+    // "allowJs": true,                             /* Allow javascript files to be compiled. */
+    // "checkJs": true,                             /* Report errors in .js files. */
+    // "jsx": "preserve",                           /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
+    // "declaration": true,                         /* Generates corresponding '.d.ts' file. */
+    // "declarationMap": true,                      /* Generates a sourcemap for each corresponding '.d.ts' file. */
+    // "sourceMap": true,                           /* Generates corresponding '.map' file. */
+    // "outFile": "./",                             /* Concatenate and emit output to single file. */
+    // "outDir": "./",                              /* Redirect output structure to the directory. */
+    // "rootDir": "./",                             /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+    // "composite": true,                           /* Enable project compilation */
+    // "tsBuildInfoFile": "./",                     /* Specify file to store incremental compilation information */
+    // "removeComments": true,                      /* Do not emit comments to output. */
+    // "noEmit": true,                              /* Do not emit outputs. */
+    // "importHelpers": true,                       /* Import emit helpers from 'tslib'. */
+    // "downlevelIteration": true,                  /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+    // "isolatedModules": true,                     /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+
+    /* Strict Type-Checking Options */
+    "strict": true,                                 /* Enable all strict type-checking options. */
+    // "noImplicitAny": true,                       /* Raise error on expressions and declarations with an implied 'any' type. */
+    // "strictNullChecks": true,                    /* Enable strict null checks. */
+    // "strictFunctionTypes": true,                 /* Enable strict checking of function types. */
+    // "strictBindCallApply": true,                 /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+    // "strictPropertyInitialization": true,        /* Enable strict checking of property initialization in classes. */
+    // "noImplicitThis": true,                      /* Raise error on 'this' expressions with an implied 'any' type. */
+    // "alwaysStrict": true,                        /* Parse in strict mode and emit "use strict" for each source file. */
+
+    /* Additional Checks */
+    // "noUnusedLocals": true,                      /* Report errors on unused locals. */
+    // "noUnusedParameters": true,                  /* Report errors on unused parameters. */
+    // "noImplicitReturns": true,                   /* Report error when not all code paths in function return a value. */
+    // "noFallthroughCasesInSwitch": true,          /* Report errors for fallthrough cases in switch statement. */
+    // "noUncheckedIndexedAccess": true,            /* Include 'undefined' in index signature results */
+    // "noImplicitOverride": true,                  /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
+    // "noPropertyAccessFromIndexSignature": true,  /* Require undeclared properties from index signatures to use element accesses. */
+
+    /* Module Resolution Options */
+    // "moduleResolution": "node",                  /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+    // "baseUrl": "./",                             /* Base directory to resolve non-absolute module names. */
+    // "paths": {},                                 /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+    // "rootDirs": [],                              /* List of root folders whose combined content represents the structure of the project at runtime. */
+    // "typeRoots": [],                             /* List of folders to include type definitions from. */
+    // "types": [],                                 /* Type declaration files to be included in compilation. */
+    // "allowSyntheticDefaultImports": true,        /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+    "esModuleInterop": true,                        /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+    // "preserveSymlinks": true,                    /* Do not resolve the real path of symlinks. */
+    // "allowUmdGlobalAccess": true,                /* Allow accessing UMD globals from modules. */
+
+    /* Source Map Options */
+    // "sourceRoot": "",                            /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+    // "mapRoot": "",                               /* Specify the location where debugger should locate map files instead of generated locations. */
+    // "inlineSourceMap": true,                     /* Emit a single file with source maps instead of having a separate file. */
+    // "inlineSources": true,                       /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+
+    /* Experimental Options */
+    // "experimentalDecorators": true,              /* Enables experimental support for ES7 decorators. */
+    // "emitDecoratorMetadata": true,               /* Enables experimental support for emitting type metadata for decorators. */
+
+    /* Advanced Options */
+    "skipLibCheck": true,                           /* Skip type checking of declaration files. */
+    "forceConsistentCasingInFileNames": true        /* Disallow inconsistently-cased references to the same file. */
+  }
+}
diff --git a/web/webpack.config.js b/web/webpack.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..1c7b808029f88e4107f429df70a5fb93e383a29b
--- /dev/null
+++ b/web/webpack.config.js
@@ -0,0 +1,67 @@
+const path = require('path');
+const { CleanWebpackPlugin } = require('clean-webpack-plugin');
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
+
+module.exports = (env, argv) => ({
+    context: path.resolve(__dirname, '.'),
+    // inline source maps only in development mode
+    devtool: argv.mode === 'production' ? undefined : 'inline-source-map',
+    plugins: [
+        // allows extracting the CSS into a CSS file instead of bundling it in a JS file
+        new MiniCssExtractPlugin({
+            filename: argv.mode === 'production' ? '[name].[contenthash].css' : '[name].css'
+        }),
+        // cleans the output directory before each build
+        new CleanWebpackPlugin({
+            // and the empty, useless style.js after each build
+            protectWebpackAssets: false,
+            cleanAfterEveryBuildPatterns: ['style*.js']
+        })
+    ],
+    entry: {
+        // a JS bundle is generated for the index.ts entry point. Since the application is really small
+        // and does not have much JavaScript logic, a single bundle is sufficient
+        script: './src/index.ts',
+        // A CSS bundle is generated for the style.scss entry point.
+        style: './src/style/style.scss'
+    },
+    module: {
+        rules: [
+            {
+                test: /\.tsx?$/,
+                use: 'ts-loader',
+                exclude: /node_modules/,
+            },
+            {
+                // .scss files are loaded by the sass-loader, which transforms them into css loaded by the css-loader
+                // which are then bundled into a css file by the MiniCssExtractPlugin loader
+                test: /\.scss$/i,
+                use: [
+                    MiniCssExtractPlugin.loader,
+                    'css-loader',
+                    'sass-loader'
+                ],
+            },
+        ],
+    },
+    resolve: {
+        // our files are .ts files, but libraries are bundled in .js files
+        extensions: ['.ts', '.js'],
+    },
+    output: {
+        // the output is stored in build/dist/assets
+        path: path.resolve(__dirname, 'build/dist/assets'),
+        filename: argv.mode === 'production' ? '[name].[contenthash].js' : '[name].js'
+    },
+    optimization: {
+        minimizer: [
+            '...',
+            new CssMinimizerPlugin()
+        ]
+    },
+    performance: {
+        maxAssetSize: 300000,
+        maxEntrypointSize: 300000
+    }
+});
diff --git a/web/yarn.lock b/web/yarn.lock
new file mode 100644
index 0000000000000000000000000000000000000000..ba30e63f6aa70b15fb6a0bde06e91ea9c5230bf2
--- /dev/null
+++ b/web/yarn.lock
@@ -0,0 +1,1826 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@discoveryjs/json-ext@^0.5.0":
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz#90420f9f9c6d3987f176a19a7d8e764271a2f55d"
+  integrity sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g==
+
+"@popperjs/core@^2.9.2":
+  version "2.9.3"
+  resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.3.tgz#8b68da1ebd7fc603999cf6ebee34a4899a14b88e"
+  integrity sha512-xDu17cEfh7Kid/d95kB6tZsLOmSWKCZKtprnhVepjsSaCij+lM3mItSJDuuHDMbCWTh8Ejmebwb+KONcCJ0eXQ==
+
+"@trysound/sax@0.1.1":
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.1.1.tgz#3348564048e7a2d7398c935d466c0414ebb6a669"
+  integrity sha512-Z6DoceYb/1xSg5+e+ZlPZ9v0N16ZvZ+wYMraFue4HYrE4ttONKtsvruIRf6t9TBR0YvSOfi1hUU0fJfBLCDYow==
+
+"@types/bootstrap@5.1.2":
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.1.2.tgz#24f08f1957ff5859633f4bf620e921d296e6c3a2"
+  integrity sha512-dSQvMi2dMyNwJU6LZjP0pimuBowsMUvGScYdfqqeiDUoj9TxXZCpfu0cTl94U0Zvw/tdH9j/9ToOhi4LKNLZhg==
+  dependencies:
+    "@popperjs/core" "^2.9.2"
+    "@types/jquery" "*"
+
+"@types/eslint-scope@^3.7.0":
+  version "3.7.1"
+  resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.1.tgz#8dc390a7b4f9dd9f1284629efce982e41612116e"
+  integrity sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==
+  dependencies:
+    "@types/eslint" "*"
+    "@types/estree" "*"
+
+"@types/eslint@*":
+  version "7.28.0"
+  resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.28.0.tgz#7e41f2481d301c68e14f483fe10b017753ce8d5a"
+  integrity sha512-07XlgzX0YJUn4iG1ocY4IX9DzKSmMGUs6ESKlxWhZRaa0fatIWaHWUVapcuGa8r5HFnTqzj+4OCjd5f7EZ/i/A==
+  dependencies:
+    "@types/estree" "*"
+    "@types/json-schema" "*"
+
+"@types/estree@*", "@types/estree@^0.0.50":
+  version "0.0.50"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83"
+  integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==
+
+"@types/geojson@*":
+  version "7946.0.8"
+  resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.8.tgz#30744afdb385e2945e22f3b033f897f76b1f12ca"
+  integrity sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==
+
+"@types/glob@^7.1.1":
+  version "7.1.4"
+  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.4.tgz#ea59e21d2ee5c517914cb4bc8e4153b99e566672"
+  integrity sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA==
+  dependencies:
+    "@types/minimatch" "*"
+    "@types/node" "*"
+
+"@types/jquery@*":
+  version "3.5.6"
+  resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.6.tgz#97ac8e36dccd8ad8ed3f3f3b48933614d9fd8cf0"
+  integrity sha512-SmgCQRzGPId4MZQKDj9Hqc6kSXFNWZFHpELkyK8AQhf8Zr6HKfCzFv9ZC1Fv3FyQttJZOlap3qYb12h61iZAIg==
+  dependencies:
+    "@types/sizzle" "*"
+
+"@types/json-schema@*", "@types/json-schema@^7.0.8":
+  version "7.0.9"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
+  integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
+
+"@types/leaflet.markercluster@1.4.5":
+  version "1.4.5"
+  resolved "https://registry.yarnpkg.com/@types/leaflet.markercluster/-/leaflet.markercluster-1.4.5.tgz#dc4457e2dff9baaacc17c9c04c65ab69b5f361f7"
+  integrity sha512-R9Ql//z6muSGI5mPfr+FaKQQB7EIdQQyivYweVSdOrWr8WyNNFcSwfl+mqGYJFhRRCO/6lbZiM3scEyp9LdaFg==
+  dependencies:
+    "@types/leaflet" "*"
+
+"@types/leaflet@*", "@types/leaflet@1.7.4":
+  version "1.7.4"
+  resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.7.4.tgz#bb9430f69d588ca5829c1ba82657e179454f93a1"
+  integrity sha512-a3qYlMwJ62+WRoiDmYODUD4KywA14jP2XohAkAWtELGuMAD3MohZa/MmIvQDqF52xNI9OYaY8BMsL+9z7yf2HQ==
+  dependencies:
+    "@types/geojson" "*"
+
+"@types/minimatch@*":
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
+  integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
+
+"@types/node@*":
+  version "16.7.2"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.2.tgz#0465a39b5456b61a04d98bd5545f8b34be340cb7"
+  integrity sha512-TbG4TOx9hng8FKxaVrCisdaxKxqEwJ3zwHoCWXZ0Jw6mnvTInpaB99/2Cy4+XxpXtjNv9/TgfGSvZFyfV/t8Fw==
+
+"@types/sizzle@*":
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef"
+  integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==
+
+"@webassemblyjs/ast@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
+  integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==
+  dependencies:
+    "@webassemblyjs/helper-numbers" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+
+"@webassemblyjs/floating-point-hex-parser@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f"
+  integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==
+
+"@webassemblyjs/helper-api-error@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16"
+  integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==
+
+"@webassemblyjs/helper-buffer@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5"
+  integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==
+
+"@webassemblyjs/helper-numbers@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae"
+  integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==
+  dependencies:
+    "@webassemblyjs/floating-point-hex-parser" "1.11.1"
+    "@webassemblyjs/helper-api-error" "1.11.1"
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/helper-wasm-bytecode@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1"
+  integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==
+
+"@webassemblyjs/helper-wasm-section@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a"
+  integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-buffer" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/wasm-gen" "1.11.1"
+
+"@webassemblyjs/ieee754@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614"
+  integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==
+  dependencies:
+    "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/leb128@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5"
+  integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==
+  dependencies:
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/utf8@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff"
+  integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==
+
+"@webassemblyjs/wasm-edit@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6"
+  integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-buffer" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/helper-wasm-section" "1.11.1"
+    "@webassemblyjs/wasm-gen" "1.11.1"
+    "@webassemblyjs/wasm-opt" "1.11.1"
+    "@webassemblyjs/wasm-parser" "1.11.1"
+    "@webassemblyjs/wast-printer" "1.11.1"
+
+"@webassemblyjs/wasm-gen@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76"
+  integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/ieee754" "1.11.1"
+    "@webassemblyjs/leb128" "1.11.1"
+    "@webassemblyjs/utf8" "1.11.1"
+
+"@webassemblyjs/wasm-opt@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2"
+  integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-buffer" "1.11.1"
+    "@webassemblyjs/wasm-gen" "1.11.1"
+    "@webassemblyjs/wasm-parser" "1.11.1"
+
+"@webassemblyjs/wasm-parser@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199"
+  integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-api-error" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/ieee754" "1.11.1"
+    "@webassemblyjs/leb128" "1.11.1"
+    "@webassemblyjs/utf8" "1.11.1"
+
+"@webassemblyjs/wast-printer@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0"
+  integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@xtuc/long" "4.2.2"
+
+"@webpack-cli/configtest@^1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.0.4.tgz#f03ce6311c0883a83d04569e2c03c6238316d2aa"
+  integrity sha512-cs3XLy+UcxiP6bj0A6u7MLLuwdXJ1c3Dtc0RkKg+wiI1g/Ti1om8+/2hc2A2B60NbBNAbMgyBMHvyymWm/j4wQ==
+
+"@webpack-cli/info@^1.3.0":
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.3.0.tgz#9d78a31101a960997a4acd41ffd9b9300627fe2b"
+  integrity sha512-ASiVB3t9LOKHs5DyVUcxpraBXDOKubYu/ihHhU+t1UPpxsivg6Od2E2qU4gJCekfEddzRBzHhzA/Acyw/mlK/w==
+  dependencies:
+    envinfo "^7.7.3"
+
+"@webpack-cli/serve@^1.5.2":
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.5.2.tgz#ea584b637ff63c5a477f6f21604b5a205b72c9ec"
+  integrity sha512-vgJ5OLWadI8aKjDlOH3rb+dYyPd2GTZuQC/Tihjct6F9GpXGZINo3Y/IVuZVTM1eDQB+/AOsjPUWH/WySDaXvw==
+
+"@xtuc/ieee754@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
+  integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
+
+"@xtuc/long@4.2.2":
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
+  integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
+
+acorn-import-assertions@^1.7.6:
+  version "1.7.6"
+  resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.7.6.tgz#580e3ffcae6770eebeec76c3b9723201e9d01f78"
+  integrity sha512-FlVvVFA1TX6l3lp8VjDnYYq7R1nyW6x3svAt4nDgrWQ9SBaSh9CnbwgSUTasgfNfOG5HlM1ehugCvM+hjo56LA==
+
+acorn@^8.4.1:
+  version "8.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c"
+  integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==
+
+ajv-keywords@^3.5.2:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
+  integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
+
+ajv@^6.12.5:
+  version "6.12.6"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+  integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
+alphanum-sort@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
+  integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
+
+ansi-styles@^4.1.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+  dependencies:
+    color-convert "^2.0.1"
+
+anymatch@~3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
+  integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
+array-union@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
+  integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=
+  dependencies:
+    array-uniq "^1.0.1"
+
+array-uniq@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+  integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
+
+balanced-match@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+binary-extensions@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
+boolbase@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+  integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+
+bootstrap@5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.1.0.tgz#543ef8f44f4b9af67b0230f19508542fec38ef55"
+  integrity sha512-bs74WNI9BgBo3cEovmdMHikSKoXnDgA6VQjJ7TyTotU6L7d41ZyCEEelPwkYEzsG/Zjv3ie9IE3EMAje0W9Xew==
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+braces@^3.0.1, braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.0, browserslist@^4.16.6:
+  version "4.16.8"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.8.tgz#cb868b0b554f137ba6e33de0ecff2eda403c4fb0"
+  integrity sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ==
+  dependencies:
+    caniuse-lite "^1.0.30001251"
+    colorette "^1.3.0"
+    electron-to-chromium "^1.3.811"
+    escalade "^3.1.1"
+    node-releases "^1.1.75"
+
+buffer-from@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+caniuse-api@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
+  integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
+  dependencies:
+    browserslist "^4.0.0"
+    caniuse-lite "^1.0.0"
+    lodash.memoize "^4.1.2"
+    lodash.uniq "^4.5.0"
+
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001251:
+  version "1.0.30001252"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001252.tgz#cb16e4e3dafe948fc4a9bb3307aea054b912019a"
+  integrity sha512-I56jhWDGMtdILQORdusxBOH+Nl/KgQSdDmpJezYddnAkVOmnoU8zwjTV9xAjMIYxr0iPreEAVylCGcmHCjfaOw==
+
+chalk@^4.1.0:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+"chokidar@>=3.0.0 <4.0.0":
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
+  integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
+  dependencies:
+    anymatch "~3.1.2"
+    braces "~3.0.2"
+    glob-parent "~5.1.2"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.6.0"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+chrome-trace-event@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
+  integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
+
+clean-webpack-plugin@4.0.0-alpha.0:
+  version "4.0.0-alpha.0"
+  resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0-alpha.0.tgz#2aef48dfe7565360d128f5caa0904097d969d053"
+  integrity sha512-+X6mASBbGSVyw8L9/1rhQ+vS4uaQMopf194kX7Aes8qfezgCFL+qv5W0nwP3a0Tud5kUckARk8tFcoyOSKEjhg==
+  dependencies:
+    del "^4.1.1"
+
+clone-deep@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
+  integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
+  dependencies:
+    is-plain-object "^2.0.4"
+    kind-of "^6.0.2"
+    shallow-clone "^3.0.0"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+colord@^2.0.1, colord@^2.6:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/colord/-/colord-2.7.0.tgz#706ea36fe0cd651b585eb142fe64b6480185270e"
+  integrity sha512-pZJBqsHz+pYyw3zpX6ZRXWoCHM1/cvFikY9TV8G3zcejCaKE0lhankoj8iScyrrePA8C7yJ5FStfA9zbcOnw7Q==
+
+colorette@^1.2.1, colorette@^1.2.2, colorette@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.3.0.tgz#ff45d2f0edb244069d3b772adeb04fed38d0a0af"
+  integrity sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==
+
+commander@^2.20.0:
+  version "2.20.3"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+commander@^7.0.0, commander@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+cross-spawn@^7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+  dependencies:
+    path-key "^3.1.0"
+    shebang-command "^2.0.0"
+    which "^2.0.1"
+
+css-color-names@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-1.0.1.tgz#6ff7ee81a823ad46e020fa2fd6ab40a887e2ba67"
+  integrity sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==
+
+css-declaration-sorter@^6.0.3:
+  version "6.1.1"
+  resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.1.1.tgz#77b32b644ba374bc562c0fc6f4fdaba4dfb0b749"
+  integrity sha512-BZ1aOuif2Sb7tQYY1GeCjG7F++8ggnwUkH5Ictw0mrdpqpEd+zWmcPdstnH2TItlb74FqR0DrVEieon221T/1Q==
+  dependencies:
+    timsort "^0.3.0"
+
+css-loader@6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.2.0.tgz#9663d9443841de957a3cb9bcea2eda65b3377071"
+  integrity sha512-/rvHfYRjIpymZblf49w8jYcRo2y9gj6rV8UroHGmBxKrIyGLokpycyKzp9OkitvqT29ZSpzJ0Ic7SpnJX3sC8g==
+  dependencies:
+    icss-utils "^5.1.0"
+    postcss "^8.2.15"
+    postcss-modules-extract-imports "^3.0.0"
+    postcss-modules-local-by-default "^4.0.0"
+    postcss-modules-scope "^3.0.0"
+    postcss-modules-values "^4.0.0"
+    postcss-value-parser "^4.1.0"
+    semver "^7.3.5"
+
+css-minimizer-webpack-plugin@3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.0.2.tgz#8fadbdf10128cb40227bff275a4bb47412534245"
+  integrity sha512-B3I5e17RwvKPJwsxjjWcdgpU/zqylzK1bPVghcmpFHRL48DXiBgrtqz1BJsn68+t/zzaLp9kYAaEDvQ7GyanFQ==
+  dependencies:
+    cssnano "^5.0.6"
+    jest-worker "^27.0.2"
+    p-limit "^3.0.2"
+    postcss "^8.3.5"
+    schema-utils "^3.0.0"
+    serialize-javascript "^6.0.0"
+    source-map "^0.6.1"
+
+css-select@^4.1.3:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067"
+  integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==
+  dependencies:
+    boolbase "^1.0.0"
+    css-what "^5.0.0"
+    domhandler "^4.2.0"
+    domutils "^2.6.0"
+    nth-check "^2.0.0"
+
+css-tree@^1.1.2, css-tree@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
+  integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
+  dependencies:
+    mdn-data "2.0.14"
+    source-map "^0.6.1"
+
+css-what@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad"
+  integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==
+
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+cssnano-preset-default@^5.1.4:
+  version "5.1.4"
+  resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.1.4.tgz#359943bf00c5c8e05489f12dd25f3006f2c1cbd2"
+  integrity sha512-sPpQNDQBI3R/QsYxQvfB4mXeEcWuw0wGtKtmS5eg8wudyStYMgKOQT39G07EbW1LB56AOYrinRS9f0ig4Y3MhQ==
+  dependencies:
+    css-declaration-sorter "^6.0.3"
+    cssnano-utils "^2.0.1"
+    postcss-calc "^8.0.0"
+    postcss-colormin "^5.2.0"
+    postcss-convert-values "^5.0.1"
+    postcss-discard-comments "^5.0.1"
+    postcss-discard-duplicates "^5.0.1"
+    postcss-discard-empty "^5.0.1"
+    postcss-discard-overridden "^5.0.1"
+    postcss-merge-longhand "^5.0.2"
+    postcss-merge-rules "^5.0.2"
+    postcss-minify-font-values "^5.0.1"
+    postcss-minify-gradients "^5.0.2"
+    postcss-minify-params "^5.0.1"
+    postcss-minify-selectors "^5.1.0"
+    postcss-normalize-charset "^5.0.1"
+    postcss-normalize-display-values "^5.0.1"
+    postcss-normalize-positions "^5.0.1"
+    postcss-normalize-repeat-style "^5.0.1"
+    postcss-normalize-string "^5.0.1"
+    postcss-normalize-timing-functions "^5.0.1"
+    postcss-normalize-unicode "^5.0.1"
+    postcss-normalize-url "^5.0.2"
+    postcss-normalize-whitespace "^5.0.1"
+    postcss-ordered-values "^5.0.2"
+    postcss-reduce-initial "^5.0.1"
+    postcss-reduce-transforms "^5.0.1"
+    postcss-svgo "^5.0.2"
+    postcss-unique-selectors "^5.0.1"
+
+cssnano-utils@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-2.0.1.tgz#8660aa2b37ed869d2e2f22918196a9a8b6498ce2"
+  integrity sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==
+
+cssnano@^5.0.6:
+  version "5.0.8"
+  resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.0.8.tgz#39ad166256980fcc64faa08c9bb18bb5789ecfa9"
+  integrity sha512-Lda7geZU0Yu+RZi2SGpjYuQz4HI4/1Y+BhdD0jL7NXAQ5larCzVn+PUGuZbDMYz904AXXCOgO5L1teSvgu7aFg==
+  dependencies:
+    cssnano-preset-default "^5.1.4"
+    is-resolvable "^1.1.0"
+    lilconfig "^2.0.3"
+    yaml "^1.10.2"
+
+csso@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
+  integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
+  dependencies:
+    css-tree "^1.1.2"
+
+del@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4"
+  integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==
+  dependencies:
+    "@types/glob" "^7.1.1"
+    globby "^6.1.0"
+    is-path-cwd "^2.0.0"
+    is-path-in-cwd "^2.0.0"
+    p-map "^2.0.0"
+    pify "^4.0.1"
+    rimraf "^2.6.3"
+
+dom-serializer@^1.0.1:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91"
+  integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==
+  dependencies:
+    domelementtype "^2.0.1"
+    domhandler "^4.2.0"
+    entities "^2.0.0"
+
+domelementtype@^2.0.1, domelementtype@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
+  integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
+
+domhandler@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059"
+  integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==
+  dependencies:
+    domelementtype "^2.2.0"
+
+domutils@^2.6.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
+  integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
+  dependencies:
+    dom-serializer "^1.0.1"
+    domelementtype "^2.2.0"
+    domhandler "^4.2.0"
+
+electron-to-chromium@^1.3.811:
+  version "1.3.818"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.818.tgz#32ed024fa8316e5d469c96eecbea7d2463d80085"
+  integrity sha512-c/Z9gIr+jDZAR9q+mn40hEc1NharBT+8ejkarjbCDnBNFviI6hvcC5j2ezkAXru//bTnQp5n6iPi0JA83Tla1Q==
+
+enhanced-resolve@^5.0.0, enhanced-resolve@^5.8.0:
+  version "5.8.2"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz#15ddc779345cbb73e97c611cd00c01c1e7bf4d8b"
+  integrity sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==
+  dependencies:
+    graceful-fs "^4.2.4"
+    tapable "^2.2.0"
+
+entities@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
+  integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
+
+envinfo@^7.7.3:
+  version "7.8.1"
+  resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475"
+  integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==
+
+es-module-lexer@^0.7.1:
+  version "0.7.1"
+  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.7.1.tgz#c2c8e0f46f2df06274cdaf0dd3f3b33e0a0b267d"
+  integrity sha512-MgtWFl5No+4S3TmhDmCz2ObFGm6lEpTnzbQi+Dd+pw4mlTIZTmM2iAs5gRlmx5zS9luzobCSBSI90JM/1/JgOw==
+
+escalade@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+eslint-scope@5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+  integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
+  dependencies:
+    esrecurse "^4.3.0"
+    estraverse "^4.1.1"
+
+esrecurse@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
+  integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+  dependencies:
+    estraverse "^5.2.0"
+
+estraverse@^4.1.1:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+  integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+estraverse@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
+  integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==
+
+events@^3.2.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+  integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
+execa@^5.0.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
+  integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
+  dependencies:
+    cross-spawn "^7.0.3"
+    get-stream "^6.0.0"
+    human-signals "^2.1.0"
+    is-stream "^2.0.0"
+    merge-stream "^2.0.0"
+    npm-run-path "^4.0.1"
+    onetime "^5.1.2"
+    signal-exit "^3.0.3"
+    strip-final-newline "^2.0.0"
+
+fast-deep-equal@^3.1.1:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-json-stable-stringify@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fastest-levenshtein@^1.0.12:
+  version "1.0.12"
+  resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2"
+  integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+find-up@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+  integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+  dependencies:
+    locate-path "^5.0.0"
+    path-exists "^4.0.0"
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@~2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+get-stream@^6.0.0:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
+  integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
+
+glob-parent@~5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
+glob-to-regexp@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
+  integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
+
+glob@^7.0.3, glob@^7.1.3:
+  version "7.1.7"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
+  integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+globby@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
+  integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=
+  dependencies:
+    array-union "^1.0.1"
+    glob "^7.0.3"
+    object-assign "^4.0.1"
+    pify "^2.0.0"
+    pinkie-promise "^2.0.0"
+
+graceful-fs@^4.1.2, graceful-fs@^4.2.4:
+  version "4.2.8"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
+  integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
+human-signals@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
+  integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
+
+icss-utils@^5.0.0, icss-utils@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
+  integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
+
+import-local@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6"
+  integrity sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==
+  dependencies:
+    pkg-dir "^4.2.0"
+    resolve-cwd "^3.0.0"
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+interpret@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
+  integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==
+
+is-absolute-url@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698"
+  integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==
+
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
+is-core-module@^2.2.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.6.0.tgz#d7553b2526fe59b92ba3e40c8df757ec8a709e19"
+  integrity sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==
+  dependencies:
+    has "^1.0.3"
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-glob@^4.0.1, is-glob@~4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
+  integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-path-cwd@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb"
+  integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==
+
+is-path-in-cwd@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb"
+  integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==
+  dependencies:
+    is-path-inside "^2.1.0"
+
+is-path-inside@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2"
+  integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==
+  dependencies:
+    path-is-inside "^1.0.2"
+
+is-plain-object@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+  integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+  dependencies:
+    isobject "^3.0.1"
+
+is-resolvable@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
+  integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
+
+is-stream@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
+  integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+isobject@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+  integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
+jest-worker@^27.0.2:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.6.tgz#a5fdb1e14ad34eb228cfe162d9f729cdbfa28aed"
+  integrity sha512-qupxcj/dRuA3xHPMUd40gr2EaAurFbkwzOh7wfPaeE9id7hyjURRQoqNfHifHK3XjJU6YJJUQKILGUnwGPEOCA==
+  dependencies:
+    "@types/node" "*"
+    merge-stream "^2.0.0"
+    supports-color "^8.0.0"
+
+json-parse-better-errors@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+  integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+json-schema-traverse@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+kind-of@^6.0.2:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+  integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+klona@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0"
+  integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==
+
+leaflet.markercluster@1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.5.0.tgz#54db42485da32fc3d92c7ae22d0d7982879e0b67"
+  integrity sha512-Fvf/cq4o806mJL50n+fZW9+QALDDLPvt7vuAjlD2vfnxx3srMDs2vWINJze4nKYJYRY45OC6tM/669C3pLwMCA==
+
+leaflet@1.7.1:
+  version "1.7.1"
+  resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.7.1.tgz#10d684916edfe1bf41d688a3b97127c0322a2a19"
+  integrity sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw==
+
+lilconfig@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.3.tgz#68f3005e921dafbd2a2afb48379986aa6d2579fd"
+  integrity sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==
+
+loader-runner@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
+  integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
+
+locate-path@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+  integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+  dependencies:
+    p-locate "^4.1.0"
+
+lodash.memoize@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+
+lodash.uniq@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+  integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
+
+lru-cache@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+  dependencies:
+    yallist "^4.0.0"
+
+mdn-data@2.0.14:
+  version "2.0.14"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
+  integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
+
+merge-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+  integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
+micromatch@^4.0.0:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
+  integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
+  dependencies:
+    braces "^3.0.1"
+    picomatch "^2.2.3"
+
+mime-db@1.49.0:
+  version "1.49.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed"
+  integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==
+
+mime-types@^2.1.27:
+  version "2.1.32"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5"
+  integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==
+  dependencies:
+    mime-db "1.49.0"
+
+mimic-fn@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+  integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+
+mini-css-extract-plugin@2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.2.0.tgz#48cb6d2bea8fa9eb36709856e003662eebb3eb92"
+  integrity sha512-91HeVHbq7PUJ4TwOuMTlFWfVWrLqf3SF0PlEDPV+wtgsfxrMebN9LLzflyQqdKLp4/H3PexRB1WLKsCqpWKkxQ==
+  dependencies:
+    schema-utils "^3.1.0"
+
+minimatch@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+  integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+nanoid@^3.1.23:
+  version "3.1.25"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152"
+  integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==
+
+neo-async@^2.6.2:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
+  integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+
+node-releases@^1.1.75:
+  version "1.1.75"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.75.tgz#6dd8c876b9897a1b8e5a02de26afa79bb54ebbfe"
+  integrity sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+normalize-url@^6.0.1:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
+  integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
+
+npm-run-path@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
+  integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
+  dependencies:
+    path-key "^3.0.0"
+
+nth-check@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125"
+  integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==
+  dependencies:
+    boolbase "^1.0.0"
+
+object-assign@^4.0.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+  integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+once@^1.3.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+  dependencies:
+    wrappy "1"
+
+onetime@^5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
+  integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
+  dependencies:
+    mimic-fn "^2.1.0"
+
+p-limit@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+  dependencies:
+    p-try "^2.0.0"
+
+p-limit@^3.0.2, p-limit@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+  integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+  dependencies:
+    yocto-queue "^0.1.0"
+
+p-locate@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+  integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+  dependencies:
+    p-limit "^2.2.0"
+
+p-map@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
+  integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
+
+p-try@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+path-exists@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+  integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+path-is-inside@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+  integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
+
+path-key@^3.0.0, path-key@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.6:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
+  integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
+
+pify@^2.0.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+  integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
+
+pify@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
+  integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
+
+pinkie-promise@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+  integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o=
+  dependencies:
+    pinkie "^2.0.0"
+
+pinkie@^2.0.0:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
+  integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
+
+pkg-dir@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+  integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+  dependencies:
+    find-up "^4.0.0"
+
+postcss-calc@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.0.0.tgz#a05b87aacd132740a5db09462a3612453e5df90a"
+  integrity sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g==
+  dependencies:
+    postcss-selector-parser "^6.0.2"
+    postcss-value-parser "^4.0.2"
+
+postcss-colormin@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.2.0.tgz#2b620b88c0ff19683f3349f4cf9e24ebdafb2c88"
+  integrity sha512-+HC6GfWU3upe5/mqmxuqYZ9B2Wl4lcoUUNkoaX59nEWV4EtADCMiBqui111Bu8R8IvaZTmqmxrqOAqjbHIwXPw==
+  dependencies:
+    browserslist "^4.16.6"
+    caniuse-api "^3.0.0"
+    colord "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-convert-values@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.0.1.tgz#4ec19d6016534e30e3102fdf414e753398645232"
+  integrity sha512-C3zR1Do2BkKkCgC0g3sF8TS0koF2G+mN8xxayZx3f10cIRmTaAnpgpRQZjNekTZxM2ciSPoh2IWJm0VZx8NoQg==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-discard-comments@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz#9eae4b747cf760d31f2447c27f0619d5718901fe"
+  integrity sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==
+
+postcss-discard-duplicates@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz#68f7cc6458fe6bab2e46c9f55ae52869f680e66d"
+  integrity sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==
+
+postcss-discard-empty@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz#ee136c39e27d5d2ed4da0ee5ed02bc8a9f8bf6d8"
+  integrity sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==
+
+postcss-discard-overridden@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz#454b41f707300b98109a75005ca4ab0ff2743ac6"
+  integrity sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==
+
+postcss-merge-longhand@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.0.2.tgz#277ada51d9a7958e8ef8cf263103c9384b322a41"
+  integrity sha512-BMlg9AXSI5G9TBT0Lo/H3PfUy63P84rVz3BjCFE9e9Y9RXQZD3+h3YO1kgTNsNJy7bBc1YQp8DmSnwLIW5VPcw==
+  dependencies:
+    css-color-names "^1.0.1"
+    postcss-value-parser "^4.1.0"
+    stylehacks "^5.0.1"
+
+postcss-merge-rules@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz#d6e4d65018badbdb7dcc789c4f39b941305d410a"
+  integrity sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg==
+  dependencies:
+    browserslist "^4.16.6"
+    caniuse-api "^3.0.0"
+    cssnano-utils "^2.0.1"
+    postcss-selector-parser "^6.0.5"
+    vendors "^1.0.3"
+
+postcss-minify-font-values@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz#a90cefbfdaa075bd3dbaa1b33588bb4dc268addf"
+  integrity sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-minify-gradients@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.0.2.tgz#7c175c108f06a5629925d698b3c4cf7bd3864ee5"
+  integrity sha512-7Do9JP+wqSD6Prittitt2zDLrfzP9pqKs2EcLX7HJYxsxCOwrrcLt4x/ctQTsiOw+/8HYotAoqNkrzItL19SdQ==
+  dependencies:
+    colord "^2.6"
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-minify-params@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz#371153ba164b9d8562842fdcd929c98abd9e5b6c"
+  integrity sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw==
+  dependencies:
+    alphanum-sort "^1.0.2"
+    browserslist "^4.16.0"
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+    uniqs "^2.0.0"
+
+postcss-minify-selectors@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz#4385c845d3979ff160291774523ffa54eafd5a54"
+  integrity sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og==
+  dependencies:
+    alphanum-sort "^1.0.2"
+    postcss-selector-parser "^6.0.5"
+
+postcss-modules-extract-imports@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
+  integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
+
+postcss-modules-local-by-default@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c"
+  integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==
+  dependencies:
+    icss-utils "^5.0.0"
+    postcss-selector-parser "^6.0.2"
+    postcss-value-parser "^4.1.0"
+
+postcss-modules-scope@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06"
+  integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==
+  dependencies:
+    postcss-selector-parser "^6.0.4"
+
+postcss-modules-values@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c"
+  integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==
+  dependencies:
+    icss-utils "^5.0.0"
+
+postcss-normalize-charset@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz#121559d1bebc55ac8d24af37f67bd4da9efd91d0"
+  integrity sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==
+
+postcss-normalize-display-values@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz#62650b965981a955dffee83363453db82f6ad1fd"
+  integrity sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-positions@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz#868f6af1795fdfa86fbbe960dceb47e5f9492fe5"
+  integrity sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-repeat-style@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz#cbc0de1383b57f5bb61ddd6a84653b5e8665b2b5"
+  integrity sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-string@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz#d9eafaa4df78c7a3b973ae346ef0e47c554985b0"
+  integrity sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-timing-functions@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz#8ee41103b9130429c6cbba736932b75c5e2cb08c"
+  integrity sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-unicode@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz#82d672d648a411814aa5bf3ae565379ccd9f5e37"
+  integrity sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA==
+  dependencies:
+    browserslist "^4.16.0"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-url@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.0.2.tgz#ddcdfb7cede1270740cf3e4dfc6008bd96abc763"
+  integrity sha512-k4jLTPUxREQ5bpajFQZpx8bCF2UrlqOTzP9kEqcEnOfwsRshWs2+oAFIHfDQB8GO2PaUaSE0NlTAYtbluZTlHQ==
+  dependencies:
+    is-absolute-url "^3.0.3"
+    normalize-url "^6.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-whitespace@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz#b0b40b5bcac83585ff07ead2daf2dcfbeeef8e9a"
+  integrity sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-ordered-values@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz#1f351426977be00e0f765b3164ad753dac8ed044"
+  integrity sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-reduce-initial@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz#9d6369865b0f6f6f6b165a0ef5dc1a4856c7e946"
+  integrity sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw==
+  dependencies:
+    browserslist "^4.16.0"
+    caniuse-api "^3.0.0"
+
+postcss-reduce-transforms@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz#93c12f6a159474aa711d5269923e2383cedcf640"
+  integrity sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5:
+  version "6.0.6"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea"
+  integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==
+  dependencies:
+    cssesc "^3.0.0"
+    util-deprecate "^1.0.2"
+
+postcss-svgo@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.0.2.tgz#bc73c4ea4c5a80fbd4b45e29042c34ceffb9257f"
+  integrity sha512-YzQuFLZu3U3aheizD+B1joQ94vzPfE6BNUcSYuceNxlVnKKsOtdo6hL9/zyC168Q8EwfLSgaDSalsUGa9f2C0A==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+    svgo "^2.3.0"
+
+postcss-unique-selectors@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz#3be5c1d7363352eff838bd62b0b07a0abad43bfc"
+  integrity sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w==
+  dependencies:
+    alphanum-sort "^1.0.2"
+    postcss-selector-parser "^6.0.5"
+    uniqs "^2.0.0"
+
+postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
+  integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
+
+postcss@^8.2.15, postcss@^8.3.5:
+  version "8.3.6"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea"
+  integrity sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==
+  dependencies:
+    colorette "^1.2.2"
+    nanoid "^3.1.23"
+    source-map-js "^0.6.2"
+
+punycode@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+randombytes@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+  integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+  dependencies:
+    safe-buffer "^5.1.0"
+
+readdirp@~3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+  dependencies:
+    picomatch "^2.2.1"
+
+rechoir@^0.7.0:
+  version "0.7.1"
+  resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686"
+  integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==
+  dependencies:
+    resolve "^1.9.0"
+
+resolve-cwd@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
+  integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==
+  dependencies:
+    resolve-from "^5.0.0"
+
+resolve-from@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+  integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
+resolve@^1.9.0:
+  version "1.20.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
+  integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
+  dependencies:
+    is-core-module "^2.2.0"
+    path-parse "^1.0.6"
+
+rimraf@^2.6.3:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
+  integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
+  dependencies:
+    glob "^7.1.3"
+
+safe-buffer@^5.1.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+sass-loader@12.1.0:
+  version "12.1.0"
+  resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.1.0.tgz#b73324622231009da6fba61ab76013256380d201"
+  integrity sha512-FVJZ9kxVRYNZTIe2xhw93n3xJNYZADr+q69/s98l9nTCrWASo+DR2Ot0s5xTKQDDEosUkatsGeHxcH4QBp5bSg==
+  dependencies:
+    klona "^2.0.4"
+    neo-async "^2.6.2"
+
+sass@1.38.1:
+  version "1.38.1"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.38.1.tgz#54dfb17fb168846b5850324b82fc62dc68f51bad"
+  integrity sha512-Lj8nPaSYOuRhgqdyShV50fY5jKnvaRmikUNalMPmbH+tKMGgEKVkltI/lP30PEfO2T1t6R9yc2QIBLgOc3uaFw==
+  dependencies:
+    chokidar ">=3.0.0 <4.0.0"
+
+schema-utils@^3.0.0, schema-utils@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281"
+  integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==
+  dependencies:
+    "@types/json-schema" "^7.0.8"
+    ajv "^6.12.5"
+    ajv-keywords "^3.5.2"
+
+semver@^7.3.4, semver@^7.3.5:
+  version "7.3.5"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
+  integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
+  dependencies:
+    lru-cache "^6.0.0"
+
+serialize-javascript@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
+  integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
+  dependencies:
+    randombytes "^2.1.0"
+
+shallow-clone@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
+  integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
+  dependencies:
+    kind-of "^6.0.2"
+
+shebang-command@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+  integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+  dependencies:
+    shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+signal-exit@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
+  integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
+
+source-map-js@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
+  integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
+
+source-map-support@~0.5.19:
+  version "0.5.19"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
+  integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map@^0.6.0, source-map@^0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+source-map@~0.7.2:
+  version "0.7.3"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
+  integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
+
+stable@^0.1.8:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
+  integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+
+strip-final-newline@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
+  integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
+
+stylehacks@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.0.1.tgz#323ec554198520986806388c7fdaebc38d2c06fb"
+  integrity sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA==
+  dependencies:
+    browserslist "^4.16.0"
+    postcss-selector-parser "^6.0.4"
+
+supports-color@^7.1.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+  dependencies:
+    has-flag "^4.0.0"
+
+supports-color@^8.0.0:
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+  integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+  dependencies:
+    has-flag "^4.0.0"
+
+svgo@^2.3.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.5.0.tgz#3c9051b606d85a02fcb59f459b19970d2cc2c9bf"
+  integrity sha512-FSdBOOo271VyF/qZnOn1PgwCdt1v4Dx0Sey+U1jgqm1vqRYjPGdip0RGrFW6ItwtkBB8rHgHk26dlVr0uCs82Q==
+  dependencies:
+    "@trysound/sax" "0.1.1"
+    colorette "^1.3.0"
+    commander "^7.2.0"
+    css-select "^4.1.3"
+    css-tree "^1.1.3"
+    csso "^4.2.0"
+    stable "^0.1.8"
+
+tapable@^2.1.1, tapable@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b"
+  integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==
+
+terser-webpack-plugin@^5.1.3:
+  version "5.1.4"
+  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.4.tgz#c369cf8a47aa9922bd0d8a94fe3d3da11a7678a1"
+  integrity sha512-C2WkFwstHDhVEmsmlCxrXUtVklS+Ir1A7twrYzrDrQQOIMOaVAYykaoo/Aq1K0QRkMoY2hhvDQY1cm4jnIMFwA==
+  dependencies:
+    jest-worker "^27.0.2"
+    p-limit "^3.1.0"
+    schema-utils "^3.0.0"
+    serialize-javascript "^6.0.0"
+    source-map "^0.6.1"
+    terser "^5.7.0"
+
+terser@^5.7.0:
+  version "5.7.2"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.2.tgz#d4d95ed4f8bf735cb933e802f2a1829abf545e3f"
+  integrity sha512-0Omye+RD4X7X69O0eql3lC4Heh/5iLj3ggxR/B5ketZLOtLiOqukUgjw3q4PDnNQbsrkKr3UMypqStQG3XKRvw==
+  dependencies:
+    commander "^2.20.0"
+    source-map "~0.7.2"
+    source-map-support "~0.5.19"
+
+timsort@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
+  integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+ts-loader@9.2.5:
+  version "9.2.5"
+  resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.5.tgz#127733a5e9243bf6dafcb8aa3b8a266d8041dca9"
+  integrity sha512-al/ATFEffybdRMUIr5zMEWQdVnCGMUA9d3fXJ8dBVvBlzytPvIszoG9kZoR+94k6/i293RnVOXwMaWbXhNy9pQ==
+  dependencies:
+    chalk "^4.1.0"
+    enhanced-resolve "^5.0.0"
+    micromatch "^4.0.0"
+    semver "^7.3.4"
+
+typescript@4.3.5:
+  version "4.3.5"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
+  integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
+
+uniqs@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
+  integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI=
+
+uri-js@^4.2.2:
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+  integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+  dependencies:
+    punycode "^2.1.0"
+
+util-deprecate@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+v8-compile-cache@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
+  integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
+
+vendors@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
+  integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
+
+watchpack@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.2.0.tgz#47d78f5415fe550ecd740f99fe2882323a58b1ce"
+  integrity sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==
+  dependencies:
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.1.2"
+
+webpack-cli@4.8.0:
+  version "4.8.0"
+  resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.8.0.tgz#5fc3c8b9401d3c8a43e2afceacfa8261962338d1"
+  integrity sha512-+iBSWsX16uVna5aAYN6/wjhJy1q/GKk4KjKvfg90/6hykCTSgozbfz5iRgDTSJt/LgSbYxdBX3KBHeobIs+ZEw==
+  dependencies:
+    "@discoveryjs/json-ext" "^0.5.0"
+    "@webpack-cli/configtest" "^1.0.4"
+    "@webpack-cli/info" "^1.3.0"
+    "@webpack-cli/serve" "^1.5.2"
+    colorette "^1.2.1"
+    commander "^7.0.0"
+    execa "^5.0.0"
+    fastest-levenshtein "^1.0.12"
+    import-local "^3.0.2"
+    interpret "^2.2.0"
+    rechoir "^0.7.0"
+    v8-compile-cache "^2.2.0"
+    webpack-merge "^5.7.3"
+
+webpack-merge@^5.7.3:
+  version "5.8.0"
+  resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61"
+  integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==
+  dependencies:
+    clone-deep "^4.0.1"
+    wildcard "^2.0.0"
+
+webpack-sources@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.0.tgz#b16973bcf844ebcdb3afde32eda1c04d0b90f89d"
+  integrity sha512-fahN08Et7P9trej8xz/Z7eRu8ltyiygEo/hnRi9KqBUs80KeDcnf96ZJo++ewWd84fEf3xSX9bp4ZS9hbw0OBw==
+
+webpack@5.51.1:
+  version "5.51.1"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.51.1.tgz#41bebf38dccab9a89487b16dbe95c22e147aac57"
+  integrity sha512-xsn3lwqEKoFvqn4JQggPSRxE4dhsRcysWTqYABAZlmavcoTmwlOb9b1N36Inbt/eIispSkuHa80/FJkDTPos1A==
+  dependencies:
+    "@types/eslint-scope" "^3.7.0"
+    "@types/estree" "^0.0.50"
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/wasm-edit" "1.11.1"
+    "@webassemblyjs/wasm-parser" "1.11.1"
+    acorn "^8.4.1"
+    acorn-import-assertions "^1.7.6"
+    browserslist "^4.14.5"
+    chrome-trace-event "^1.0.2"
+    enhanced-resolve "^5.8.0"
+    es-module-lexer "^0.7.1"
+    eslint-scope "5.1.1"
+    events "^3.2.0"
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.2.4"
+    json-parse-better-errors "^1.0.2"
+    loader-runner "^4.2.0"
+    mime-types "^2.1.27"
+    neo-async "^2.6.2"
+    schema-utils "^3.1.0"
+    tapable "^2.1.1"
+    terser-webpack-plugin "^5.1.3"
+    watchpack "^2.2.0"
+    webpack-sources "^3.2.0"
+
+which@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+  dependencies:
+    isexe "^2.0.0"
+
+wildcard@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
+  integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yaml@^1.10.2:
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
+  integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+
+yocto-queue@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+  integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==