diff --git a/.gitignore b/.gitignore
index 0f2a07fa3e7d44347af04ff682a5e37827e5bfe1..7d30c4e33027e919e91ba0767556ba8790578c80 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,9 +3,6 @@
 __pycache__/
 *.py[cod]
 
-# pkglts files
-.pkglts/info.log*
-
 # Packages
 *.egg
 *.egg-info
@@ -93,20 +90,15 @@ target/
 
 # sphinx autogen file
 doc/_dvlpt/
-
+doc/tutorials/
+doc/tutorials.rst
 
 # #}
 
 # user custom filters
 *.DS_Store
 report.txt
-doc/*.ipynb
 
 *.h5
 *.xdmf
-doc/notebook.rst
-doc/README.rst
-doc/tutorials.rst
 *jitfailure-dolfin*/
-/doc/utils/
-/doc/_static/growth_scheme.png
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 67814915309c1ab25df76c07e703238e4962eee8..5676eda58b81c191abd244a546bb27b720c4465f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -4,7 +4,11 @@ stages:
   - build
   - test
   - deploy
+  - post-deploy
+  - release
 
+variables:
+  TUTORIALS_ZIP: "tutorials.zip"
 
 conda_build:
   stage: build
@@ -30,6 +34,9 @@ conda_build:
       allow_failure: true
     - if: '$CI_COMMIT_BRANCH == "master"'
       when: on_success
+    - if: $CI_COMMIT_BRANCH =~ /^hotfix/
+      when: manual
+      allow_failure: true
 
 
 coverage:
@@ -61,10 +68,18 @@ coverage:
       - coverage.xml
     when: always
     expire_in: 7 days
-  only:
-    - develop
-    - master
-    - /^release/
+  rules:
+    - if: $CI_COMMIT_BRANCH =~ /^release/
+      when: manual
+      allow_failure: true
+    - if: '$CI_COMMIT_BRANCH == "develop"'
+      when: manual
+      allow_failure: true
+    - if: '$CI_COMMIT_BRANCH == "master"'
+      when: always
+    - if: $CI_COMMIT_BRANCH =~ /^hotfix/
+      when: manual
+      allow_failure: true
 
 anaconda :
   stage: deploy
@@ -101,7 +116,7 @@ pages:
   image: continuumio/miniconda3
   script:
     # Install required 'libgl1' & 'pyvista xvfb' system dependency:
-    - apt-get update && apt-get install libgl1 libgl1-mesa-glx xvfb wget -y
+    - apt-get update && apt-get install libgl1 libgl1-mesa-glx xvfb wget zip -y
     # Download and install pandoc3.1.12.3 (version must be at least (2.14.2) but less than (4.0.0)):
     - wget https://github.com/jgm/pandoc/releases/download/3.1.12.3/pandoc-3.1.12.3-1-amd64.deb
     - dpkg -i pandoc-3.1.12.3-1-amd64.deb
@@ -115,6 +130,8 @@ pages:
     - python -m pip install -e .[doc]
     # Build the documentation with Sphinx:
     - sphinx-build -b html doc/ public/
+    - echo "Zipping the tutorials folder..."
+    - zip -r public/tutorials.zip tutorials/
   retry:
     max: 2
     when: runner_system_failure
@@ -123,7 +140,7 @@ pages:
     paths:
       - public
     exclude:
-      - public/*.ipynb
+      - public/**/*.ipynb
   dependencies: []
   rules:
     - if: $CI_COMMIT_BRANCH =~ /^release/
@@ -134,6 +151,9 @@ pages:
       allow_failure: true
     - if: '$CI_COMMIT_BRANCH == "master"'
       when: on_success
+    - if: $CI_COMMIT_BRANCH =~ /^hotfix/
+      when: manual
+      allow_failure: true
 
 docker_build_deploy:
   image: docker:latest
@@ -154,4 +174,4 @@ docker_build_deploy:
     when: runner_system_failure
   dependencies: []
   only:
-    - tags
+    - tags
\ No newline at end of file
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 55cf4cab5901be7f642a44f09d8c3163f7eeab6f..454cdc1d09e369a3d951aa3bb6aeb745ec4eb0a4 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -5,17 +5,21 @@ Credits
 Development Lead
 ----------------
 
-.. {# pkglts, doc.authors
+* Manuel Petit, <manuel.petit@inria.fr>
 
-* Florian Gacon, <florian.gacon@inria.fr>
-
-.. #}
 
-Contributors
+Main Contributors
 ------------
 
-.. {# pkglts, doc.contributors
-
 * ALI Olivier <olivier.ali@inria.fr>
+* Florian Gacon, <florian.gacon@inria.fr>
+
+
+Other Contributors
+------------------
+
+* Jonathan Legrand
+* Guillaume Cerutti
+* Adrien Heymans
+* Gonzalo Revilla
 
-.. #}
diff --git a/HISTORY.rst b/HISTORY.rst
index 45e1ef0001ca24d4d830e9114bcdbea7ddd25a8d..8c2da189233ddbcc8de0db2b073879e387282708 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -7,6 +7,19 @@ creation (2020-08-26)
 
 * First release on PyPI.
 
+version 1.1.0 (2024-03-25)
+--------------------------
+
+### **New Features**
+
+1. Added **PyVista** module `visu_pysta` for visualization.
+
+2. Improved handling of **3D geometry (tetra)** in `domains`.
+
+3. Improved access to solver parameters for better usability and flexibility:
+
+    - List all the parameters that can be access in `parameter.py`
+
 version 1.3.0 (2025-01-11)
 --------------------------
 
diff --git a/LICENSE b/LICENSE
index 650a3f2c7442de4804864065ca70f3d0b8951a2e..57e7a5350ad168208e8b3d6e5c6b9864fb115a95 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,3 @@
-{# pkglts, license
 GNU LESSER GENERAL PUBLIC LICENSE
 Version 3, 29 June 2007
 
@@ -153,6 +152,4 @@ Public License ever published by the Free Software Foundation.
 If the Library as you received it specifies that a proxy can decide whether
 future versions of the GNU Lesser General Public License shall apply, that
 proxy's public statement of acceptance of any version is permanent
-authorization for you to choose that version for the Library.
-
-#}
+authorization for you to choose that version for the Library.
\ No newline at end of file
diff --git a/MANIFEST.in b/MANIFEST.in
index bfa8d262e4adabe18094820e1f5b511dc1eb9730..62ca18c91f5dab8cbafff464f7948ca025a39177 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,27 +1,19 @@
-# {# pkglts, pysetup
+# Include important documentation files
 include AUTHORS.rst
 include CONTRIBUTING.rst
 include HISTORY.rst
 include README.rst
-
 include LICENSE
 
-include requirements.txt
-include requirements_minimal.txt
-
-
+# Include all test-related files for validating the library
 recursive-include test *
 
-
-
-recursive-exclude * __pycache__
-recursive-exclude * *.py[co]
-
-
+# Include all documentation files (e.g., Sphinx files and additional docs)
 recursive-include doc *.rst
 include doc/conf.py
 include doc/Makefile
 include doc/make.bat
 
-
-# #}
+# Exclude unnecessary files and directories
+recursive-exclude * __pycache__
+recursive-exclude * *.py[co]
diff --git a/README.rst b/README.rst
index 219c3e0868e98e91592897c94551a0064a6b25bd..cd32a0ddbf09ff911c6f33133eaa72ca54897da3 100644
--- a/README.rst
+++ b/README.rst
@@ -57,67 +57,60 @@ Requirements
 Installation
 ------------
 
-You will need conda in order to install ``bvpy``, you can download it  `here <https://docs.conda.io/en/latest/miniconda.html>`_.
+The full installation procedure is available `here <https://mosaic.gitlabpages.inria.fr/bvpy/>`_.
 
-- From sources:
+To install ``bvpy``, follow these steps:
 
-.. code-block:: bash
+1. **Using Anaconda** (recommended for most users):
 
-  git clone https://gitlab.inria.fr/mosaic/bvpy.git
-  cd bvpy
-  conda env create --file conda/env.yaml -n bvpy-dev
-  conda activate bvpy-dev
-  python -m pip install -e .
+   - Install with a new environment:
 
-- From anaconda:
+     .. code-block:: bash
 
-.. code-block:: bash
+        conda create -n bvpy -c mosaic -c conda-forge bvpy
+        conda activate bvpy
 
-	conda install -c mosaic bvpy
+   - Or install in an existing environment:
 
+     .. code-block:: bash
 
-**Important Note:** Bvpy is currently running on *FEniCS legacy* and not *FEniCSx*. If you are using an ARM-based computer (mac M1s, M2s), there is currently no ARM-Friendly conda package of *FEniCS legacy*. But you can still run x86-based packages. to do so type the following:
+        conda activate <your-environment-name>
+        conda install -c mosaic -c conda-forge bvpy
 
-.. code-block:: bash
-  
-  CONDA_SUBDIR=osx-64 conda create -n bvpy_x86 
-  conda activate bvpy_x86
-  conda config --env --set subdir osx-64
+2. **Using Docker**:
 
-Once this is done, you can import manually all the required libraries, listed within the `conda/env_osx.yaml` configuration file.
+   - Pull the Docker image:
 
+     .. code-block:: bash
 
-- From docker
+        docker pull registry.gitlab.inria.fr/mosaic/bvpy:<TAG>
 
-.. code-block:: bash
+   - Run the container and launch Jupyter Notebook:
 
-	docker pull registry.gitlab.inria.fr/mosaic/bvpy:<TAG>
+     .. code-block:: bash
 
-Where <TAG> is the version of bvpy you want. You can find all the available version on the `repository <https://gitlab.inria.fr/mosaic/bvpy/container_registry/>`_.
+        docker run -it -p 8888:8888 registry.gitlab.inria.fr/mosaic/bvpy:<TAG>
+        jupyter notebook --ip 0.0.0.0 --no-browser --allow-root --port 8888
 
-To run the docker container and launch jupyter use:
+3. **From Sources**:
 
-.. code-block:: bash
+   - Clone the repository:
 
-  docker run -it -p 8888:8888 registry.gitlab.inria.fr/mosaic/bvpy:<TAG>
-  jupyter notebook --ip 0.0.0.0 --no-browser --allow-root --port 8888
+     .. code-block:: bash
 
-And use the URL on the terminal to open the tutorials.
+        git clone https://gitlab.inria.fr/mosaic/bvpy.git
+        cd bvpy
+        conda env create --file conda/env.yaml -n bvpy-dev
+        conda activate bvpy-dev
+        python -m pip install -e .
 
-Test
-----
-
-If you install ``bvpy`` from sources install ``pytest`` and ``pytest-cov`` inside your environment:
-
-.. code-block:: bash
-
-	python -m pip install -e ".[test]"
-
-And then run at the root of the project:
-
-.. code-block:: bash
-
-	pytest -v --cov=bvpy test/
+.. The following content is commented out : no need
+    **Important Note:** Bvpy is currently running on *FEniCS legacy* and not *FEniCSx*. If you are using an ARM-based computer (mac M1s, M2s), there is currently no ARM-Friendly conda package of *FEniCS legacy*. But you can still run x86-based packages. to do so type the following:
+    .. code-block:: bash
+      CONDA_SUBDIR=osx-64 conda create -n bvpy_x86
+      conda activate bvpy_x86
+      conda config --env --set subdir osx-64
+    Once this is done, you can import manually all the required libraries, listed within the `conda/env_osx.yaml` configuration file.
 
 Support
 -------
diff --git a/conda/env_20240207.yaml b/conda/env_20240207.yaml
deleted file mode 100644
index 554b135747cd06666e6386da53bd0c805f16f288..0000000000000000000000000000000000000000
--- a/conda/env_20240207.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-name: bvpy
-channels:
-  - conda-forge
-  - defaults
-dependencies:
-  - python >=3.9,<4.0
-  - ipython
-  - numpy
-  - pandas
-  - matplotlib
-  - python-gmsh=4.11
-  - meshio
-  - fenics=2019.1
-  - importlib_metadata
-  - notebook
-  - vtk
-  - plotly
-  - pyvista
-  - trame
-  - ipywidgets
-  - pytest
-  - pytest-cov
-  - sphinx
-  - sphinx_rtd_theme
-  - make
-  - nbsphinx
-  - recommonmark
-  - typing_extensions
\ No newline at end of file
diff --git a/conda/meta.yaml b/conda/meta.yaml
index f9c5622a646f687a35748ea573a3a17d0d721f3b..709a8186ae35ca240e8df32376099ae909ce5c4a 100644
--- a/conda/meta.yaml
+++ b/conda/meta.yaml
@@ -1,6 +1,6 @@
 {% set pyproject = load_file_data('../pyproject.toml', 'toml', from_recipe_dir=True) %}
 {% set project = pyproject.get('project', {}) %}
-{% set deps = project.get('dependencies', {}) %}
+{% set deps = project.get('dependencies', []) %}
 {% set urls = project.get('urls', {}) %}
 # https://docs.conda.io/projects/conda-build/en/latest/resources/define-metadata.html#loading-data-from-other-files
 # Source code for Jinja context: https://github.com/conda/conda-build/blob/main/conda_build/jinja_context.py
@@ -25,6 +25,7 @@ requirements:
     - python  {{ python }}
     - python-gmsh=4.11
     - fenics=2019.1.0
+    - h5py
     {% for dep in deps %}
     - {{ dep }}
     {% endfor %}
diff --git a/data/tutorial_results/tuto_6_result_4_paraview_snapshot.png b/data/tutorial_results/tuto_6_result_4_paraview_snapshot.png
deleted file mode 100644
index 1edb12898f9a16e45468f0cad925743c81187d7d..0000000000000000000000000000000000000000
Binary files a/data/tutorial_results/tuto_6_result_4_paraview_snapshot.png and /dev/null differ
diff --git a/doc/_static/nonempty.txt b/doc/_static/nonempty.txt
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/doc/conf.py b/doc/conf.py
index f4ae60995a112a302f8cae28a707bc84d13fce2f..eaa931499c0563a2707aa1ae4b580d6776e40ed1 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -1,4 +1,3 @@
-# {# pkglts, sphinx
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 #
@@ -17,98 +16,104 @@
 import os
 import sys
 import shutil
+from sphinx.application import Sphinx
 
 sys.path.insert(0, os.path.abspath('.')) # add doc/ to the path
-from utils.convert_notebook import modify_notebook_for_sphinx
-
-def setup(app):
-    app.add_css_file('css/custom.css')
-
-# -------------------- Copy images --------------------
-
-source_dir = '../tutorials/images'
-target_dir = './_static'
-
-os.makedirs(target_dir, exist_ok=True)
-
-for filename in os.listdir(source_dir):
-    if filename.endswith('.png'):
-        source_file = os.path.join(source_dir, filename)
-        target_file = os.path.join(target_dir, filename)
-
-        # Remove the target file if it exists
-        if os.path.exists(target_file):
-            os.remove(target_file)
-
-        # Copy the source file to the target location
-        shutil.copyfile(source_file, target_file)
-        print(f'Copied {source_file} to {target_file}')
-
-# -------------------- Import Notebooks --------------------
-
-for file in os.listdir('.'):
-    if '.ipynb' in file:
-        os.remove(file)
-    if file == 'tutorials.rst':
-        os.remove(file)
-
-notebooks = os.listdir('../tutorials')
-
-to_import = ['bvpy_tutorial_1_hello_world.ipynb',
-             'bvpy_tutorial_2_domains.ipynb',
-             'bvpy_tutorial_3_vforms.ipynb',
-             'bvpy_tutorial_4_bnd_cond.ipynb',
-             'bvpy_tutorial_5_reaction_diffusion.ipynb',
-             'bvpy_tutorial_6_linear_elasticity.ipynb',
-             'bvpy_tutorial_7_hyper_elasticity.ipynb',
-             'bvpy_tutorial_8_pyvista.ipynb',
-             'bvpy_tutorial_9_gmsh.ipynb',
-             'bvpy_tutorial_10_morphoelasticity.ipynb']
-
-# - Copy & update notebook if needed (pyvista is used ?)
-for file in notebooks:
-    if file in to_import:
-        print(f'Importing file: {file}')
-        shutil.copy(f'../tutorials/{file}', file)
-        modify_notebook_for_sphinx(file, inplace=True, dynamic_plot=False) # dynamic_plot should be set to True when
-                                                                           # vtk.js have been updated
-
-with open("tutorials.rst", "w+") as f:
-    f.write(".. _tutorials:\n\n")
-    f.write("Tutorials\n")
-    f.write("*********\n\n")
-    f.write(".. toctree::\n")
-    f.write("\t:titlesonly:\n")
-    f.write("\t:maxdepth: 1\n\n")
-    for i in to_import:
-        f.write("\t"+i.replace(".ipynb", "")+"\n")
-
-
-# If extensions (or modules to document with autodoc) are in another
-# directory, add these directories to sys.path here. If the directory is
-# relative to the documentation root, use os.path.abspath to make it
-# absolute, like shown here.
-# sys.path.insert(0, os.path.abspath('.'))
-
-# Get the project root dir, which is the parent dir of this
-cwd = os.getcwd()
-project_root = os.path.dirname(cwd)
-src_dir = os.path.abspath(os.path.join(project_root, "src"))
-
-# Insert the project root dir as the first element in the PYTHONPATH.
-# This lets us ensure that the source package is imported, and that its
-# version is used.
-sys.path.insert(0, os.path.join(project_root, 'src'))
+#sys.path.insert(0, os.path.abspath('./tutorials'))  # Add the `tutorials` directory
+
+from utils.convert_notebook import process_notebook, clean_and_reset_notebook
+
+# ---------------------- Process Tutorials ---------------------- #
+source_tutorials_dir = '../tutorials'
+tutorial_output_dir = './tutorials'
+tutorial_rst_path = "tutorials.rst"
+
+# List of tutorial notebooks to process
+notebooks = [
+    'bvpy_tutorial_1_hello_world.ipynb',
+    'bvpy_tutorial_2_domains.ipynb',
+    'bvpy_tutorial_3_vforms.ipynb',
+    'bvpy_tutorial_4_bnd_cond.ipynb',
+    'bvpy_tutorial_5_reaction_diffusion.ipynb',
+    'bvpy_tutorial_6_linear_elasticity.ipynb',
+    'bvpy_tutorial_7_hyper_elasticity.ipynb',
+    'bvpy_tutorial_8_pyvista.ipynb',
+    'bvpy_tutorial_9_gmsh.ipynb',
+    'bvpy_tutorial_10_morphoelasticity.ipynb',
+]
 
-# import bvpy as mypkg
+# Ensure the tutorials directory exists
+os.makedirs(tutorial_output_dir, exist_ok=True)
+
+# Copy supporting folders (images, tutorial_data)
+folders_to_copy = ['images', 'tutorial_data']
+for folder_name in folders_to_copy:
+    source_folder = os.path.join(source_tutorials_dir, folder_name)
+    target_folder = os.path.join(tutorial_output_dir, folder_name)
+    if os.path.exists(source_folder):
+        shutil.rmtree(target_folder, ignore_errors=True)  # Clear old data
+        shutil.copytree(source_folder, target_folder)
+        print(f"Copied {source_folder} -> {target_folder}")
+    else:
+        print(f"Source folder {source_folder} not found. Skipping...")
+
+# Remove the old tutorials.rst file
+if os.path.exists(tutorial_rst_path):
+    print(f"Removing old {tutorial_rst_path}")
+    os.remove(tutorial_rst_path)
+
+# Generate tutorials.rst with a toctree
+tutorial_rst_lines = [
+    ".. _tutorials:\n\n",
+    "Tutorials\n*********\n\n",
+    "Download the tutorial notebooks and accompanying data as a ZIP file: "
+    "`tutorials.zip <https://mosaic.gitlabpages.inria.fr/bvpy/tutorials.zip>`_\n\n\n",
+    ".. toctree::\n",
+    "    :titlesonly:\n",
+    "    :maxdepth: 1\n\n",
+]
 
-# -- General configuration ---------------------------------------------
+# Process each notebook
+for notebook in notebooks:
+    input_path = os.path.join(source_tutorials_dir, notebook)
+    output_path = os.path.join(tutorial_output_dir, notebook)
+    process_notebook(input_notebook_path=input_path, output_notebook_path=output_path, dynamic_plot=False)
+    tutorial_rst_lines.append(f"    tutorials/{notebook.replace('.ipynb', '')}\n")
+
+# Write the tutorials.rst file
+with open(tutorial_rst_path, "w", encoding="utf-8") as f:
+    f.writelines(tutorial_rst_lines)
+print(f"Wrote {tutorial_rst_path}")
+
+# -------------------- Clean Tutorials After Build -------------------- #
+def clean_doc(app: Sphinx, exception):
+    """
+    Remove temporary folders created during the build process.
+    Cleans up the tutorials folder and the _dvlpt folder after the build is complete.
+    """
+    # clean the tutorials/ folder in order to zip its content in CI/CD
+    if os.path.exists(tutorial_output_dir):
+        print(f"Cleaning up notebooks in {tutorial_output_dir}")
+        for notebook in notebooks:
+            notebook_path = os.path.join(tutorial_output_dir, notebook)
+            clean_and_reset_notebook(input_notebook_path=notebook_path, output_notebook_path=notebook_path)
+
+    # Clean up the _dvlpt folder
+    dev_folder = os.path.abspath(os.path.join(project_root, "doc", "_dvlpt"))
+    if os.path.exists(dev_folder):
+        print(f"Cleaning up {dev_folder}")
+        shutil.rmtree(dev_folder, ignore_errors=True)
+
+# ---------------------- Sphinx Setup ---------------------- #
+def setup(app):
+    """Hook custom setup functionality into Sphinx."""
+    app.add_css_file('css/custom.css')  # Add custom CSS
+    app.connect('build-finished', clean_doc)
 
-# If your documentation needs a minimal Sphinx version, state it here.
-# needs_sphinx = '1.0'
+# ------------------- General Configuration ------------------- #
+project_root = os.path.dirname(os.getcwd())
+sys.path.insert(0, os.path.join(project_root, 'src'))  # Add src/ to the path
 
-# Add any Sphinx extension module names here, as strings. They can be
-# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
 extensions = [
     'sphinx.ext.autodoc',
     'sphinx.ext.autosummary',
@@ -121,11 +126,12 @@ extensions = [
     'sphinx.ext.napoleon',
     'sphinx.ext.todo',
     'sphinx.ext.viewcode',
+    'sphinx_tabs.tabs',
     'nbsphinx',
     'recommonmark',
     'pyvista.ext.plot_directive',
     'pyvista.ext.viewer_directive',
-    'sphinx_design'
+    'sphinx_design',
 ]
 
 # Enable plotly figures and add custom CSS for hiding cells with `remove_cell` tags
@@ -134,263 +140,63 @@ nbsphinx_prolog = r"""
 
     <script src='http://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.10/require.min.js'></script>
     <script>require=requirejs;</script>
-
 """
-nbsphinx_timeout = -1
 
-# try to add more extensions which are not default
-# but still useful
-# based on the fact that the extension is installed on the system
-
-#try:
-#    import matplotlib.sphinxext.plot_directive
-#    extensions.append('matplotlib.sphinxext.plot_directive')
-#except ImportError:
-#    pass
-
-
-
-# default settings that can be redefined outside of the pkglts block
+nbsphinx_timeout = -1
 todo_include_todos = True
 autosummary_generate = True
-intersphinx_mapping = {'python': ('https://docs.python.org/3.4', None),
-                    #    'dolfin': ('https://fenicsproject.org/docs/dolfin/latest/python/', None),
-                       'dolfin': ('https://fenicsproject.org/olddocs/dolfin/2019.1.0/python/', None),
-                       'ufl': ('https://fenics.readthedocs.io/projects/ufl/en/latest/', None)}
-inheritance_node_attrs = dict(shape='ellipse', fontsize=12,
-                              color='orange', style='filled')
+intersphinx_mapping = {
+    'python': ('https://docs.python.org/3.4', None),
+    'dolfin': ('https://fenicsproject.org/olddocs/dolfin/2019.1.0/python/', None),
+    'ufl': ('https://fenics.readthedocs.io/projects/ufl/en/latest/', None),
+}
+inheritance_node_attrs = dict(shape='ellipse', fontsize=12, color='orange', style='filled')
 
-# Add any paths that contain templates here, relative to this directory.
+# Templates, source files, and static paths
 templates_path = ['_templates']
-
-# The suffix of source filenames.
 source_suffix = '.rst'
-
-# The encoding of source files.
-# source_encoding = 'utf-8-sig'
-
-# The master toctree document.
 master_doc = 'index'
+exclude_patterns = ['build', 'dist', '**.ipynb_checkpoints']
 
-# General information about the project.
-project = u"bvpy"
-copyright = u"2020, bvpy"
-
-# The version info for the project you're documenting, acts as replacement
-# for |version| and |release|, also used in various other places throughout
-# the built documents.
-#
-
-# The short X.Y version.
+# General information
+project = "bvpy"
+copyright = "2020, bvpy"
 version = "1.3.0"
-# The full version, including alpha/beta/rc tags.
 release = "1.3.0"
 
-
-# The language for content autogenerated by Sphinx. Refer to documentation
-# for a list of supported languages.
-# language = None
-
-# There are two options for replacing |today|: either, you set today to
-# some non-false value, then it is used:
-# today = ''
-# Else, today_fmt is used as the format for a strftime call.
-# today_fmt = '%B %d, %Y'
-
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-exclude_patterns = ['build', 'dist', '**.ipynb_checkpoints']
-
-# The reST default role (used for this markup: `text`) to use for all
-# documents.
-# default_role = None
-
-# If true, '()' will be appended to :func: etc. cross-reference text.
-# add_function_parentheses = True
-
-# If true, the current module name will be prepended to all description
-# unit titles (such as .. function::).
-# add_module_names = True
-
-# If true, sectionauthor and moduleauthor directives will be shown in the
-# output. They are ignored by default.
-# show_authors = False
-
-# The name of the Pygments (syntax highlighting) style to use.
+# Syntax highlighting style
 pygments_style = 'sphinx'
 
-# A list of ignored prefixes for module index sorting.
-# modindex_common_prefix = []
-
-# If true, keep warnings as "system message" paragraphs in the built
-# documents.
-# keep_warnings = False
-
-
-# -- Options for HTML output -------------------------------------------
-
-# The theme to use for HTML and HTML Help pages.  See the documentation for
-# a list of builtin themes.
+# ------------------- HTML Output ------------------- #
 html_theme = "sphinx_rtd_theme"
-
-# Theme options are theme-specific and customize the look and feel of a
-# theme further.  For a list of options available for each theme, see the
-# documentation.
-# html_theme_options = {}
-
-# Add any paths that contain custom themes here, relative to this directory.
-# html_theme_path = []
-
-# The name for this set of Sphinx documents.  If None, it defaults to
-# "<project> v<release> documentation".
-# html_title = None
-
-# A shorter title for the navigation bar.  Default is the same as
-# html_title.
-# html_short_title = None
-
-# The name of an image file (relative to this directory) to place at the
-# top of the sidebar.
 html_logo = "_static/logo_side.png"
-
-# The name of an image file (within the static path) to use as favicon
-# of the docs.  This file should be a Windows icon file (.ico) being
-# 16x16 or 32x32 pixels large.
 html_favicon = "_static/favicon.ico"
-
-# Add any paths that contain custom static files (such as style sheets)
-# here, relative to this directory. They are copied after the builtin
-# static files, so a file named "default.css" will overwrite the builtin
-# "default.css".
 html_static_path = ['_static']
-
-# If not '', a 'Last updated on:' timestamp is inserted at every page
-# bottom, using the given strftime format.
-# html_last_updated_fmt = '%b %d, %Y'
-
-# If true, SmartyPants will be used to convert quotes and dashes to
-# typographically correct entities.
-# html_use_smartypants = True
-
-# Custom sidebar templates, maps document names to template names.
-# html_sidebars = {}
-
-# Additional templates that should be rendered to pages, maps page names
-# to template names.
-# html_additional_pages = {}
-
-# If false, no module index is generated.
-# html_domain_indices = True
-
-# If false, no index is generated.
-# html_use_index = True
-
-# If true, the index is split into individual pages for each letter.
-# html_split_index = False
-
-# If true, links to the reST sources are added to the pages.
-# html_show_sourcelink = True
-
-# If true, "Created using Sphinx" is shown in the HTML footer.
-# Default is True.
-# html_show_sphinx = True
-
-# If true, "(C) Copyright ..." is shown in the HTML footer.
-# Default is True.
-# html_show_copyright = True
-
-# If true, an OpenSearch description file will be output, and all pages
-# will contain a <link> tag referring to it.  The value of this option
-# must be the base URL from which the finished HTML is served.
-# html_use_opensearch = ''
-
-# This is the file name suffix for HTML files (e.g. ".xhtml").
-# html_file_suffix = None
-
-# Output file base name for HTML help builder.
 htmlhelp_basename = "bvpydoc"
 
-
-# -- Options for LaTeX output ------------------------------------------
-
+# ------------------- LaTeX Output ------------------- #
 latex_elements = {
     'preamble': r'''
-        \usepackage{amsmath}  % For math expressions
-        \usepackage{amssymb}  % For symbols like \nabla
+        \usepackage{amsmath}
+        \usepackage{amssymb}
     ''',
 }
-
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title, author, documentclass
-# [howto/manual]).
-latex_documents = [
-    (u"index", u"bvpy.tex",
-     u"bvpy Documentation",
-     u"moi", u"manual"),
-]
-
-# The name of an image file (relative to this directory) to place at
-# the top of the title page.
-# latex_logo = None
-
-# For "manual" documents, if this is true, then toplevel headings
-# are parts, not chapters.
-# latex_use_parts = False
-
-# If true, show page references after internal links.
-# latex_show_pagerefs = False
-
-# If true, show URL addresses after external links.
-# latex_show_urls = False
-
-# Documents to append as an appendix to all manuals.
-# latex_appendices = []
-
-# If false, no module index is generated.
-# latex_domain_indices = True
-
-
-# -- Options for manual page output ------------------------------------
-
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
-man_pages = [
-    (u"index", u"bvpy",
-     u"bvpy Documentation",
-     [u"moi"], 1)
+latex_documents = [
+    ('index', 'bvpy', 'bvpy Documentation', ['moi'], 1),
 ]
 
-# If true, show URL addresses after external links.
-# man_show_urls = False
-
-
-# -- Options for Texinfo output ----------------------------------------
-
+# ------------------- Texinfo Output ------------------- #
 # Grouping the document tree into Texinfo files. List of tuples
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-    (u"index", u"bvpy",
-     u"bvpy Documentation",
-     u"moi",
-     u"bvpy",
-     u"A package implementing Boundary Value Problem.",
-     u"Miscellaneous"),
+    ('index', 'bvpy', 'bvpy Documentation', 'moi', 'bvpy', 'A package implementing Boundary Value Problem.',
+     'Miscellaneous'),
 ]
 
-# Documents to append as an appendix to all manuals.
-# texinfo_appendices = []
-
-# If false, no module index is generated.
-# texinfo_domain_indices = True
-
-# How to display URL addresses: 'footnote', 'no', or 'inline'.
-# texinfo_show_urls = 'footnote'
-
-# If true, do not generate a @detailmenu in the "Top" node's menu.
-# texinfo_no_detailmenu = False
-
-# use apidoc to generate developer doc
+# ------------------- Apidoc generation ------------------- #
 try:
     from sphinx.ext.apidoc import main
 except ImportError:
@@ -402,6 +208,5 @@ destdir = os.path.abspath(os.path.join(project_root, "doc", "_dvlpt"))
 if not os.path.isdir(destdir):
     os.makedirs(destdir)
 
+src_dir = os.path.abspath(os.path.join(project_root, "src"))
 main(['-e', '-o', destdir, '-d', '4', '-s', source_suffix[1:], '--force', src_dir])
-
-# #}
diff --git a/doc/index.rst b/doc/index.rst
index cf290da8c46c8cb15672ce0c0d0bd74041352635..cbc505cef6520efb976976dac8b6662554645cb8 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -31,4 +31,213 @@
    Sources <_dvlpt/modules>
 
 
-.. include:: ../README.rst
+Welcome
+-------
+
+.. image:: https://joss.theoj.org/papers/10.21105/joss.02831/status.svg
+   :target: https://doi.org/10.21105/joss.02831
+
+**Bvpy** is a python library, based on `FEniCS <https://fenicsproject.org/>`_ and `GMSH <http://gmsh.info/>`_, to easily implement and study numerically Boundary Value Problems (BVPs) as well as Initial Boundary Value Problems (IBVPs) through the Finite Element Method (FEM).
+
+Initially built up in the context of developmental biology and morphomechanics, **Bvpy** proposes an intuitive API as close as possible to the formal definition of BVPs. Its purpose is to enable users to quickly and efficiently estimate the behavior of a wide variety of fields (scalars, vectors, tensors) on biologically relevant structures (Riemannian and non-Rieamannian manifolds), inspired by biophysical and biochemical processes (morphogene patterning, active matter mechanics, diffusion-reaction processes, active transports...).
+
+Despite this biological motivation, the **Bvpy** library has been implemented in an *agnostic* manner that makes it suitable for many other scientific context.
+
+
+.. sidebar:: **Bvpy**
+
+  :Lead Development: Manuel Petit
+
+  :Coordination: Olivier Ali
+
+  :Main Contributors: Florian Gacon
+
+  :Other Contributors: Guillaume Cerutti, Adrien Heymans, Jonathan Legrand, Gonzalo Revilla
+
+  :Active team: Inria project team `Mosaic <https://team.inria.fr/mosaic/>`_
+
+  :Institutes: `Inria <http://www.inria.fr>`_
+
+  :Language: Python
+
+  :Supported OS: Linux, MacOS
+
+  :Licence: `LGPL`
+
+  :Funding: Inria ADT Gnomon (Christophe Godin)
+
+
+Documentation
+-------------
+
+A quick introduction to boundary-value problems and Initial-Boundary-value problems, as well as a the general philosophy behind the **Bvpy** library development can be found `here <https://mosaic.gitlabpages.inria.fr/bvpy/general_introduction.html>`_.
+
+A detailed description of the main classes and models of the library can be found `here <https://mosaic.gitlabpages.inria.fr/bvpy/library_description.html>`_.
+
+Some tutorials explaining basic manipulations are gathered `here <https://mosaic.gitlabpages.inria.fr/bvpy/tutorials.html>`_.
+
+
+Requirements
+------------
+
+* Python 3.9
+* FEniCS 2019.1.0
+* GMSH 4.11
+
+
+Installation
+------------
+
+You will need conda in order to install ``bvpy``. If you don’t have conda installed, you can download it  `here <https://docs.conda.io/en/latest/miniconda.html>`_.
+
+.. tabs::
+
+   .. tab:: Installing from Anaconda
+
+    ``bvpy`` can be installed directly from the `Anaconda` distribution using the **conda package manager**. You have the flexibility to either create a new environment or install it into an existing one.
+
+    1. Open a terminal or Conda environment prompt.
+
+    2. Choose one of the following installation methods:
+
+       - **Create a New Environment** (recommended):
+
+         .. code-block:: bash
+
+            conda create -n bvpy -c mosaic -c conda-forge bvpy
+            conda activate bvpy
+
+       - **Install into an Existing Environment**:
+
+         .. code-block:: bash
+
+            conda activate <your-environment-name>
+            conda install -c mosaic -c conda-forge bvpy
+
+    3. Verify the ``bvpy`` installation:
+
+       .. code-block:: bash
+
+          python -c "from bvpy.utils.examples import cantilever_beam; cantilever_beam()"
+
+       This should produce a Pyvista Plotter showing the displacement field of the cantilever beam. If you see the correctly rendered visualization, the installation and dependencies of ``bvpy`` are working as expected.
+
+    **Note**: Replace `<your-environment-name>` with the name of your existing environment.
+
+   .. tab:: Installing from Docker
+
+      To install ``bvpy`` using Docker, follow these steps:
+
+      1. Pull the Docker image:
+
+         .. code-block:: bash
+
+            docker pull registry.gitlab.inria.fr/mosaic/bvpy:<TAG>
+
+         Replace `<TAG>` with the desired version of ``bvpy``.
+         You can find all available versions on the `repository <https://gitlab.inria.fr/mosaic/bvpy/container_registry/>`_.
+
+      2. Run the Docker container and launch Jupyter Notebook:
+
+         .. code-block:: bash
+
+            docker run -it -p 8888:8888 registry.gitlab.inria.fr/mosaic/bvpy:<TAG>
+            jupyter notebook --ip 0.0.0.0 --no-browser --allow-root --port 8888
+
+         Use the URL displayed in the terminal to open the Jupyter Notebook in your browser for running tutorials.
+
+   .. tab:: Installing from Sources
+
+      If you want to install ``bvpy`` from its source code (e.g., for development purposes or using the latest version), use the following steps:
+
+      1. Clone the Repository:
+
+      .. code-block:: bash
+
+         git clone https://gitlab.inria.fr/mosaic/bvpy.git
+         cd bvpy
+
+      2. Create a Development Environment:
+
+      Use the provided `conda` environment file to create a new development environment:
+
+      .. code-block:: bash
+
+         conda env create --file conda/env.yaml -n bvpy-dev
+
+      After the environment is created, activate it:
+
+      .. code-block:: bash
+
+         conda activate bvpy-dev
+
+      3. Install ``bvpy`` in Editable Mode:
+
+      Install the repository in editable mode (using pip) to allow changes to the source code to take effect immediately:
+
+      .. code-block:: bash
+
+         python -m pip install -e .
+
+      4. Verify the ``bvpy`` installation:
+
+         .. code-block:: bash
+
+            python -c "from bvpy.utils.examples import cantilever_beam; cantilever_beam()"
+
+      This should produce a Pyvista Plotter showing the displacement field of the cantilever beam. If you see
+      the correctly rendered visualization, the installation and dependencies of ``bvpy`` are working as expected.
+
+      **Optional: Run Tests**
+
+        If you installed ``bvpy`` from sources, install ``pytest`` and ``pytest-cov`` inside your environment:
+
+        .. code-block:: bash
+
+           python -m pip install -e ".[test]"
+
+        Then, run the tests from the root of the project directory to ensure that everything works correctly:
+
+        .. code-block:: bash
+
+           pytest -v --cov=bvpy test/
+
+
+.. The following content is commented out : no need
+    **Important Note:** Bvpy is currently running on *FEniCS legacy* and not *FEniCSx*. If you are using an ARM-based computer (mac M1s, M2s), there is currently no ARM-Friendly conda package of *FEniCS legacy*. But you can still run x86-based packages. to do so type the following:
+    .. code-block:: bash
+      CONDA_SUBDIR=osx-64 conda create -n bvpy_x86
+      conda activate bvpy_x86
+      conda config --env --set subdir osx-64
+    Once this is done, you can import manually all the required libraries, listed within the `conda/env_osx.yaml` configuration file.
+
+Support
+-------
+
+If you encounter an error or need help, please `raise an issue <https://gitlab.com/oali/bvpy/-/issues>`_.
+
+Contributing
+------------
+
+Bvpy is a is a collaborative project and contributions are welcome. If you want to contribute, please contact the `coordinator <mailto:olivier.ali@inria.fr>`_ prior to any `merge request <https://gitlab.com/oali/bvpy/-/merge_requests>`_ and
+check the `gitlab merge request guidelines <https://docs.gitlab.com/ee/development/contributing/merge_request_workflow.html>`_ if needed.
+
+Citation
+--------
+
+If you are using Bvpy in a published work, please use this bibtex entry as reference:
+
+.. code-block::
+
+  @article{Gacon2021,
+  doi = {10.21105/joss.02831},
+  url = {https://doi.org/10.21105/joss.02831},
+  year = {2021},
+  publisher = {The Open Journal},
+  volume = {6},
+  number = {59},
+  pages = {2831},
+  author = {Florian Gacon and Christophe Godin and Olivier Ali},
+  title = {BVPy: A FEniCS-based Python package to ease the expression and study of boundary value problems in Biology.},
+  journal = {Journal of Open Source Software}}
+
diff --git a/doc/utils/convert_notebook.py b/doc/utils/convert_notebook.py
index b9680bc70c6fd609e19805cf438071636edd01fa..bf256f226f5b216a005488aa16b7ed8f21092280 100644
--- a/doc/utils/convert_notebook.py
+++ b/doc/utils/convert_notebook.py
@@ -1,8 +1,142 @@
 import nbformat
 import re
+import os
+from nbconvert import HTMLExporter
+from nbconvert.preprocessors import ExecutePreprocessor, ClearOutputPreprocessor
 
-def modify_notebook_for_sphinx(input_path, output_path=None, inplace=True, dynamic_plot=False):
-    """ Change the backend if pyvista is imported and update image paths.
+def process_notebook(input_notebook_path, output_notebook_path, dynamic_plot=False):
+    """ Process a Jupyter notebook: Clear outputs, modify for PyVista and write to another path.
+
+    This function orchestrates the following:
+      1. Loads the notebook from the specified path.
+      2. Clears all outputs.
+      3. Modifies the notebook to support PyVista and Sphinx documentation.
+      4. Write the notebook to the specified path
+
+    Parameters
+    ----------
+    input_notebook_path : str
+        Path to the input Jupyter notebook file (.ipynb).
+    output_notebook_path : str
+        Path to write the Jupyter notebook file (.ipynb).
+    dynamic_plot : bool, optional
+        Whether to enable PyVista dynamic plot mode, by default False.
+
+    Raises
+    ------
+    FileNotFoundError
+        If the notebook specified by `input_notebook_path` does not exist.
+    Exception
+        If any issues arise during the notebook clearing or modification
+
+    Notes
+    -----
+
+    - This function is part of a workflow for preparing Jupyter notebooks for HTML-based documentation.
+    - By enabling `dynamic_plot`, PyVista's interactive plot mode is incorporated into the notebook.
+    """
+    try:
+        # Step 1: Load the notebook from the specified path
+        if not os.path.exists(input_notebook_path):
+            raise FileNotFoundError(f"Notebook not found: {input_notebook_path}")
+
+        with open(input_notebook_path, 'r', encoding='utf-8') as f:
+            notebook_content = nbformat.read(f, as_version=4)
+        print(f"Loaded notebook: {input_notebook_path}")
+
+        # Step 2: Clear outputs
+        cleared_notebook = clear_notebook(notebook_content)
+
+        # Step 3: Modify notebook content for PyVista
+        modified_notebook = modify_notebook_for_pyvista(cleared_notebook,
+                                                        dynamic_plot=dynamic_plot)
+
+        # Step 4: Write the modified notebook to the specified output path
+        with open(output_notebook_path, 'w', encoding='utf-8') as f:
+            nbformat.write(modified_notebook, f)
+        print(f"Modified notebook written to: {output_notebook_path}")
+
+    except Exception as e:
+        print(f"Unexpected error during notebook processing: {e}")
+        return None
+
+def clean_and_reset_notebook(input_notebook_path, output_notebook_path):
+    """
+    Clean and reset a Jupyter notebook.
+
+    Steps:
+      1. Open the input notebook.
+      2. Remove output cells using clear_notebook.
+      3. Undo modifications made by modify_notebook_for_pyvista by removing specific cells.
+      4. Save the cleaned and reset notebook to the specified output path.
+
+    Parameters
+    ----------
+    input_notebook_path : str
+        Path to the input Jupyter notebook file (.ipynb).
+    output_notebook_path : str
+        Path to save the cleaned and reset Jupyter notebook file (.ipynb).
+
+    """
+    try:
+        # Step 1: Load the notebook
+        if not os.path.exists(input_notebook_path):
+            raise FileNotFoundError(f"Notebook not found: {input_notebook_path}")
+
+        with open(input_notebook_path, 'r', encoding='utf-8') as f:
+            notebook_content = nbformat.read(f, as_version=4)
+        print(f"Loaded notebook: {input_notebook_path}")
+
+        # Step 2: Clear all notebook outputs
+        cleared_notebook = clear_notebook(notebook_content)
+
+        # Step 3: Undo modifications from `modify_notebook_for_pyvista`
+        cleaned_cells = []
+        for cell in cleared_notebook["cells"]:
+            # Only keep the cell if it doesn't match the pattern added by `modify_notebook_for_pyvista`
+            if cell["cell_type"] == "code" and re.search(r"# ---- for online doc only ---- #", cell["source"]):
+                print("Removing cell from notebook with PyVista modifications.")
+                continue  # Skip this cell
+            # Add the cell to the cleaned list
+            cleaned_cells.append(cell)
+
+        # Update the notebook content with the cleaned cells
+        cleared_notebook["cells"] = cleaned_cells
+
+        # Step 4: Write the cleaned and reset notebook to output path
+        with open(output_notebook_path, 'w', encoding='utf-8') as f:
+            nbformat.write(cleared_notebook, f)
+        print(f"Cleaned and reset notebook written to: {output_notebook_path}")
+
+    except Exception as e:
+        print(f"Error during notebook cleaning and resetting: {e}")
+
+
+def clear_notebook(notebook_content):
+    """ Clear all outputs in a Jupyter notebook content.
+
+    This function removes all cell outputs and return the notebook content.
+
+    Parameters
+    ----------
+    notebook_content : dict, optional
+        The loaded notebook content (as a Python dictionary).
+    Returns
+    -------
+    dict
+        Cleared notebook content.
+    """
+    try:
+        # Clear outputs from the notebook
+        clear_preprocessor = ClearOutputPreprocessor()
+        cleared_notebook, _ = clear_preprocessor.preprocess(notebook_content, {})
+        print("Cleared notebook outputs.")
+        return cleared_notebook
+    except Exception as e:
+        print(f"Error while clearing notebook outputs: {e}")
+
+def modify_notebook_for_pyvista(notebook_content, dynamic_plot=False):
+    """ Modify notebook content for Sphinx documentation by changing pyvista backend configurations
 
         TODO: follow issues to know when a full dynamic plot with `html` can be done
               https://github.com/pyvista/pyvista/issues/6250 --> add_text (title)
@@ -10,65 +144,47 @@ def modify_notebook_for_sphinx(input_path, output_path=None, inplace=True, dynam
 
     Parameters
     ----------
-    input_path : str
-        input path of the notebook
-    output_path : str, optional
-        output path of the notebook, by default None
-    inplace : bool, optional
-        update the notebook inplace, by default True
+    notebook_content : dict
+        The loaded notebook content (as a Python dictionary).
     dynamic_plot : bool, optional
-        make the pyvista plot dynamic or static, by default False.
+        Make the pyvista plot dynamic or static, by default False.
+
+    Returns
+    -------
+    dict
+        Modified notebook content.
     """
-    # Load the notebook
-    with open(input_path) as f:
-        nb = nbformat.read(f, as_version=4)
-
-    # List to store modified cells
-    new_cells = []
-
-    # Should be set to `html` as soon as vtk.js supports text & categorical labelling
-    if dynamic_plot:
-        pyvista_flag = 'html'
-    else:
-        pyvista_flag = 'static'
-
-    for cell in nb.cells:
-        # For markdown cells, update image references
-        if cell.cell_type == "markdown":
-            cell.source = re.sub(
-                r'(<img\s+src=")images/([^"]+)(".*?>)',
-                r'\1_static/\2\3',
-                cell.source
-            )
-
-        # Append the original cell
-        new_cells.append(cell)
-
-        # Check if the cell is a code cell and contains 'import pyvista as pv'
-        if cell.cell_type == "code":
-            if 'import pyvista as pv' in cell.source:
+    try:
+        # List to store modified cells
+        new_cells = []
+
+        # Set the PyVista backend flag
+        pyvista_flag = 'html' if dynamic_plot else 'static'
+
+        for cell in notebook_content['cells']:
+            # Append the original (or modified) cell
+            new_cells.append(cell)
+
+            # Check if the cell is a code cell and contains 'import pyvista as pv'
+            if cell['cell_type'] == "code" and 'import pyvista as pv' in cell['source']:
                 # Create a new cell with PyVista backend configuration
                 new_cell = nbformat.v4.new_code_cell(
                     source=(
                         "# ---- for online doc only ---- #\n"
-                        f"pv.set_jupyter_backend('{pyvista_flag}')\n" 
-                        "pv.start_xvfb()\n"
-                        "import time\n"
-                        "time.sleep(3)  # Give time to xvfb to start \n"
+                        f"pv.set_jupyter_backend('{pyvista_flag}')\n"
+                        "pv.start_xvfb(3)\n"
                         "# ---- end of online doc only ---- #"
                     )
                 )
 
                 # Add metadata to hide the new cell in Sphinx-generated documentation
-                new_cell.metadata["nbsphinx"] = "hidden"
+                new_cell['metadata']["nbsphinx"] = "hidden"
                 new_cells.append(new_cell)
 
-    # Update notebook cells with modified cells
-    nb.cells = new_cells
-
-    # Save the modified notebook
-    if inplace or (output_path is None):
-        output_path = input_path
+        # Update notebook content
+        notebook_content['cells'] = new_cells
+        return notebook_content
 
-    with open(output_path, 'w') as f:
-        nbformat.write(nb, f)
\ No newline at end of file
+    except Exception as e:
+        print(f"Error while modifying notebook: {e}")
+        return None
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 394a172df01cb6e6cd40585cf1ae1c7f35a34237..1cbeddae54624fdb47d6e2e851988ea2fda51e83 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,13 +16,34 @@ dependencies = [
   "meshio",
   "importlib_metadata",
   "plotly",
-  "pyvista[all]",
+  "pyvista",
   "typing_extensions",
   "nbformat>=4.2.0",
-  "scipy"
+  "scipy",
+  "trame",
+  "trame-vtk",
+  "trame-vuetify",
+  "ipywidgets"
 ]
 license = {file = "LICENSE"}
 
+[roles]
+lead_development = [
+    {name = "Manuel Petit", email = "manuel.petit@inria.fr"}
+]
+coordination = [
+    {name = "Olivier Ali", email = "olivier.ali@inria.fr"}
+]
+main_contributors = [
+    {name = "Florian Gacon", email = "florian.gacon@inria.fr"}
+]
+other_contributors = [
+    {name = "Guillaume Cerutti"},
+    {name = "Adrien Heymans"},
+    {name = "Gonzalo Revilla"},
+    {name = "Jonathan Legrand"}
+]
+
 [project.urls]
 Homepage = "https://mosaic.gitlabpages.inria.fr/bvpy/"
 Documentation = "https://mosaic.gitlabpages.inria.fr/bvpy/"
diff --git a/src/bvpy/utils/examples.py b/src/bvpy/utils/examples.py
new file mode 100644
index 0000000000000000000000000000000000000000..1bb1b3a9a5cc48eb8f5dbafdfefe00b1bd5a00d3
--- /dev/null
+++ b/src/bvpy/utils/examples.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+#       bvpy.utils.examples
+#
+#       File author(s):
+#           Manuel Petit <manuel.petit@inria.fr>
+#
+#       File contributor(s):
+#           Manuel Petit <manuel.petit@inria.fr>
+#
+#       File maintainer(s):
+#           Manuel Petit <manuel.petit@inria.fr>
+#
+#       Copyright © by Inria
+#       Distributed under the LGPL License..
+#       See accompanying file LICENSE.txt or copy at
+#           https://www.gnu.org/licenses/lgpl-3.0.en.html
+#
+# -----------------------------------------------------------------------
+
+from bvpy.domains import Cylinder
+from bvpy.boundary_conditions import dirichlet
+from bvpy.vforms.elasticity import LinearElasticForm
+from bvpy.utils.visu import plot
+from bvpy.utils.visu_pyvista import visualize
+from bvpy.bvp import BVP
+
+def cantilever_beam(visualizer: str='pyvista'):
+    """ Example of a cantilever beam simulation.
+
+    This function models a cantilever beam as a cylindrical domain with clamped boundary conditions at one end and
+    a gravitational load applied  along the negative Z-axis. The problem is solved using a variational formulation
+    for linear elasticity, and the solution is returned as a visualization of the displacement field.
+
+    Parameters
+    ----------
+    visualizer : str, optional
+        The type of visualizer to use for visualization purposes. Acceptable values are 'pyvista' (default) or
+        'plotly'.
+
+    Returns
+    -------
+    None
+        This function visualizes the displacement field of the solved boundary value problem
+        either through a Pyvista Plotter or as a Plotly plot depending on the given `visualizer`
+        argument.
+
+    Notes
+    -----
+    The simulation steps include:
+    1. Define the domain of the cantilever beam as a cylinder:
+        - Length of the cylinder: dx = 2 (unit length).
+        - Radius: r = 0.2.
+        - Meshing: tetrahedral cells with a size of 0.1.
+        - Algorithm: Frontal-Delaunay meshing.
+        - The domain is then discretized.
+
+    2. Define boundary conditions:
+        - Dirichlet boundary conditions are applied to the end of the cylinder at x=0:
+          All translations (displacements) in X, Y, and Z are fixed.
+
+    3. Define the variational formulation:
+        - Linear Elasticity Formulation.
+        - Material Parameters:
+          - Young's modulus = 1.
+          - Poisson's ratio = 0.3.
+        - Body force:
+          - Gravitational acceleration: g = 0.016.
+          - Density: ρ = 0.1.
+          - Source term: [0, 0, -ρ * g].
+
+    4. Create and solve the boundary-value problem:
+        - Combine domain, boundary conditions, and variational form into a BVP instance.
+        - Solve the BVP for the cantilever beam.
+
+    5. Visualize the displacement field at the mechanical equilibrium state
+
+    Examples
+    --------
+    >>> from bvpy.utils.examples import cantilever_beam
+    >>> cantilever_beam()
+    """
+    assert visualizer in ['pyvista', 'plotly'], "Acceptable values are 'pyvista' (default) or 'plotly'."
+
+    # - Create a cylinder and set up some parameters for the mesh construction
+    cylinder = Cylinder(x=0, dx=2, dz=0, r=0.2, cell_type='tetra',
+                        cell_size=.1, algorithm='Frontal-Delaunay', clear=True)
+    cylinder.discretize()  # discretize (from Gmsh -> FeniCS)
+
+    # - Create boundary conditions : clamp the beam at x=0
+    bc = [dirichlet(val=[0, 0, 0], boundary="near(x, 0, 1e-2)")]
+
+    # - Create the variational form
+    g = 0.016
+    rho = .1
+    vform = LinearElasticForm(young=1,
+                              poisson=0.3,
+                              source=[0, 0, -rho * g])
+
+    # - Create the boundary-value problem and solve it
+    bvp = BVP(domain=cylinder, vform=vform, bc=bc)
+    bvp.solve()
+
+    # - Visualize the resulting displacement field
+    if visualizer == 'pyvista':
+        visualize(bvp, 'solution')
+    elif visualizer == 'plotly':
+        visualize(bvp.solution)
diff --git a/src/bvpy/utils/io.py b/src/bvpy/utils/io.py
index 6a30e947cb37c28521c6996f676521387edc3a75..054adedca64c7fbdc1bd704f06377acd57d37b7c 100644
--- a/src/bvpy/utils/io.py
+++ b/src/bvpy/utils/io.py
@@ -166,21 +166,26 @@ def read_polygonal_data(path):
                 Values of these properties of each element
                 of the considered topolgocial degree.
 
+    Raises
+    ------
+    ValueError
+        If the file format is not supported.
+
     """
-    try:
-        assert path[-4:] in ['.txt', '.ply', '.obj']
-        if path[-4:] == '.ply':
-            return read_polygonal_ply(path)
+    if path[-4:] not in ['.txt', '.ply', '.obj']:
+        raise ValueError(
+            f"The format of the given file {path} is not supported. "
+            "Supported formats are: '.txt', '.ply', '.obj'."
+        )
 
-        elif path[-4:] == '.obj':
-            return read_polygonal_obj(path)
+    if path[-4:] == '.ply':
+        return read_polygonal_ply(path)
 
-        elif path[-4:] == '.txt':
-            return read_polygonal_txt(path)
+    elif path[-4:] == '.obj':
+        return read_polygonal_obj(path)
 
-    except AssertionError:
-        print('WARNING - utils.read():'
-              + 'the format of the given file is not supported.')
+    elif path[-4:] == '.txt':
+        return read_polygonal_txt(path)
 
 
 def read_polygonal_ply(path):
@@ -218,7 +223,7 @@ def read_polygonal_ply(path):
     data = {0: None, 1: None, 2: None, 3: None}
     property_infos = {0: [], 1: [], 2: [], 3: []}
 
-    # -- Reading the data
+    # -- Reading the tutorial_data
     with open(path, 'r') as file:
         content = file.readlines()
         header = content[:content.index('end_header\n')]
diff --git a/data/mesh.msh b/test/data/mesh_example.msh
similarity index 100%
rename from data/mesh.msh
rename to test/data/mesh_example.msh
diff --git a/data/simple_obj_example.obj b/test/data/obj_example.obj
similarity index 100%
rename from data/simple_obj_example.obj
rename to test/data/obj_example.obj
diff --git a/data/test_small.ply b/test/data/ply_example.ply
similarity index 100%
rename from data/test_small.ply
rename to test/data/ply_example.ply
diff --git a/data/tissue_example.txt b/test/data/tissue_example.txt
similarity index 100%
rename from data/tissue_example.txt
rename to test/data/tissue_example.txt
diff --git a/data/tissue_example_2.txt b/test/data/tissue_example_2.txt
similarity index 100%
rename from data/tissue_example_2.txt
rename to test/data/tissue_example_2.txt
diff --git a/test/test_io.py b/test/test_io.py
index 354f22461a020226cddde59386b49a686d5ceaea..d293ceb46f8ff8f9b9f858a3e728806b3beff636 100644
--- a/test/test_io.py
+++ b/test/test_io.py
@@ -1,5 +1,6 @@
 import unittest
 import tempfile
+from pathlib import Path
 
 from fenics import UnitSquareMesh
 from bvpy import BVP
@@ -19,45 +20,56 @@ class TestIO(unittest.TestCase):
         """
         self.tmp = tempfile.TemporaryDirectory()
 
+        # Base directory where the `tutorial_data` folder resides
+        self.test_data_dir = Path(__file__).parent / "data"
+
     def tearDown(self):
         """Concludes and closes the test.
         """
         self.tmp.cleanup()
 
     def test_read_gmsh_data(self):
-        path = "data/mesh.msh"
+        path = str(self.test_data_dir / "mesh_example.msh")
         dom = CustomDomainGmsh(path)
 
     def test_read_polygonal_data_ply(self):
-        path = "data/test_small.ply"
+        path = str(self.test_data_dir / "ply_example.ply")
         points, cells, _ = read_polygonal_data(path)
 
         self.assertEqual(len(points), 10)
         self.assertEqual(len(cells), 1)
 
     def test_read_polygonal_data_obj(self):
-        path = "data/simple_obj_example.obj"
+        path = str(self.test_data_dir / "obj_example.obj")
         points, cells, _ = read_polygonal_data(path)
 
         self.assertEqual(len(points), 6)
         self.assertEqual(len(cells), 2)
 
     def test_read_polygonal_data_txt(self):
-        path = "data/tissue_example.txt"
+        path = str(self.test_data_dir / "tissue_example.txt")
         points, cells, _ = read_polygonal_data(path)
         self.assertEqual(len(points), 220)
         self.assertEqual(len(cells), 61)
 
     def test_read_polygonal_data_txt_2(self):
-        path = "data/tissue_example_2.txt"
+        path = str(self.test_data_dir / "tissue_example_2.txt")
         points, cells, label = read_polygonal_data(path)
         self.assertEqual(label[0], 0)
         self.assertEqual(label[-1], 60)
         self.assertEqual(len(label), 61)
 
     def test_read_polygonal_data_wrong_type(self):
-        path = "data/test_recording_mesh.xdmf"
-        read_polygonal_data(path)
+        path = str(self.test_data_dir / "test_recording_mesh.xdmf")
+
+        with self.assertRaises(ValueError) as context:
+            read_polygonal_data(path)
+
+        # Check that the exception message is correct
+        self.assertIn(
+            f"The format of the given file {path} is not supported.",
+            str(context.exception)
+        )
 
     def test_write_ply_mesh(self):
         mesh = UnitSquareMesh(2, 2)
diff --git a/tutorials/bvpy_tutorial_10_morphoelasticity.ipynb b/tutorials/bvpy_tutorial_10_morphoelasticity.ipynb
index 2c3e00123e440386a9402afd573326c0e3f79480..e14c508fbdb2c1edb1bff26297c73eacc087a65a 100644
--- a/tutorials/bvpy_tutorial_10_morphoelasticity.ipynb
+++ b/tutorials/bvpy_tutorial_10_morphoelasticity.ipynb
@@ -10,6 +10,8 @@
     "\n",
     "To help understand the concepts and tools used in this notebook, we provide a brief theoretical background and context for the methods implemented here.\n",
     "\n",
+    "Download the tutorial notebooks and accompanying data as a ZIP file: [tutorials.zip](https://mosaic.gitlabpages.inria.fr/bvpy/tutorials.zip).\n",
+    "\n",
     "## Overview of Functions and Methods\n",
     "\n",
     "This tutorial focuses on implementing and visualizing morphoelastic simulations using specialized classes and methods from `bvpy`. Here is an introduction to the main components utilized in this tutorial:\n",
diff --git a/tutorials/bvpy_tutorial_1_hello_world.ipynb b/tutorials/bvpy_tutorial_1_hello_world.ipynb
index 819f26bb05742b290d9d40e233a2a8d971e7c509..292058a77876c856b5700edd02b5c7eccfb75c89 100644
--- a/tutorials/bvpy_tutorial_1_hello_world.ipynb
+++ b/tutorials/bvpy_tutorial_1_hello_world.ipynb
@@ -14,9 +14,9 @@
     "\n",
     "- Basic classes: `Rectangle()` (domain), `PoissonForm()` (vform), `BVP()` and main methods/functions: `.solve()`, `.info()`, `save()`, `plot()`...\n",
     "\n",
-    "- Error analys.\n",
+    "- Error analysis.\n",
     "\n",
-    "Download the notebook: [bvpy_tutorial_1](https://gitlab.inria.fr/mosaic/publications/bvpy/-/raw/develop/tutorials/bvpy_tutorial_1_hello_world.ipynb?inline=false)."
+    "Download the tutorial notebooks and accompanying data as a ZIP file: [tutorials.zip](https://mosaic.gitlabpages.inria.fr/bvpy/tutorials.zip)."
    ]
   },
   {
@@ -183,9 +183,11 @@
    ]
   },
   {
-   "metadata": {},
    "cell_type": "markdown",
-   "source": "In its current state, **Bvpy** provides minimalist visualization and analysis tools. One may want to turn to other frameworks, such as **Paraview** for instance, to visualize and analyze simulation results. To do so, **Bvpy** enable the recording of simulation in the `.xdmf` format."
+   "metadata": {},
+   "source": [
+    "In its current state, **Bvpy** provides minimalist visualization and analysis tools. One may want to turn to other frameworks, such as **Paraview** for instance, to visualize and analyze simulation results. To do so, **Bvpy** enable the recording of simulation in the `.xdmf` format."
+   ]
   },
   {
    "cell_type": "code",
@@ -194,7 +196,7 @@
    "outputs": [],
    "source": [
     "from bvpy.utils.io import save\n",
-    "save(prblm_1, '../data/tutorial_results/tuto_1_result_1.xdmf')"
+    "save(prblm_1, 'tutorial_data/tuto_1_result_1.xdmf')"
    ]
   },
   {
@@ -411,9 +413,7 @@
   {
    "cell_type": "markdown",
    "metadata": {},
-   "source": [
-    "> **Note:** The `cdata` and `bdata` attributes are `MeshFunctionSizet`, *i.e.* **FEniCS** functions defined on a given mesh returning integer values. They can be useful for parametrization, as we will see in tutorial #6."
-   ]
+   "source": "> **Note:** The `cdata` and `bdata` attributes are `MeshFunctionSizet`, *i.e.* **FEniCS** functions defined on a given mesh returning integer values. They can be useful for parametrization, as we will see in tutorial [Linear Elasticity](bvpy_tutorial_6_linear_elasticity.html)."
   },
   {
    "cell_type": "markdown",
@@ -480,7 +480,7 @@
  ],
  "metadata": {
   "kernelspec": {
-   "display_name": "Python 3",
+   "display_name": "Python 3 (ipykernel)",
    "language": "python",
    "name": "python3"
   },
@@ -494,7 +494,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.7.9"
+   "version": "3.9.20"
   },
   "latex_envs": {
    "LaTeX_envs_menu_present": true,
diff --git a/tutorials/bvpy_tutorial_2_domains.ipynb b/tutorials/bvpy_tutorial_2_domains.ipynb
index 526f4776bfb4e46ee6882ca0f75b99e56bab2b95..3f070177fef36c5ef6776d2fceed01f8b0b24e0a 100644
--- a/tutorials/bvpy_tutorial_2_domains.ipynb
+++ b/tutorials/bvpy_tutorial_2_domains.ipynb
@@ -23,7 +23,7 @@
     "- Generation of domains from existing files: `CustomDomain` and `CustomPolygonalDomain` classes.\n",
     "\n",
     "\n",
-    "Download the notebook: [bvpy_tutorial_2](https://gitlab.inria.fr/mosaic/publications/bvpy/-/raw/develop/tutorials/bvpy_tutorial_2_domains.ipynb?inline=false)."
+    "Download the tutorial notebooks and accompanying data as a ZIP file: [tutorials.zip](https://mosaic.gitlabpages.inria.fr/bvpy/tutorials.zip)."
    ]
   },
   {
@@ -392,7 +392,7 @@
    "source": [
     "from bvpy.domains import CustomDomain\n",
     "\n",
-    "path = '../data/example_meristem.ply'\n",
+    "path = 'tutorial_data/domains/example_meristem.ply'\n",
     "\n",
     "cd = CustomDomain.read(path)\n",
     "\n",
@@ -419,7 +419,7 @@
     "from bvpy.utils.visu import plot\n",
     "from bvpy.domains import CustomPolygonalDomain\n",
     "\n",
-    "path ='../data/tissue_example_curved_big.txt'\n",
+    "path ='tutorial_data/domains/tissue_example_curved_big.txt'\n",
     "\n",
     "tissue = CustomPolygonalDomain.read(path)\n",
     "\n",
diff --git a/tutorials/bvpy_tutorial_3_vforms.ipynb b/tutorials/bvpy_tutorial_3_vforms.ipynb
index 459e89c16fe6b788d35d016cc0367ff5dfa50488..17250c8c88c4b325cdfb4fd0376db283f79c808e 100644
--- a/tutorials/bvpy_tutorial_3_vforms.ipynb
+++ b/tutorials/bvpy_tutorial_3_vforms.ipynb
@@ -16,7 +16,7 @@
     "\n",
     "- Implementation of *de novo* `vform`: Definition of the variational formulation of a differential equation, instanciation of mandatory `@staticmethods`: `.construct_form()` and `.set_expression`.\n",
     "\n",
-    "Download the notebook: [bvpy_tutorial_3](https://gitlab.inria.fr/mosaic/publications/bvpy/-/raw/develop/tutorials/bvpy_tutorial_3_vforms.ipynb?inline=false)."
+    "Download the tutorial notebooks and accompanying data as a ZIP file: [tutorials.zip](https://mosaic.gitlabpages.inria.fr/bvpy/tutorials.zip)."
    ]
   },
   {
@@ -321,7 +321,7 @@
     "from bvpy.utils.io import save\n",
     "\n",
     "plot(prblm.solution)\n",
-    "save(prblm.solution, '../data/tutorial_results/tuto_3_result_1.xdmf')"
+    "save(prblm.solution, 'tutorial_data/tuto_3_result_1.xdmf')"
    ]
   }
  ],
diff --git a/tutorials/bvpy_tutorial_4_bnd_cond.ipynb b/tutorials/bvpy_tutorial_4_bnd_cond.ipynb
index 941388edef2808ab5f3a16125f8be6124dc884ff..c20c581cd4192354f5548a2a5595eeb145395cc4 100644
--- a/tutorials/bvpy_tutorial_4_bnd_cond.ipynb
+++ b/tutorials/bvpy_tutorial_4_bnd_cond.ipynb
@@ -23,7 +23,7 @@
     "\n",
     "- Example of visualization of boundary conditions when a bvp is defined.\n",
     "\n",
-    "Download the notebook: [bvpy_tutorial_4](https://gitlab.inria.fr/mosaic/publications/bvpy/-/raw/develop/tutorials/bvpy_tutorial_4_bnd_cond.ipynb?inline=false)."
+    "Download the tutorial notebooks and accompanying data as a ZIP file: [tutorials.zip](https://mosaic.gitlabpages.inria.fr/bvpy/tutorials.zip)."
    ]
   },
   {
diff --git a/tutorials/bvpy_tutorial_5_reaction_diffusion.ipynb b/tutorials/bvpy_tutorial_5_reaction_diffusion.ipynb
index 864116f6a5237f55d022e961166618da92ca3564..21188ed3e4dd8b7a1ec68620ee109f350b9bcd99 100644
--- a/tutorials/bvpy_tutorial_5_reaction_diffusion.ipynb
+++ b/tutorials/bvpy_tutorial_5_reaction_diffusion.ipynb
@@ -25,7 +25,7 @@
     "\n",
     "\n",
     "\n",
-    "Download the notebook: [bvpy_tutorial_5](https://gitlab.inria.fr/mosaic/publications/bvpy/-/raw/develop/tutorials/bvpy_tutorial_5_turing_system.ipynb?inline=false)."
+    "Download the tutorial notebooks and accompanying data as a ZIP file: [tutorials.zip](https://mosaic.gitlabpages.inria.fr/bvpy/tutorials.zip)."
    ]
   },
   {
@@ -152,16 +152,16 @@
    ]
   },
   {
-   "cell_type": "code",
-   "execution_count": null,
    "metadata": {},
+   "cell_type": "code",
    "outputs": [],
+   "execution_count": null,
    "source": [
     "t_min = 0\n",
     "t_max = 29\n",
     "dt = 0.1\n",
     "\n",
-    "wave_propagation.integrate(t_min, t_max, dt, '../data/tutorial_results/tuto_5_result_1.xdmf', store_steps=True)"
+    "wave_propagation.integrate(t_min, t_max, dt, 'tutorial_data/tuto_5_result_1.xdmf', store_steps=True)"
    ]
   },
   {
@@ -188,14 +188,14 @@
    ]
   },
   {
-   "cell_type": "code",
-   "execution_count": null,
    "metadata": {},
+   "cell_type": "code",
    "outputs": [],
+   "execution_count": null,
    "source": [
     "from IPython.display import Image\n",
     "\n",
-    "Image(filename='../data/tutorial_results/tuto_5_result_1_paraview_snapshot.gif.png')"
+    "Image(filename='tutorial_data/results/tuto_5_result_1_paraview_snapshot.gif.png')"
    ]
   },
   {
@@ -463,16 +463,16 @@
    ]
   },
   {
-   "cell_type": "code",
-   "execution_count": null,
    "metadata": {},
+   "cell_type": "code",
    "outputs": [],
+   "execution_count": null,
    "source": [
     "t_min = 0\n",
     "t_max = 9\n",
     "dt = 0.1\n",
     "\n",
-    "save_path = '../data/tutorial_results/tuto_5_result_2.xdmf'\n",
+    "save_path = 'tutorial_data/tuto_5_result_2.xdmf'\n",
     "\n",
     "turing_system.integrate(t_min, t_max, dt, save_path)"
    ]
@@ -522,16 +522,16 @@
    ]
   },
   {
-   "cell_type": "code",
-   "execution_count": null,
    "metadata": {},
+   "cell_type": "code",
    "outputs": [],
+   "execution_count": null,
    "source": [
     "from IPython.display import Image\n",
     "\n",
-    "Image(filename='../data/tutorial_results/tuto_5_result_2_paraview_snapshot.gif.png')\n",
+    "Image(filename='tutorial_data/results/tuto_5_result_2_paraview_snapshot.gif.png')\n",
     "#from IPython.display import HTML\n",
-    "#HTML('<img src=\"../data/tutorial_results/tuto_5_result_2_paraview_snapshot.gif\">')"
+    "#HTML('<img src=\"../tutorial_data/tutorial_results/tuto_5_result_2_paraview_snapshot.gif\">')"
    ]
   }
  ],
diff --git a/tutorials/bvpy_tutorial_6_linear_elasticity.ipynb b/tutorials/bvpy_tutorial_6_linear_elasticity.ipynb
index bc3b1a80d432132175830978119d3cb0c207ec33..1625e8a77ec1e146978a37e5af8b4e4fee86bfc2 100644
--- a/tutorials/bvpy_tutorial_6_linear_elasticity.ipynb
+++ b/tutorials/bvpy_tutorial_6_linear_elasticity.ipynb
@@ -25,7 +25,7 @@
     "\n",
     "\n",
     "\n",
-    "Download the notebook: [bvpy_tutorial_6](https://gitlab.inria.fr/mosaic/publications/bvpy/-/raw/develop/tutorials/bvpy_tutorial_6_linear_elasticity.ipynb?inline=false)."
+    "Download the tutorial notebooks and accompanying data as a ZIP file: [tutorials.zip](https://mosaic.gitlabpages.inria.fr/bvpy/tutorials.zip)."
    ]
   },
   {
@@ -273,7 +273,7 @@
    "source": [
     "from bvpy.utils.io import save\n",
     "\n",
-    "path = '../data/tutorial_results/tuto_6_result_1.xdmf'\n",
+    "path = 'tutorial_data/tuto_6_result_1.xdmf'\n",
     "save(stretching, path)"
    ],
    "outputs": [],
@@ -285,7 +285,7 @@
    "source": [
     "from IPython.display import Image\n",
     "\n",
-    "Image(filename='../data/tutorial_results/tuto_6_result_1_paraview_snapshot.png')"
+    "Image(filename='tutorial_data/results/tuto_6_result_1_paraview_snapshot.png')"
    ],
    "outputs": [],
    "execution_count": null
@@ -410,7 +410,7 @@
    "source": [
     "wound_displacement = wound_healing.solution\n",
     "plot(wound_displacement, size=1)\n",
-    "path = '../data/tutorial_results/tuto_6_result_2.xdmf'\n",
+    "path = 'tutorial_data/tuto_6_result_2.xdmf'\n",
     "save(wound_healing, path)"
    ],
    "outputs": [],
@@ -468,18 +468,14 @@
   {
    "cell_type": "code",
    "metadata": {},
-   "source": [
-    "save(stress, '../data/tutorial_results/tuto_6_result_3.xdmf')"
-   ],
+   "source": "save(stress, 'tutorial_data/tuto_6_result_3.xdmf')",
    "outputs": [],
    "execution_count": null
   },
   {
    "cell_type": "code",
    "metadata": {},
-   "source": [
-    "Image(filename=\"../data/tutorial_results/tuto_6_result_3_paraview_snapshot.png\")"
-   ],
+   "source": "Image(filename=\"tutorial_data/results/tuto_6_result_3_paraview_snapshot.png\")",
    "outputs": [],
    "execution_count": null
   },
@@ -508,7 +504,7 @@
     "from bvpy.domains import CustomPolygonalDomain\n",
     "from bvpy.utils.visu import plot\n",
     "\n",
-    "tissue = CustomPolygonalDomain.read(\"../data/example_domain_sepal_2D_slice.ply\",\n",
+    "tissue = CustomPolygonalDomain.read(\"tutorial_data/domains/example_domain_sepal_2D_slice.ply\",\n",
     "                                    cell_size=.5,\n",
     "                                    dim=2)\n",
     "tissue.discretize()\n",
@@ -581,7 +577,7 @@
     "displacement = prblm.solution\n",
     "#plot(displacement, size=1, norm=True)\n",
     "\n",
-    "save(displacement, '../data/tutorial_results/tuto_6_result_4.xdmf')"
+    "save(displacement, 'tutorial_data/tuto_6_result_4.xdmf')"
    ],
    "outputs": [],
    "execution_count": null
@@ -610,8 +606,8 @@
    "metadata": {},
    "source": [
     "stress = heterogeneous_elastic_response.get_stress(displacement)\n",
-    "save(stress, \"../data/tutorial_results/tuto_6_result_5.xdmf\")\n",
-    "Image(filename=\"../data/tutorial_results/tuto_6_result_5_paraview_snapshot.png\")"
+    "save(stress, \"tutorial_data/tuto_6_result_5.xdmf\")\n",
+    "Image(filename=\"tutorial_data/results/tuto_6_result_5_paraview_snapshot.png\")"
    ],
    "outputs": [],
    "execution_count": null
@@ -820,9 +816,9 @@
    "cell_type": "code",
    "metadata": {},
    "source": [
-    "fixed_center = [dirichlet(0, boundary='near(x, 0)', subspace=0),\n",
-    "                dirichlet(0, boundary='near(y, 0)', subspace=1),\n",
-    "                dirichlet(0, boundary='near(z, 0)', subspace=2)]"
+    "fixed_center = [dirichlet(0, boundary='near(x, 0)', subspace=0, method='pointwise'),\n",
+    "                dirichlet(0, boundary='near(y, 0)', subspace=1, method='pointwise'),\n",
+    "                dirichlet(0, boundary='near(z, 0)', subspace=2, method='pointwise')]"
    ],
    "outputs": [],
    "execution_count": null
@@ -851,7 +847,7 @@
    "source": [
     "displacement = pressurized_shell.solution\n",
     "\n",
-    "save(displacement, '../data/tutorial_results/tuto_6_result_6.xdmf')\n",
+    "save(displacement, 'tutorial_data/tuto_6_result_6.xdmf')\n",
     "\n",
     "plot(displacement, size=.25, norm=True)"
    ],
@@ -880,9 +876,7 @@
   {
    "cell_type": "code",
    "metadata": {},
-   "source": [
-    "tissue = CustomPolygonalDomain.read('../data/example_domain_curved_cellularized_tissue.ply')"
-   ],
+   "source": "tissue = CustomPolygonalDomain.read('tutorial_data/domains/example_domain_curved_cellularized_tissue.ply')",
    "outputs": [],
    "execution_count": null
   },
@@ -958,7 +952,7 @@
    "source": [
     "displacement = prblm.solution\n",
     "\n",
-    "save(displacement, '../data/tutorial_results/tuto_6_result_7.xdmf')\n",
+    "save(displacement, 'tutorial_data/tuto_6_result_7.xdmf')\n",
     "\n",
     "plot(displacement, size=1000, norm=True)"
    ],
diff --git a/tutorials/bvpy_tutorial_7_hyper_elasticity.ipynb b/tutorials/bvpy_tutorial_7_hyper_elasticity.ipynb
index 820ce379d2b3fd275caa5567f34d8bcd063c678b..48b9fb1c98c5a6818378218dd6bea27144eac138 100644
--- a/tutorials/bvpy_tutorial_7_hyper_elasticity.ipynb
+++ b/tutorials/bvpy_tutorial_7_hyper_elasticity.ipynb
@@ -21,7 +21,7 @@
     "\n",
     "\n",
     "\n",
-    "Download the notebook: [bvpy_tutorial_7](https://gitlab.inria.fr/mosaic/publications/bvpy/-/raw/develop/tutorials/bvpy_tutorial_7_hyper_elasticity.ipynb?inline=false)."
+    "Download the tutorial notebooks and accompanying data as a ZIP file: [tutorials.zip](https://mosaic.gitlabpages.inria.fr/bvpy/tutorials.zip)."
    ]
   },
   {
@@ -684,7 +684,7 @@
     "    t_max = kwargs.get('t_max',2.5)\n",
     "    dt = kwargs.get('dt',.1)\n",
     "    \n",
-    "    save_path = kwargs.get('saving_path', '../data/tutorial_results/tuto_7_result_1.xdmf')\n",
+    "    save_path = kwargs.get('saving_path', 'tutorial_data/tuto_7_result_1.xdmf')\n",
     "    \n",
     "    # -- Instancing Turing model\n",
     "    LE_model = CoupledTransportForm()\n",
@@ -770,7 +770,7 @@
     "    stress = compute_mecha_equi(concentrations)\n",
     "    concentrations = compute_turing_pattern(concentrations)\n",
     "\n",
-    "    save(stress, f\"../data/tutorial_results/tuto_7_result_{1+step}.xdmf\")"
+    "    save(stress, f\"tutorial_data/tuto_7_result_{1+step}.xdmf\")"
    ],
    "outputs": [],
    "execution_count": null
@@ -795,7 +795,7 @@
    "source": [
     "from IPython.display import Image\n",
     "\n",
-    "Image(filename=\"../data/tutorial_results/tuto_7_result_3_paraview_snapshot.gif.png\")"
+    "Image(filename=\"tutorial_data/results/tuto_7_result_3_paraview_snapshot.gif.png\")"
    ],
    "outputs": [],
    "execution_count": null
diff --git a/tutorials/bvpy_tutorial_8_pyvista.ipynb b/tutorials/bvpy_tutorial_8_pyvista.ipynb
index 553ecd8a7b4b15b63dae7a9134eac7f4aab10415..3e40b020ae43f3e893c96a4b1210b991f49ac57e 100644
--- a/tutorials/bvpy_tutorial_8_pyvista.ipynb
+++ b/tutorials/bvpy_tutorial_8_pyvista.ipynb
@@ -1,15 +1,17 @@
 {
  "cells": [
   {
-   "cell_type": "markdown",
-   "id": "ce32e709",
    "metadata": {},
+   "cell_type": "markdown",
    "source": [
     "\n",
     "# Visualization with `PyVista`\n",
     "\n",
-    "This tutorial explores the use of `PyVista` for visualizing various objects within the `bvpy` library. While previous visualizations in `bvpy` primarily used `Plotly`, this tutorial demonstrates how `PyVista` can enhance the visualization experience. Users can still use `Plotly` if preferred; however, `PyVista` provides specific advantages in handling complex 3D meshes and volumetric data more efficiently.\n"
-   ]
+    "This tutorial explores the use of `PyVista` for visualizing various objects within the `bvpy` library. While previous visualizations in `bvpy` primarily used `Plotly`, this tutorial demonstrates how `PyVista` can enhance the visualization experience. Users can still use `Plotly` if preferred; however, `PyVista` provides specific advantages in handling complex 3D meshes and volumetric data more efficiently.\n",
+    "\n",
+    "Download the tutorial notebooks and accompanying data as a ZIP file: [tutorials.zip](https://mosaic.gitlabpages.inria.fr/bvpy/tutorials.zip).\n"
+   ],
+   "id": "c57d32315ff947c"
   },
   {
    "metadata": {},
@@ -28,19 +30,19 @@
     "\n",
     "*Note: Visualization of tensor fields is currently not supported but will be introduced in a future release.*"
    ],
-   "id": "1d939304979bb125"
+   "id": "a316fe34f491896a"
   },
   {
-   "cell_type": "code",
-   "id": "e8ec604b7f241a60",
    "metadata": {},
+   "cell_type": "code",
+   "outputs": [],
+   "execution_count": null,
    "source": [
     "import pyvista as pv\n",
     "import math\n",
     "from bvpy.utils.visu_pyvista import visualize"
    ],
-   "outputs": [],
-   "execution_count": null
+   "id": "afbf7befc1a34a8b"
   },
   {
    "cell_type": "markdown",
@@ -54,8 +56,10 @@
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "6fd0fb4b-7f5d-4979-af33-ef96041d75cc",
    "metadata": {},
+   "outputs": [],
    "source": [
     "from bvpy.domains.primitives import Cylinder\n",
     "\n",
@@ -66,9 +70,7 @@
     "cylinder  = Cylinder(x=0, dx=L, dz=0, r=r, cell_type='triangle',\n",
     "                     cell_size=.1, algorithm='Frontal-Delaunay', clear=True)\n",
     "cylinder.discretize() # discretize (from Gmsh -> FeniCS)"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
    "cell_type": "markdown",
@@ -80,29 +82,31 @@
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "45c8981941e15904",
    "metadata": {},
+   "outputs": [],
    "source": [
     "visualize(cylinder, visu_type='mesh')"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
    "cell_type": "markdown",
    "id": "9eb5b9c3fd496d6d",
    "metadata": {},
-   "source": "Notice that it is possible to give directly the `FeniCS` Mesh object"
+   "source": [
+    "Notice that it is possible to give directly the `FeniCS` Mesh object"
+   ]
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "b43a12b3-005d-4d2e-afc8-ce69eb00df43",
    "metadata": {},
+   "outputs": [],
    "source": [
     "visualize(cylinder.mesh, visu_type='mesh', window_size=[600, 400]) # give the Mesh object and control the window size"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
    "cell_type": "markdown",
@@ -117,12 +121,17 @@
   {
    "metadata": {},
    "cell_type": "markdown",
-   "source": "As an example, we will define a custom geometry called `CutCylinder`. This geometry represents a cylinder that is divided into two distinct subdomains. For detailed steps on generating this geometry, refer to the tutorial [Generate advanced geometries using Gmsh](bvpy_tutorial_9_gmsh).",
-   "id": "497835f2c378930a"
+   "source": [
+    "\n",
+    "As an example, we will define a custom geometry called `CutCylinder`. This geometry represents a cylinder that is divided into two distinct subdomains. For detailed steps on generating this geometry, refer to the tutorial [Generate advanced geometries using Gmsh](bvpy_tutorial_9_gmsh.html)."
+   ],
+   "id": "dc47158816d1ea4d"
   },
   {
    "metadata": {},
    "cell_type": "code",
+   "outputs": [],
+   "execution_count": null,
    "source": [
     "from bvpy.domains.abstract import OccModel, AbstractDomain\n",
     "\n",
@@ -156,19 +165,22 @@
     "        self.model.addPhysicalGroup(3, [right_surface], tag=2)\n",
     "        self.model.setPhysicalName(3, 2, \"Right_Subdomain\")"
    ],
-   "id": "99e8589b7c70f8f9",
-   "outputs": [],
-   "execution_count": null
+   "id": "6547c0d4549458df"
   },
   {
-   "metadata": {},
    "cell_type": "markdown",
-   "source": "We can now initialize and visualize an instance of the `CutCylinder` class, which represents the custom geometry defined above.",
-   "id": "674698ebdbd01f11"
+   "id": "674698ebdbd01f11",
+   "metadata": {},
+   "source": [
+    "We can now initialize and visualize an instance of the `CutCylinder` class, which represents the custom geometry defined above."
+   ]
   },
   {
-   "metadata": {},
    "cell_type": "code",
+   "execution_count": null,
+   "id": "6b6950b42f835af5",
+   "metadata": {},
+   "outputs": [],
    "source": [
     "cut_cylinder = CutCylinder(length=L, radius=r,\n",
     "                           cell_type='tetra', algorithm='Frontal-Delaunay',\n",
@@ -176,53 +188,60 @@
     "cut_cylinder.discretize()\n",
     "\n",
     "print(f\"The available sub-domains are {cut_cylinder.sub_domain_names}\")"
-   ],
-   "id": "6b6950b42f835af5",
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
-   "metadata": {},
    "cell_type": "code",
-   "source": "visualize(cut_cylinder, visu_type='domain')",
+   "execution_count": null,
    "id": "be0836fa9004f4fb",
+   "metadata": {},
    "outputs": [],
-   "execution_count": null
+   "source": [
+    "visualize(cut_cylinder, visu_type='domain')"
+   ]
   },
   {
-   "metadata": {},
    "cell_type": "markdown",
-   "source": "Notice that it is possible to give directly the `FeniCS` MeshSizet object",
-   "id": "66f4f0ee9e579385"
+   "id": "66f4f0ee9e579385",
+   "metadata": {},
+   "source": [
+    "Notice that it is possible to give directly the `FeniCS` MeshSizet object"
+   ]
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "a5be726f73bf8fa0",
    "metadata": {},
-   "source": "visualize(cut_cylinder.cdata, visu_type='domain', show_edges=False) # give the MeshSizet object and control the display of the edges",
    "outputs": [],
-   "execution_count": null
+   "source": [
+    "visualize(cut_cylinder.cdata, visu_type='domain', show_edges=False) # give the MeshSizet object and control the display of the edges"
+   ]
   },
   {
    "cell_type": "markdown",
    "id": "f5d23d182bc6da05",
    "metadata": {},
-   "source": "If you directly provide the `FeniCS` `MeshSizet` object, you can retrieve the sub-domain names by using the optional argument `dict_values`:"
+   "source": [
+    "If you directly provide the `FeniCS` `MeshSizet` object, you can retrieve the sub-domain names by using the optional argument `dict_values`:"
+   ]
   },
   {
-   "metadata": {},
    "cell_type": "code",
-   "source": "visualize(cut_cylinder.cdata, visu_type='domain', dict_values=cut_cylinder.sub_domain_names)",
+   "execution_count": null,
    "id": "8c9d36506e6a17ac",
+   "metadata": {},
    "outputs": [],
-   "execution_count": null
+   "source": [
+    "visualize(cut_cylinder.cdata, visu_type='domain', dict_values=cut_cylinder.sub_domain_names)"
+   ]
   },
   {
    "cell_type": "markdown",
    "id": "9bf14848-0641-4826-b4a2-d9f760344d79",
    "metadata": {},
    "source": [
-    "### Diriclet boundary conditions\n",
+    "### Dirichlet boundary conditions\n",
     "\n",
     "In this section, we will visualize the Dirichlet boundary conditions to verify that they are correctly applied in the problem setup.\n",
     "We will use a simple linear elastic problem where the previously defined cylinder is stretched at its ends."
@@ -230,8 +249,10 @@
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "40f911dc-78ae-4b73-bd6d-65af44d5ed44",
    "metadata": {},
+   "outputs": [],
    "source": [
     "from bvpy.boundary_conditions import dirichlet, Boundary\n",
     "from bvpy.utils.pre_processing import HeterogeneousParameter\n",
@@ -252,9 +273,7 @@
     "\n",
     "### - Build the problem - ###\n",
     "elastic_pb = BVP(cut_cylinder, elastic_model, bc1+bc2)"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
    "cell_type": "markdown",
@@ -266,13 +285,13 @@
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "def488a2833689cc",
    "metadata": {},
+   "outputs": [],
    "source": [
     "visualize(elastic_pb, visu_type='dirichlet', scale=0.1, val_range=[0, 5])"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
    "cell_type": "markdown",
@@ -286,36 +305,36 @@
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "7793c8bac4680bf9",
    "metadata": {},
+   "outputs": [],
    "source": [
     "# Available parameters from the weak form\n",
     "print(f\"Available parameters from the weak form : {list(elastic_pb.vform._parameters.keys())}\")"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "467cf8b5-7433-4bac-a347-6f9f38f28360",
    "metadata": {},
+   "outputs": [],
    "source": [
     "visualize(elastic_pb, visu_type='young')"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "ad63e001-dd9c-448b-93cc-6a6e379b753f",
    "metadata": {},
+   "outputs": [],
    "source": [
     "# Do the same for the poisson parameter but change the camera view of the pyvista.Plotter\n",
     "pl = visualize(elastic_pb, visu_type='poisson') # get the pyvisya.Plotter\n",
     "pl.view_xz() # change the camera view"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
    "cell_type": "markdown",
@@ -329,30 +348,30 @@
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "d3d84515-748e-4f9b-8e12-c2a5d6e0f05c",
    "metadata": {},
+   "outputs": [],
    "source": [
     "# - Solve the previous elastic problem\n",
     "elastic_pb.solve()\n",
     "\n",
     "# - Visualize the resulting displacement field\n",
     "visualize(elastic_pb, 'solution', scale=0.1)"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "b965cf59-d94e-4b74-b711-3e3468f37480",
    "metadata": {},
+   "outputs": [],
    "source": [
     "print(type(elastic_pb.solution))\n",
     "\n",
     "# - You can also visualize any FeniCS Function\n",
     "visualize(elastic_pb.solution, scale=0.1, show_mesh=False) # show directly the FeniCS Function & remove mesh"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
    "cell_type": "markdown",
@@ -364,14 +383,14 @@
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "7e77fab2-6171-4cbb-a093-348545cd9b0a",
    "metadata": {},
+   "outputs": [],
    "source": [
     "pl = visualize(elastic_pb.solution, scale=0.1, show_mesh=False, show_plot=False) # show the FeniCS Function without the mesh\n",
     "visualize(elastic_pb.domain.mesh, plotter=pl, actor_kwargs={'style':'wireframe', 'line_width': 2}) # add the mesh as a 'wireframe' & specify the edge size"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
    "cell_type": "markdown",
@@ -397,8 +416,10 @@
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "354f8ef0-2f33-44ac-a077-f451c9abe24b",
    "metadata": {},
+   "outputs": [],
    "source": [
     "# - Some parameters\n",
     "young = {1: 0.5, 2: 1.5}\n",
@@ -417,14 +438,14 @@
     "\n",
     "# - Compute the deformed mesh\n",
     "deformed_cylinder = cut_cylinder.move(bvp.solution, return_cdata=False) # create a copy of the object & apply displacement field"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "60f96a29-b395-45d3-974c-bd74b5819cd2",
    "metadata": {},
+   "outputs": [],
    "source": [
     "# - Define a pv.Plotter with 6 subplots (shape = (nrow, ncol))\n",
     "pl = pv.Plotter(shape=(2, 3), window_size=[1000, 800])\n",
@@ -451,9 +472,7 @@
     "\n",
     "pl.link_views() # link the camera of the subplots\n",
     "pl.show() # finally show the plot"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
    "cell_type": "markdown",
@@ -467,7 +486,9 @@
    "cell_type": "markdown",
    "id": "1cd028f30cc62ca9",
    "metadata": {},
-   "source": "### Visualize sub-space of `FeniCS` object"
+   "source": [
+    "### Visualize sub-space of `FeniCS` object"
+   ]
   },
   {
    "cell_type": "markdown",
@@ -479,15 +500,15 @@
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "b8b0e47b3808d048",
    "metadata": {},
+   "outputs": [],
    "source": [
     "stress = vform.get_stress(bvp.solution)\n",
     "\n",
     "print(f\"The stress field is a tensor field of shape {stress.ufl_shape}\")"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
    "cell_type": "markdown",
@@ -499,17 +520,17 @@
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "145fc3ce7d3ce05c",
    "metadata": {},
+   "outputs": [],
    "source": [
     "SCALAR_BAR_ARGS = dict(title_font_size=12,\n",
     "                              label_font_size=10,\n",
     "                              n_labels=3,\n",
     "                              italic=True,\n",
     "                              fmt=\"%.1e\")"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
    "cell_type": "markdown",
@@ -521,8 +542,10 @@
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "c5619019b6d87755",
    "metadata": {},
+   "outputs": [],
    "source": [
     "dict_sub = {'xx': 0, 'yy': 4, 'zz': 8} # FeniCS subspace index are organized in 1D --> xx, xy, xz, yx, yy, yz, zx, zy, zz\n",
     "\n",
@@ -534,21 +557,23 @@
     "\n",
     "pl.link_views()\n",
     "pl.show()"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   },
   {
    "cell_type": "markdown",
    "id": "c7a5faacfdbc7609",
    "metadata": {},
-   "source": "### Use `Pyvista` widgets to enhance visualization options"
+   "source": [
+    "### Use `Pyvista` widgets to enhance visualization options"
+   ]
   },
   {
    "cell_type": "markdown",
    "id": "ce8999330d5618e4",
    "metadata": {},
-   "source": "Various `Pyvista` widgets can be used to help the visualization of `FeniCS` object. Feel free to consult the widgets from https://docs.pyvista.org/examples/03-widgets/index.html."
+   "source": [
+    "Various `Pyvista` widgets can be used to help the visualization of `FeniCS` object. Feel free to consult the widgets from https://docs.pyvista.org/examples/03-widgets/index.html."
+   ]
   },
   {
    "cell_type": "markdown",
@@ -560,16 +585,16 @@
   },
   {
    "cell_type": "code",
+   "execution_count": null,
    "id": "ed38c8e729addf54",
    "metadata": {},
+   "outputs": [],
    "source": [
     "pl = visualize(bvp.solution, show_plot=False, show_mesh=False, show_scalar_bar=False, actor_kwargs={'show_glyph': False}) # do not show mesh & glyph (arrows)\n",
     "pl.add_mesh_clip_plane(pl.meshes[0], invert=True, assign_to_axis='y') # then retrieve the mesh and add a clip plane. Assign to one axis (can be deactivated)\n",
     "pl.add_title('Displacement field\\n(with clip plane)', font_size=10)\n",
     "pl.show()"
-   ],
-   "outputs": [],
-   "execution_count": null
+   ]
   }
  ],
  "metadata": {
diff --git a/tutorials/bvpy_tutorial_9_gmsh.ipynb b/tutorials/bvpy_tutorial_9_gmsh.ipynb
index 83dcf7d6e80db3179c0532f37d9a11300d68b4dd..a663263dceef6c52ecf4df7cbce943f9f4bffeb4 100644
--- a/tutorials/bvpy_tutorial_9_gmsh.ipynb
+++ b/tutorials/bvpy_tutorial_9_gmsh.ipynb
@@ -21,7 +21,10 @@
     "\n",
     "> https://koehlerson.github.io/gmsh.jl/dev/examples/t1/\n",
     "\n",
-    "> https://gmsh.info/doc/texinfo/gmsh.html#Gmsh-tutorial"
+    "> https://gmsh.info/doc/texinfo/gmsh.html#Gmsh-tutorial\n",
+    "\n",
+    "\n",
+    "Download the tutorial notebooks and accompanying data as a ZIP file: [tutorials.zip](https://mosaic.gitlabpages.inria.fr/bvpy/tutorials.zip)."
    ]
   },
   {
diff --git a/data/example_domain_curved_cellularized_tissue.ply b/tutorials/tutorial_data/domains/example_domain_curved_cellularized_tissue.ply
similarity index 100%
rename from data/example_domain_curved_cellularized_tissue.ply
rename to tutorials/tutorial_data/domains/example_domain_curved_cellularized_tissue.ply
diff --git a/data/example_domain_sepal_2D_slice.ply b/tutorials/tutorial_data/domains/example_domain_sepal_2D_slice.ply
similarity index 100%
rename from data/example_domain_sepal_2D_slice.ply
rename to tutorials/tutorial_data/domains/example_domain_sepal_2D_slice.ply
diff --git a/data/example_meristem.ply b/tutorials/tutorial_data/domains/example_meristem.ply
similarity index 100%
rename from data/example_meristem.ply
rename to tutorials/tutorial_data/domains/example_meristem.ply
diff --git a/data/tissue_example_curved_big.txt b/tutorials/tutorial_data/domains/tissue_example_curved_big.txt
similarity index 100%
rename from data/tissue_example_curved_big.txt
rename to tutorials/tutorial_data/domains/tissue_example_curved_big.txt
diff --git a/data/tutorial_results/tuto_5_result_1_paraview_snapshot.gif.png b/tutorials/tutorial_data/results/tuto_5_result_1_paraview_snapshot.gif.png
similarity index 100%
rename from data/tutorial_results/tuto_5_result_1_paraview_snapshot.gif.png
rename to tutorials/tutorial_data/results/tuto_5_result_1_paraview_snapshot.gif.png
diff --git a/data/tutorial_results/tuto_5_result_2_paraview_snapshot.gif.png b/tutorials/tutorial_data/results/tuto_5_result_2_paraview_snapshot.gif.png
similarity index 100%
rename from data/tutorial_results/tuto_5_result_2_paraview_snapshot.gif.png
rename to tutorials/tutorial_data/results/tuto_5_result_2_paraview_snapshot.gif.png
diff --git a/data/tutorial_results/tuto_6_result_1_paraview_snapshot.png b/tutorials/tutorial_data/results/tuto_6_result_1_paraview_snapshot.png
similarity index 100%
rename from data/tutorial_results/tuto_6_result_1_paraview_snapshot.png
rename to tutorials/tutorial_data/results/tuto_6_result_1_paraview_snapshot.png
diff --git a/data/tutorial_results/tuto_6_result_3_paraview_snapshot.png b/tutorials/tutorial_data/results/tuto_6_result_3_paraview_snapshot.png
similarity index 100%
rename from data/tutorial_results/tuto_6_result_3_paraview_snapshot.png
rename to tutorials/tutorial_data/results/tuto_6_result_3_paraview_snapshot.png
diff --git a/data/tutorial_results/tuto_6_result_5_paraview_snapshot.png b/tutorials/tutorial_data/results/tuto_6_result_5_paraview_snapshot.png
similarity index 100%
rename from data/tutorial_results/tuto_6_result_5_paraview_snapshot.png
rename to tutorials/tutorial_data/results/tuto_6_result_5_paraview_snapshot.png
diff --git a/data/tutorial_results/tuto_7_result_3_paraview_snapshot.gif.png b/tutorials/tutorial_data/results/tuto_7_result_3_paraview_snapshot.gif.png
similarity index 100%
rename from data/tutorial_results/tuto_7_result_3_paraview_snapshot.gif.png
rename to tutorials/tutorial_data/results/tuto_7_result_3_paraview_snapshot.gif.png