diff --git a/android/app/build.gradle b/android/app/build.gradle
index 703e3f5665b3acda38b0bd01bbc10a7a2c94a6be..861e1af4157a0a584205e671f83c482e6ac54fc4 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -37,7 +37,7 @@ if (keystorePropertiesFile.exists()) {
 }
 
 android {
-    compileSdkVersion 33
+    compileSdkVersion 34
     namespace "org.benoitharrault.puissance4"
 
     defaultConfig {
diff --git a/android/gradle.properties b/android/gradle.properties
index 3e4ee4f7fc752c7bb9d11bf7ffc76f46dd8039d7..bfd8ff8463f540149e419194f582356f7aeabfed 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,5 +1,5 @@
 org.gradle.jvmargs=-Xmx1536M
 android.useAndroidX=true
 android.enableJetifier=true
-app.versionName=1.0.21
-app.versionCode=22
+app.versionName=1.1.0
+app.versionCode=23
diff --git a/assets/fonts/Nunito-Bold.ttf b/assets/fonts/Nunito-Bold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..6519feb781449ebe0015cbc74dfd9e13110fbba9
Binary files /dev/null and b/assets/fonts/Nunito-Bold.ttf differ
diff --git a/assets/fonts/Nunito-Light.ttf b/assets/fonts/Nunito-Light.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..8a0736c41cd6c2a1225d356bf274de1d0afc3497
Binary files /dev/null and b/assets/fonts/Nunito-Light.ttf differ
diff --git a/assets/fonts/Nunito-Medium.ttf b/assets/fonts/Nunito-Medium.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..88fccdc0638b6f5d6ac49d9d269dc3d518618ad1
Binary files /dev/null and b/assets/fonts/Nunito-Medium.ttf differ
diff --git a/assets/fonts/Nunito-Regular.ttf b/assets/fonts/Nunito-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..e7b8375a896ef0cd8e06730a78c84532b377e784
Binary files /dev/null and b/assets/fonts/Nunito-Regular.ttf differ
diff --git a/assets/translations/en.json b/assets/translations/en.json
new file mode 100644
index 0000000000000000000000000000000000000000..82d188f9bd55d92364d62f560e2214d7dc6d60d8
--- /dev/null
+++ b/assets/translations/en.json
@@ -0,0 +1,12 @@
+{
+  "app_name": "4 in a row",
+
+  "settings_title": "Settings",
+  "settings_label_theme": "Theme mode",
+
+  "about_title": "Informations",
+  "about_content": "4 in a row",
+  "about_version": "Version: {version}",
+
+  "": ""
+}
diff --git a/assets/translations/fr.json b/assets/translations/fr.json
new file mode 100644
index 0000000000000000000000000000000000000000..472a7d93f1e3017f5df6cd229c5a1f4a8b34ad12
--- /dev/null
+++ b/assets/translations/fr.json
@@ -0,0 +1,12 @@
+{
+  "app_name": "Puissance 4",
+
+  "settings_title": "Réglages",
+  "settings_label_theme": "Thème de couleurs",
+
+  "about_title": "Informations",
+  "about_content": "Puissance 4.",
+  "about_version": "Version : {version}",
+
+  "": ""
+}
diff --git a/assets/ui/button_back.png b/assets/ui/button_back.png
new file mode 100644
index 0000000000000000000000000000000000000000..51d7a01d171f7d7f047ecf9dee2d7ceee23b310d
Binary files /dev/null and b/assets/ui/button_back.png differ
diff --git a/assets/ui/button_delete_saved_game.png b/assets/ui/button_delete_saved_game.png
new file mode 100644
index 0000000000000000000000000000000000000000..4ca5b749c208c4b7eac2a4b141a1bd918d7cb98f
Binary files /dev/null and b/assets/ui/button_delete_saved_game.png differ
diff --git a/assets/ui/button_resume_game.png b/assets/ui/button_resume_game.png
new file mode 100644
index 0000000000000000000000000000000000000000..2fe433b7d18a39880a14e3f0af18cb75c4ccbaed
Binary files /dev/null and b/assets/ui/button_resume_game.png differ
diff --git a/assets/ui/button_start.png b/assets/ui/button_start.png
new file mode 100644
index 0000000000000000000000000000000000000000..23c7a4f670de19ffac455d6c510c3c53653a048b
Binary files /dev/null and b/assets/ui/button_start.png differ
diff --git a/assets/ui/game_draw.png b/assets/ui/game_draw.png
new file mode 100644
index 0000000000000000000000000000000000000000..047508dc7427561315ec1c3834f25ab888324c06
Binary files /dev/null and b/assets/ui/game_draw.png differ
diff --git a/assets/ui/game_fail.png b/assets/ui/game_fail.png
new file mode 100644
index 0000000000000000000000000000000000000000..047508dc7427561315ec1c3834f25ab888324c06
Binary files /dev/null and b/assets/ui/game_fail.png differ
diff --git a/assets/ui/game_win.png b/assets/ui/game_win.png
new file mode 100644
index 0000000000000000000000000000000000000000..876334279c1711b349a62131a33607eecf924eb6
Binary files /dev/null and b/assets/ui/game_win.png differ
diff --git a/assets/ui/placeholder.png b/assets/ui/placeholder.png
new file mode 100644
index 0000000000000000000000000000000000000000..814df31be6ddc4275ebe4490c79365578dbef1f0
Binary files /dev/null and b/assets/ui/placeholder.png differ
diff --git a/fastlane/metadata/android/en-US/changelogs/23.txt b/fastlane/metadata/android/en-US/changelogs/23.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5482b7fa40ca24a8240ec8e571116215a31f6f61
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/23.txt
@@ -0,0 +1 @@
+Clean / improve / update code and UI.
diff --git a/fastlane/metadata/android/fr-FR/changelogs/23.txt b/fastlane/metadata/android/fr-FR/changelogs/23.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8e88019e5d88c4e6e123ad73c0c1c0dce6e4ff70
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/23.txt
@@ -0,0 +1 @@
+Nettoyage / amélioration / mise à jour de code et de l'interface.
diff --git a/icons/build_game_icons.sh b/icons/build_game_icons.sh
deleted file mode 100755
index 218080d1eb12952690ba1e182a468f6e3cf03d65..0000000000000000000000000000000000000000
--- a/icons/build_game_icons.sh
+++ /dev/null
@@ -1,98 +0,0 @@
-#! /bin/bash
-
-# Check dependencies
-command -v inkscape >/dev/null 2>&1 || { echo >&2 "I require inkscape but it's not installed. Aborting."; exit 1; }
-command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not installed. Aborting."; exit 1; }
-command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; }
-
-CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
-BASE_DIR="$(dirname "${CURRENT_DIR}")"
-ASSETS_DIR="${BASE_DIR}/assets"
-
-OPTIPNG_OPTIONS="-preserve -quiet -o7"
-ICON_SIZE=192
-
-#######################################################
-
-# Game images
-AVAILABLE_GAME_IMAGES="
-"
-
-# Settings images
-AVAILABLES_GAME_SETTINGS="
-"
-
-#######################################################
-
-# optimize svg
-function optimize_svg() {
-  SOURCE="$1"
-
-  cp ${SOURCE} ${SOURCE}.tmp
-  scour \
-      --remove-descriptive-elements \
-      --enable-id-stripping \
-      --enable-viewboxing \
-      --enable-comment-stripping \
-      --nindent=4 \
-      --quiet \
-      -i ${SOURCE}.tmp \
-      -o ${SOURCE}
-  rm ${SOURCE}.tmp
-}
-
-# build icons
-function build_icon() {
-  SOURCE="$1"
-  TARGET="$2"
-
-  echo "Building ${TARGET}"
-
-  if [ ! -f "${SOURCE}" ]; then
-    echo "Missing file: ${SOURCE}"
-    exit 1
-  fi
-
-  optimize_svg "${SOURCE}"
-
-  inkscape \
-      --export-width=${ICON_SIZE} \
-      --export-height=${ICON_SIZE} \
-      --export-filename=${TARGET} \
-      ${SOURCE}
-
-  optipng ${OPTIPNG_OPTIONS} ${TARGET}
-}
-
-function build_settings_icons() {
-  INPUT_STRING="$1"
-
-  SETTING_NAME="$(echo "${INPUT_STRING}" | cut -d":" -f1)"
-  SETTING_VALUES="$(echo "${INPUT_STRING}" | cut -d":" -f2 | tr "," " ")"
-
-  for SETTING_VALUE in ${SETTING_VALUES}
-  do
-    SETTING_CODE="${SETTING_NAME}_${SETTING_VALUE}"
-    build_icon ${CURRENT_DIR}/${SETTING_CODE}.svg ${ASSETS_DIR}/icons/${SETTING_CODE}.png
-  done
-}
-
-#######################################################
-
-# Create output folders
-mkdir -p ${ASSETS_DIR}/icons
-
-# Delete existing generated images
-find ${ASSETS_DIR}/icons -type f -name "*.png" -delete
-
-# build game images
-for GAME_IMAGE in ${AVAILABLE_GAME_IMAGES}
-do
-  build_icon ${CURRENT_DIR}/${GAME_IMAGE}.svg ${ASSETS_DIR}/icons/${GAME_IMAGE}.png
-done
-
-# build settings images
-for GAME_SETTING in ${AVAILABLES_GAME_SETTINGS}
-do
-  build_settings_icons "${GAME_SETTING}"
-done
diff --git a/lib/board.dart b/lib/board.dart
deleted file mode 100644
index e7498f80fee7c7455f0b20e64d8d2f1bcc089053..0000000000000000000000000000000000000000
--- a/lib/board.dart
+++ /dev/null
@@ -1,151 +0,0 @@
-import 'package:puissance4/coordinate.dart';
-
-import 'package:puissance4/match_page.dart';
-
-class Board {
-  List<List<Color?>> _boxes = List.generate(
-    7,
-    (i) => List.generate(
-      7,
-      (i) => null,
-    ),
-  );
-
-  Board();
-
-  Board.from(List<List<Color?>> boxes) {
-    _boxes = boxes;
-  }
-
-  Color? getBox(Coordinate coordinate) => _boxes[coordinate.col ?? 0][coordinate.row ?? 0];
-
-  int getColumnTarget(int? col) => _boxes[col ?? 0].lastIndexOf(null);
-
-  void setBox(Coordinate coordinate, Color? player) =>
-      _boxes[coordinate.col ?? 0][coordinate.row ?? 0] = player;
-
-  bool checkWinner(Coordinate coordinate, Color? player) {
-    return checkHorizontally(coordinate, player) ||
-        checkVertically(coordinate, player) ||
-        checkDiagonally(coordinate, player);
-  }
-
-  bool checkHorizontally(Coordinate coordinate, Color? player) {
-    var r = 0;
-    for (;
-        (coordinate.col ?? 0) + r < 7 &&
-            r < 4 &&
-            getBox(coordinate.copyWith(col: (coordinate.col ?? 0) + r)) == player;
-        ++r) {}
-    if (r >= 4) {
-      return true;
-    }
-
-    var l = 0;
-    for (;
-        (coordinate.col ?? 0) - l >= 0 &&
-            l < 4 &&
-            getBox(coordinate.copyWith(col: (coordinate.col ?? 0) - l)) == player;
-        ++l) {}
-    if (l >= 4 || l + r >= 5) {
-      return true;
-    }
-
-    return false;
-  }
-
-  bool checkDiagonally(Coordinate coordinate, Color? player) {
-    var ur = 0;
-    for (;
-        (coordinate.col ?? 0) + ur < 7 &&
-            (coordinate.row ?? 0) + ur < 7 &&
-            ur < 4 &&
-            getBox(coordinate.copyWith(
-                  col: (coordinate.col ?? 0) + ur,
-                  row: (coordinate.row ?? 0) + ur,
-                )) ==
-                player;
-        ++ur) {}
-    if (ur >= 4) {
-      return true;
-    }
-    var dl = 0;
-    for (;
-        (coordinate.col ?? 0) - dl >= 0 &&
-            (coordinate.row ?? 0) - dl >= 0 &&
-            dl < 4 &&
-            getBox(coordinate.copyWith(
-                  col: (coordinate.col ?? 0) - dl,
-                  row: (coordinate.row ?? 0) - dl,
-                )) ==
-                player;
-        ++dl) {}
-    if (dl >= 4 || dl + ur >= 5) {
-      return true;
-    }
-
-    var dr = 0;
-    for (;
-        (coordinate.col ?? 0) + dr < 7 &&
-            (coordinate.row ?? 0) - dr >= 0 &&
-            dr < 4 &&
-            getBox(coordinate.copyWith(
-                  col: (coordinate.col ?? 0) + dr,
-                  row: (coordinate.row ?? 0) - dr,
-                )) ==
-                player;
-        ++dr) {}
-    if (dr >= 4) {
-      return true;
-    }
-
-    var ul = 0;
-    for (;
-        (coordinate.col ?? 0) - ul >= 0 &&
-            (coordinate.row ?? 0) + ul < 7 &&
-            ul < 4 &&
-            getBox(coordinate.copyWith(
-                  col: (coordinate.col ?? 0) - ul,
-                  row: (coordinate.row ?? 0) + ul,
-                )) ==
-                player;
-        ++ul) {}
-    if (ul >= 4 || dr + ul >= 5) {
-      return true;
-    }
-    return false;
-  }
-
-  bool checkVertically(Coordinate coordinate, Color? player) {
-    var u = 0;
-    for (;
-        (coordinate.row ?? 0) + u < 7 &&
-            u < 4 &&
-            getBox(coordinate.copyWith(
-                  row: (coordinate.row ?? 0) + u,
-                )) ==
-                player;
-        ++u) {}
-    if (u >= 4) {
-      return true;
-    }
-    var d = 0;
-    for (;
-        (coordinate.row ?? 0) - d >= 0 &&
-            d < 4 &&
-            getBox(coordinate.copyWith(
-                  row: (coordinate.row ?? 0) - d,
-                )) ==
-                player;
-        ++d) {}
-    if (d >= 4 || d + u >= 5) {
-      return true;
-    }
-
-    return false;
-  }
-
-  Board clone() {
-    return Board.from(_boxes.map((c) => c.map((b) => b).toList()).toList());
-  }
-}
diff --git a/lib/config/default_game_settings.dart b/lib/config/default_game_settings.dart
new file mode 100644
index 0000000000000000000000000000000000000000..63bfd58548fe86abdac1cc6850a5ec631da5d76a
--- /dev/null
+++ b/lib/config/default_game_settings.dart
@@ -0,0 +1,46 @@
+import 'package:puissance4/utils/tools.dart';
+
+class DefaultGameSettings {
+  // available game parameters codes
+  static const String parameterCodeGameMode = 'gameMode';
+  static const String parameterCodeSize = 'size';
+  static const List<String> availableParameters = [
+    parameterCodeGameMode,
+    parameterCodeSize,
+  ];
+
+  // game mode: available values
+  static const String gameModeHumanVsHuman = 'human-vs-human';
+  static const String gameModeHumanVsRobot = 'human-vs-robot';
+  static const String gameModeRobotVsHuman = 'robot-vs-human';
+  static const String gameModeRobotVsRobot = 'robot-vs-robot';
+  static const List<String> allowedGameModeValues = [
+    gameModeHumanVsHuman,
+    gameModeHumanVsRobot,
+    gameModeRobotVsHuman,
+    gameModeRobotVsRobot,
+  ];
+  // game mode: default value
+  static const String defaultGameModeValue = gameModeHumanVsHuman;
+
+  // size: available values
+  static const String sizeValueMedium = '7x6';
+  static const List<String> allowedSizeValues = [
+    sizeValueMedium,
+  ];
+  // size: default value
+  static const String defaultSizeValue = sizeValueMedium;
+
+  // available values from parameter code
+  static List<String> getAvailableValues(String parameterCode) {
+    switch (parameterCode) {
+      case parameterCodeGameMode:
+        return DefaultGameSettings.allowedGameModeValues;
+      case parameterCodeSize:
+        return DefaultGameSettings.allowedSizeValues;
+    }
+
+    printlog('Did not find any available value for game parameter "$parameterCode".');
+    return [];
+  }
+}
diff --git a/lib/config/default_global_settings.dart b/lib/config/default_global_settings.dart
new file mode 100644
index 0000000000000000000000000000000000000000..bea2f7b57c479dc143dc521184f9e4285349652d
--- /dev/null
+++ b/lib/config/default_global_settings.dart
@@ -0,0 +1,28 @@
+import 'package:puissance4/utils/tools.dart';
+
+class DefaultGlobalSettings {
+  // available global parameters codes
+  static const String parameterCodeSkin = 'skin';
+  static const List<String> availableParameters = [
+    parameterCodeSkin,
+  ];
+
+  // skin: available values
+  static const String skinValueDefault = 'default';
+  static const List<String> allowedSkinValues = [
+    skinValueDefault,
+  ];
+  // skin: default value
+  static const String defaultSkinValue = skinValueDefault;
+
+  // available values from parameter code
+  static List<String> getAvailableValues(String parameterCode) {
+    switch (parameterCode) {
+      case parameterCodeSkin:
+        return DefaultGlobalSettings.allowedSkinValues;
+    }
+
+    printlog('Did not find any available value for global parameter "$parameterCode".');
+    return [];
+  }
+}
diff --git a/lib/config/menu.dart b/lib/config/menu.dart
new file mode 100644
index 0000000000000000000000000000000000000000..b9acab474c2dca97261b9227b0115b174977e3a5
--- /dev/null
+++ b/lib/config/menu.dart
@@ -0,0 +1,52 @@
+import 'package:flutter/material.dart';
+import 'package:unicons/unicons.dart';
+
+import 'package:puissance4/ui/screens/page_about.dart';
+import 'package:puissance4/ui/screens/page_game.dart';
+import 'package:puissance4/ui/screens/page_settings.dart';
+
+class MenuItem {
+  final Icon icon;
+  final Widget page;
+
+  const MenuItem({
+    required this.icon,
+    required this.page,
+  });
+}
+
+class Menu {
+  static const indexGame = 0;
+  static const menuItemGame = MenuItem(
+    icon: Icon(UniconsLine.home),
+    page: PageGame(),
+  );
+
+  static const indexSettings = 1;
+  static const menuItemSettings = MenuItem(
+    icon: Icon(UniconsLine.setting),
+    page: PageSettings(),
+  );
+
+  static const indexAbout = 2;
+  static const menuItemAbout = MenuItem(
+    icon: Icon(UniconsLine.info_circle),
+    page: PageAbout(),
+  );
+
+  static Map<int, MenuItem> items = {
+    indexGame: menuItemGame,
+    indexSettings: menuItemSettings,
+    indexAbout: menuItemAbout,
+  };
+
+  static bool isIndexAllowed(int pageIndex) {
+    return items.keys.contains(pageIndex);
+  }
+
+  static Widget getPageWidget(int pageIndex) {
+    return items[pageIndex]?.page ?? menuItemGame.page;
+  }
+
+  static int itemsCount = Menu.items.length;
+}
diff --git a/lib/config/theme.dart b/lib/config/theme.dart
new file mode 100644
index 0000000000000000000000000000000000000000..74f532fd5abf693979118609564d29167e902009
--- /dev/null
+++ b/lib/config/theme.dart
@@ -0,0 +1,190 @@
+import 'package:flutter/material.dart';
+
+/// Colors from Tailwind CSS (v3.0) - June 2022
+///
+/// https://tailwindcss.com/docs/customizing-colors
+
+const int _primaryColor = 0xFF6366F1;
+const MaterialColor primarySwatch = MaterialColor(_primaryColor, <int, Color>{
+  50: Color(0xFFEEF2FF), // indigo-50
+  100: Color(0xFFE0E7FF), // indigo-100
+  200: Color(0xFFC7D2FE), // indigo-200
+  300: Color(0xFFA5B4FC), // indigo-300
+  400: Color(0xFF818CF8), // indigo-400
+  500: Color(_primaryColor), // indigo-500
+  600: Color(0xFF4F46E5), // indigo-600
+  700: Color(0xFF4338CA), // indigo-700
+  800: Color(0xFF3730A3), // indigo-800
+  900: Color(0xFF312E81), // indigo-900
+});
+
+const int _textColor = 0xFF64748B;
+const MaterialColor textSwatch = MaterialColor(_textColor, <int, Color>{
+  50: Color(0xFFF8FAFC), // slate-50
+  100: Color(0xFFF1F5F9), // slate-100
+  200: Color(0xFFE2E8F0), // slate-200
+  300: Color(0xFFCBD5E1), // slate-300
+  400: Color(0xFF94A3B8), // slate-400
+  500: Color(_textColor), // slate-500
+  600: Color(0xFF475569), // slate-600
+  700: Color(0xFF334155), // slate-700
+  800: Color(0xFF1E293B), // slate-800
+  900: Color(0xFF0F172A), // slate-900
+});
+
+const Color errorColor = Color(0xFFDC2626); // red-600
+
+final ColorScheme lightColorScheme = ColorScheme.light(
+  primary: primarySwatch.shade500,
+  secondary: primarySwatch.shade500,
+  onSecondary: Colors.white,
+  error: errorColor,
+  onSurface: textSwatch.shade500,
+  surface: textSwatch.shade50,
+  surfaceContainerHighest: Colors.white,
+  shadow: textSwatch.shade900.withOpacity(.1),
+);
+
+final ColorScheme darkColorScheme = ColorScheme.dark(
+  primary: primarySwatch.shade500,
+  secondary: primarySwatch.shade500,
+  onSecondary: Colors.white,
+  error: errorColor,
+  onSurface: textSwatch.shade300,
+  surface: const Color(0xFF262630),
+  surfaceContainerHighest: const Color(0xFF282832),
+  shadow: textSwatch.shade900.withOpacity(.2),
+);
+
+final ThemeData lightTheme = ThemeData(
+  colorScheme: lightColorScheme,
+  fontFamily: 'Nunito',
+  textTheme: TextTheme(
+    displayLarge: TextStyle(
+      color: textSwatch.shade700,
+      fontFamily: 'Nunito',
+    ),
+    displayMedium: TextStyle(
+      color: textSwatch.shade600,
+      fontFamily: 'Nunito',
+    ),
+    displaySmall: TextStyle(
+      color: textSwatch.shade500,
+      fontFamily: 'Nunito',
+    ),
+    headlineLarge: TextStyle(
+      color: textSwatch.shade700,
+      fontFamily: 'Nunito',
+    ),
+    headlineMedium: TextStyle(
+      color: textSwatch.shade600,
+      fontFamily: 'Nunito',
+    ),
+    headlineSmall: TextStyle(
+      color: textSwatch.shade500,
+      fontFamily: 'Nunito',
+    ),
+    titleLarge: TextStyle(
+      color: textSwatch.shade700,
+      fontFamily: 'Nunito',
+    ),
+    titleMedium: TextStyle(
+      color: textSwatch.shade600,
+      fontFamily: 'Nunito',
+    ),
+    titleSmall: TextStyle(
+      color: textSwatch.shade500,
+      fontFamily: 'Nunito',
+    ),
+    bodyLarge: TextStyle(
+      color: textSwatch.shade700,
+      fontFamily: 'Nunito',
+    ),
+    bodyMedium: TextStyle(
+      color: textSwatch.shade600,
+      fontFamily: 'Nunito',
+    ),
+    bodySmall: TextStyle(
+      color: textSwatch.shade500,
+      fontFamily: 'Nunito',
+    ),
+    labelLarge: TextStyle(
+      color: textSwatch.shade700,
+      fontFamily: 'Nunito',
+    ),
+    labelMedium: TextStyle(
+      color: textSwatch.shade600,
+      fontFamily: 'Nunito',
+    ),
+    labelSmall: TextStyle(
+      color: textSwatch.shade500,
+      fontFamily: 'Nunito',
+    ),
+  ),
+);
+
+final ThemeData darkTheme = lightTheme.copyWith(
+  colorScheme: darkColorScheme,
+  textTheme: TextTheme(
+    displayLarge: TextStyle(
+      color: textSwatch.shade200,
+      fontFamily: 'Nunito',
+    ),
+    displayMedium: TextStyle(
+      color: textSwatch.shade300,
+      fontFamily: 'Nunito',
+    ),
+    displaySmall: TextStyle(
+      color: textSwatch.shade400,
+      fontFamily: 'Nunito',
+    ),
+    headlineLarge: TextStyle(
+      color: textSwatch.shade200,
+      fontFamily: 'Nunito',
+    ),
+    headlineMedium: TextStyle(
+      color: textSwatch.shade300,
+      fontFamily: 'Nunito',
+    ),
+    headlineSmall: TextStyle(
+      color: textSwatch.shade400,
+      fontFamily: 'Nunito',
+    ),
+    titleLarge: TextStyle(
+      color: textSwatch.shade200,
+      fontFamily: 'Nunito',
+    ),
+    titleMedium: TextStyle(
+      color: textSwatch.shade300,
+      fontFamily: 'Nunito',
+    ),
+    titleSmall: TextStyle(
+      color: textSwatch.shade400,
+      fontFamily: 'Nunito',
+    ),
+    bodyLarge: TextStyle(
+      color: textSwatch.shade200,
+      fontFamily: 'Nunito',
+    ),
+    bodyMedium: TextStyle(
+      color: textSwatch.shade300,
+      fontFamily: 'Nunito',
+    ),
+    bodySmall: TextStyle(
+      color: textSwatch.shade400,
+      fontFamily: 'Nunito',
+    ),
+    labelLarge: TextStyle(
+      color: textSwatch.shade200,
+      fontFamily: 'Nunito',
+    ),
+    labelMedium: TextStyle(
+      color: textSwatch.shade300,
+      fontFamily: 'Nunito',
+    ),
+    labelSmall: TextStyle(
+      color: textSwatch.shade400,
+      fontFamily: 'Nunito',
+    ),
+  ),
+);
diff --git a/lib/coordinate.dart b/lib/coordinate.dart
deleted file mode 100644
index 3d137c74442be8a8baf6d4d9c26e06edb46d6040..0000000000000000000000000000000000000000
--- a/lib/coordinate.dart
+++ /dev/null
@@ -1,17 +0,0 @@
-class Coordinate {
-  final int? row, col;
-
-  Coordinate(
-    this.col,
-    this.row,
-  );
-
-  Coordinate copyWith({
-    int? col,
-    int? row,
-  }) =>
-      Coordinate(
-        col ?? this.col,
-        row ?? this.row,
-      );
-}
diff --git a/lib/cpu.dart b/lib/cpu.dart
deleted file mode 100644
index 1f7ed4bf249747530b435220eb60d56f68b562d5..0000000000000000000000000000000000000000
--- a/lib/cpu.dart
+++ /dev/null
@@ -1,117 +0,0 @@
-import 'dart:math';
-
-import 'package:puissance4/board.dart';
-import 'package:puissance4/coordinate.dart';
-import 'package:puissance4/match_page.dart';
-
-abstract class Cpu {
-  final Color color;
-  final Random _random = Random(DateTime.now().millisecond);
-
-  Cpu(this.color);
-
-  Color get otherPlayer => color == Color.red ? Color.yellow : Color.red;
-
-  Future<int> chooseCol(Board board);
-}
-
-class DumbCpu extends Cpu {
-  DumbCpu(super.player);
-
-  @override
-  Color get otherPlayer => color == Color.red ? Color.yellow : Color.red;
-
-  @override
-  Future<int> chooseCol(Board board) async {
-    await Future.delayed(Duration(seconds: _random.nextInt(2)));
-    int col = _random.nextInt(7);
-
-    return col;
-  }
-
-  @override
-  String toString() => 'DUMB CPU';
-}
-
-class HarderCpu extends Cpu {
-  HarderCpu(super.player);
-
-  @override
-  Future<int> chooseCol(Board board) async {
-    final List<double?> scores = List.filled(7, 0.0);
-
-    await Future.delayed(Duration(seconds: 1 + _random.nextInt(2)));
-    return _compute(board, 0, 1, scores);
-  }
-
-  int _compute(Board board, int step, int deepness, List<double?> scores) {
-    for (var i = 0; i < 7; ++i) {
-      final boardCopy = board.clone();
-
-      final target = boardCopy.getColumnTarget(i);
-      if (target == -1) {
-        scores[i] = null;
-        continue;
-      }
-
-      final coordinate = Coordinate(i, target);
-
-      boardCopy.setBox(coordinate, color);
-      if (boardCopy.checkWinner(coordinate, color)) {
-        scores[i] = (scores[i] ?? 0) + deepness / (step + 1);
-        continue;
-      }
-
-      for (var j = 0; j < 7; ++j) {
-        final target = boardCopy.getColumnTarget(j);
-        if (target == -1) {
-          continue;
-        }
-
-        final coordinate = Coordinate(j, target);
-
-        boardCopy.setBox(coordinate, otherPlayer);
-        if (boardCopy.checkWinner(coordinate, otherPlayer)) {
-          scores[i] = (scores[i] ?? 0) - deepness / (step + 1);
-          continue;
-        }
-
-        if (step + 1 < deepness) {
-          _compute(board, step + 1, deepness, scores);
-        }
-      }
-    }
-
-    return _getBestScoreIndex(scores);
-  }
-
-  int _getBestScoreIndex(List<double?> scores) {
-    int bestScoreIndex = scores.indexWhere((s) => s != null);
-    scores.asMap().forEach((index, score) {
-      if (score != null &&
-          (score > (scores[bestScoreIndex] ?? 0) ||
-              (score == scores[bestScoreIndex] && _random.nextBool()))) {
-        bestScoreIndex = index;
-      }
-    });
-    return bestScoreIndex;
-  }
-
-  @override
-  String toString() => 'HARDER CPU';
-}
-
-class HardestCpu extends HarderCpu {
-  HardestCpu(super.player);
-
-  @override
-  Future<int> chooseCol(Board board) async {
-    final List<double?> scores = List.filled(7, 0);
-
-    await Future.delayed(Duration(seconds: 2 + _random.nextInt(2)));
-    return _compute(board, 0, 4, scores);
-  }
-
-  @override
-  String toString() => 'HARDEST CPU';
-}
diff --git a/lib/cpu_level_page.dart b/lib/cpu_level_page.dart
deleted file mode 100644
index 21074f4934db9024b041a9d8dd44f7558be3c273..0000000000000000000000000000000000000000
--- a/lib/cpu_level_page.dart
+++ /dev/null
@@ -1,92 +0,0 @@
-import 'dart:math';
-
-import 'package:flutter/material.dart';
-
-import 'package:puissance4/cpu.dart';
-import 'package:puissance4/match_page.dart';
-
-class CpuLevelPage extends StatelessWidget {
-  const CpuLevelPage({super.key});
-
-  @override
-  Widget build(BuildContext context) {
-    return Scaffold(
-      appBar: AppBar(
-        elevation: 0,
-      ),
-      backgroundColor: Colors.blue,
-      body: Center(
-        child: Column(
-          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
-          crossAxisAlignment: CrossAxisAlignment.center,
-          mainAxisSize: MainAxisSize.max,
-          children: <Widget>[
-            TextButton(
-              style: TextButton.styleFrom(
-                backgroundColor: Colors.yellow,
-                padding: const EdgeInsets.all(15),
-              ),
-              child: Text(
-                '☺️ FACILE',
-                style:
-                    Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.black),
-              ),
-              onPressed: () {
-                Navigator.pushNamed(
-                  context,
-                  '/match',
-                  arguments: {
-                    'mode': Mode.pvc,
-                    'cpu': DumbCpu(Random().nextBool() ? Color.red : Color.yellow),
-                  },
-                );
-              },
-            ),
-            TextButton(
-              style: TextButton.styleFrom(
-                backgroundColor: Colors.red,
-                padding: const EdgeInsets.all(15),
-              ),
-              child: Text(
-                '🤔 DIFFICILE',
-                style:
-                    Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white),
-              ),
-              onPressed: () {
-                Navigator.pushNamed(
-                  context,
-                  '/match',
-                  arguments: {
-                    'mode': Mode.pvc,
-                    'cpu': HarderCpu(Random().nextBool() ? Color.red : Color.yellow),
-                  },
-                );
-              },
-            ),
-            TextButton(
-              style: TextButton.styleFrom(
-                backgroundColor: Colors.deepPurpleAccent,
-                padding: const EdgeInsets.all(15),
-              ),
-              child: Text(
-                '🤯 TRES DIFFICILE',
-                style:
-                    Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white),
-              ),
-              onPressed: () {
-                Navigator.pushNamed(
-                  context,
-                  '/match',
-                  arguments: {
-                    'mode': Mode.pvc,
-                    'cpu': HardestCpu(Random().nextBool() ? Color.red : Color.yellow),
-                  },
-                );
-              },
-            ),
-          ],
-        ),
-      ),
-    );
-  }
-}
diff --git a/lib/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart
new file mode 100644
index 0000000000000000000000000000000000000000..7a08295655a55cd0bd63ac0905bf61cbaa0cd374
--- /dev/null
+++ b/lib/cubit/game_cubit.dart
@@ -0,0 +1,177 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:puissance4/models/game/game.dart';
+import 'package:puissance4/models/settings/settings_game.dart';
+import 'package:puissance4/models/settings/settings_global.dart';
+import 'package:puissance4/robot/robot_player.dart';
+import 'package:puissance4/utils/tools.dart';
+
+part 'game_state.dart';
+
+class GameCubit extends HydratedCubit<GameState> {
+  GameCubit()
+      : super(GameState(
+          currentGame: Game.createNull(),
+        ));
+
+  void updateState(Game game) {
+    emit(GameState(
+      currentGame: game,
+    ));
+  }
+
+  void refresh() {
+    final Game game = Game(
+      // Settings
+      gameSettings: state.currentGame.gameSettings,
+      globalSettings: state.currentGame.globalSettings,
+      // State
+      isRunning: state.currentGame.isRunning,
+      isStarted: state.currentGame.isStarted,
+      isFinished: state.currentGame.isFinished,
+      animationInProgress: state.currentGame.animationInProgress,
+      // Base data
+      board: state.currentGame.board,
+      sizeHorizontal: state.currentGame.sizeHorizontal,
+      sizeVertical: state.currentGame.sizeVertical,
+      // Game data
+      currentPlayer: state.currentGame.currentPlayer,
+    );
+    // game.dump();
+
+    updateState(game);
+  }
+
+  void startNewGame({
+    required GameSettings gameSettings,
+    required GlobalSettings globalSettings,
+  }) {
+    final Game newGame = Game.createNew(
+      // Settings
+      gameSettings: gameSettings,
+      globalSettings: globalSettings,
+    );
+
+    newGame.dump();
+
+    updateState(newGame);
+    refresh();
+
+    robotPlay();
+  }
+
+  void quitGame() {
+    state.currentGame.isRunning = false;
+    refresh();
+  }
+
+  void resumeSavedGame() {
+    state.currentGame.isRunning = true;
+    refresh();
+  }
+
+  void deleteSavedGame() {
+    state.currentGame.isRunning = false;
+    state.currentGame.isFinished = true;
+    refresh();
+  }
+
+  void toggleCurrentPlayer() {
+    state.currentGame.currentPlayer = 3 - state.currentGame.currentPlayer;
+    refresh();
+
+    robotPlay();
+  }
+
+  void robotPlay() async {
+    if (!state.currentGame.isFinished && !state.currentGame.isCurrentPlayerHuman()) {
+      final int? pickedCell = RobotPlayer.pickColumn(state.currentGame);
+      await Future.delayed(const Duration(milliseconds: 500));
+
+      if (pickedCell != null) {
+        tapOnColumn(col: pickedCell);
+      }
+    }
+  }
+
+  void tapOnColumn({required int col}) async {
+    printlog('tap on column: $col');
+
+    if (!state.currentGame.isMoveAllowed(col: col)) {
+      printlog('not allowed');
+      return;
+    }
+
+    state.currentGame.animationInProgress = true;
+    refresh();
+
+    await dropCoin(col: col);
+
+    if (state.currentGame.connected4()) {
+      printlog('user connected 4');
+      state.currentGame.isFinished = true;
+      refresh();
+    }
+    if (!state.currentGame.canPlay()) {
+      printlog('user has no more move to play');
+      state.currentGame.isFinished = true;
+      refresh();
+    }
+
+    state.currentGame.animationInProgress = false;
+    if (state.currentGame.isFinished == false) {
+      toggleCurrentPlayer();
+    }
+
+    refresh();
+  }
+
+  // put coin on top cell
+  // and make it fall until bottom or other coin
+  Future<void> dropCoin({required int col}) async {
+    int currentRow = 0;
+
+    do {
+      if (currentRow > 0) {
+        state.currentGame.board.setCellValue(
+          row: currentRow - 1,
+          col: col,
+          value: 0,
+        );
+      }
+      state.currentGame.board.setCellValue(
+        row: currentRow++,
+        col: col,
+        value: state.currentGame.currentPlayer,
+      );
+      refresh();
+
+      await Future.delayed(const Duration(milliseconds: 50));
+    } while (currentRow < state.currentGame.sizeVertical &&
+        state.currentGame.board.getCellValue(
+              row: currentRow,
+              col: col,
+            ) ==
+            0);
+
+    refresh();
+  }
+
+  @override
+  GameState? fromJson(Map<String, dynamic> json) {
+    final Game currentGame = json['currentGame'] as Game;
+
+    return GameState(
+      currentGame: currentGame,
+    );
+  }
+
+  @override
+  Map<String, dynamic>? toJson(GameState state) {
+    return <String, dynamic>{
+      'currentGame': state.currentGame.toJson(),
+    };
+  }
+}
diff --git a/lib/cubit/game_state.dart b/lib/cubit/game_state.dart
new file mode 100644
index 0000000000000000000000000000000000000000..00e211668c3269255926939324355792abd61c41
--- /dev/null
+++ b/lib/cubit/game_state.dart
@@ -0,0 +1,15 @@
+part of 'game_cubit.dart';
+
+@immutable
+class GameState extends Equatable {
+  const GameState({
+    required this.currentGame,
+  });
+
+  final Game currentGame;
+
+  @override
+  List<dynamic> get props => <dynamic>[
+        currentGame,
+      ];
+}
diff --git a/lib/cubit/nav_cubit.dart b/lib/cubit/nav_cubit.dart
new file mode 100644
index 0000000000000000000000000000000000000000..89b3365ea089cfdf984ee0650818dad72d788f59
--- /dev/null
+++ b/lib/cubit/nav_cubit.dart
@@ -0,0 +1,37 @@
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:puissance4/config/menu.dart';
+
+class NavCubit extends HydratedCubit<int> {
+  NavCubit() : super(0);
+
+  void updateIndex(int index) {
+    if (Menu.isIndexAllowed(index)) {
+      emit(index);
+    } else {
+      goToGamePage();
+    }
+  }
+
+  void goToGamePage() {
+    emit(Menu.indexGame);
+  }
+
+  void goToSettingsPage() {
+    emit(Menu.indexSettings);
+  }
+
+  void goToAboutPage() {
+    emit(Menu.indexAbout);
+  }
+
+  @override
+  int fromJson(Map<String, dynamic> json) {
+    return Menu.indexGame;
+  }
+
+  @override
+  Map<String, dynamic>? toJson(int state) {
+    return <String, int>{'pageIndex': state};
+  }
+}
diff --git a/lib/cubit/settings_game_cubit.dart b/lib/cubit/settings_game_cubit.dart
new file mode 100644
index 0000000000000000000000000000000000000000..54fdfe01c809f523ebd262933c773214f29e51a1
--- /dev/null
+++ b/lib/cubit/settings_game_cubit.dart
@@ -0,0 +1,72 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:puissance4/config/default_game_settings.dart';
+import 'package:puissance4/models/settings/settings_game.dart';
+
+part 'settings_game_state.dart';
+
+class GameSettingsCubit extends HydratedCubit<GameSettingsState> {
+  GameSettingsCubit() : super(GameSettingsState(settings: GameSettings.createDefault()));
+
+  void setValues({
+    String? gameMode,
+    String? size,
+  }) {
+    emit(
+      GameSettingsState(
+        settings: GameSettings(
+          gameMode: gameMode ?? state.settings.gameMode,
+          size: size ?? state.settings.size,
+        ),
+      ),
+    );
+  }
+
+  String getParameterValue(String code) {
+    switch (code) {
+      case DefaultGameSettings.parameterCodeGameMode:
+        return GameSettings.getGameModeValueFromUnsafe(state.settings.gameMode);
+      case DefaultGameSettings.parameterCodeSize:
+        return GameSettings.getSizeValueFromUnsafe(state.settings.size);
+    }
+
+    return '';
+  }
+
+  void setParameterValue(String code, String value) {
+    final String gameMode = code == DefaultGameSettings.parameterCodeGameMode
+        ? value
+        : getParameterValue(DefaultGameSettings.parameterCodeGameMode);
+    final String size = code == DefaultGameSettings.parameterCodeSize
+        ? value
+        : getParameterValue(DefaultGameSettings.parameterCodeSize);
+
+    setValues(
+      gameMode: gameMode,
+      size: size,
+    );
+  }
+
+  @override
+  GameSettingsState? fromJson(Map<String, dynamic> json) {
+    final String gameMode = json[DefaultGameSettings.parameterCodeGameMode] as String;
+    final String size = json[DefaultGameSettings.parameterCodeSize] as String;
+
+    return GameSettingsState(
+      settings: GameSettings(
+        gameMode: gameMode,
+        size: size,
+      ),
+    );
+  }
+
+  @override
+  Map<String, dynamic>? toJson(GameSettingsState state) {
+    return <String, dynamic>{
+      DefaultGameSettings.parameterCodeGameMode: state.settings.gameMode,
+      DefaultGameSettings.parameterCodeSize: state.settings.size,
+    };
+  }
+}
diff --git a/lib/cubit/settings_game_state.dart b/lib/cubit/settings_game_state.dart
new file mode 100644
index 0000000000000000000000000000000000000000..5acd85b44ba541e1c5e9c26af1c4be26a385b9ed
--- /dev/null
+++ b/lib/cubit/settings_game_state.dart
@@ -0,0 +1,15 @@
+part of 'settings_game_cubit.dart';
+
+@immutable
+class GameSettingsState extends Equatable {
+  const GameSettingsState({
+    required this.settings,
+  });
+
+  final GameSettings settings;
+
+  @override
+  List<dynamic> get props => <dynamic>[
+        settings,
+      ];
+}
diff --git a/lib/cubit/settings_global_cubit.dart b/lib/cubit/settings_global_cubit.dart
new file mode 100644
index 0000000000000000000000000000000000000000..47bb401c83ef145baecb90f4c95d29e0d38f2219
--- /dev/null
+++ b/lib/cubit/settings_global_cubit.dart
@@ -0,0 +1,60 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:puissance4/config/default_global_settings.dart';
+import 'package:puissance4/models/settings/settings_global.dart';
+
+part 'settings_global_state.dart';
+
+class GlobalSettingsCubit extends HydratedCubit<GlobalSettingsState> {
+  GlobalSettingsCubit() : super(GlobalSettingsState(settings: GlobalSettings.createDefault()));
+
+  void setValues({
+    String? skin,
+  }) {
+    emit(
+      GlobalSettingsState(
+        settings: GlobalSettings(
+          skin: skin ?? state.settings.skin,
+        ),
+      ),
+    );
+  }
+
+  String getParameterValue(String code) {
+    switch (code) {
+      case DefaultGlobalSettings.parameterCodeSkin:
+        return GlobalSettings.getSkinValueFromUnsafe(state.settings.skin);
+    }
+    return '';
+  }
+
+  void setParameterValue(String code, String value) {
+    final String skin = (code == DefaultGlobalSettings.parameterCodeSkin)
+        ? value
+        : getParameterValue(DefaultGlobalSettings.parameterCodeSkin);
+
+    setValues(
+      skin: skin,
+    );
+  }
+
+  @override
+  GlobalSettingsState? fromJson(Map<String, dynamic> json) {
+    final String skin = json[DefaultGlobalSettings.parameterCodeSkin] as String;
+
+    return GlobalSettingsState(
+      settings: GlobalSettings(
+        skin: skin,
+      ),
+    );
+  }
+
+  @override
+  Map<String, dynamic>? toJson(GlobalSettingsState state) {
+    return <String, dynamic>{
+      DefaultGlobalSettings.parameterCodeSkin: state.settings.skin,
+    };
+  }
+}
diff --git a/lib/cubit/settings_global_state.dart b/lib/cubit/settings_global_state.dart
new file mode 100644
index 0000000000000000000000000000000000000000..ebcddd700f252257223ca8e16c85202b04f3ff24
--- /dev/null
+++ b/lib/cubit/settings_global_state.dart
@@ -0,0 +1,15 @@
+part of 'settings_global_cubit.dart';
+
+@immutable
+class GlobalSettingsState extends Equatable {
+  const GlobalSettingsState({
+    required this.settings,
+  });
+
+  final GlobalSettings settings;
+
+  @override
+  List<dynamic> get props => <dynamic>[
+        settings,
+      ];
+}
diff --git a/lib/cubit/theme_cubit.dart b/lib/cubit/theme_cubit.dart
new file mode 100644
index 0000000000000000000000000000000000000000..b793e895dbb0c672d451cd403e0036c3d9ac9b42
--- /dev/null
+++ b/lib/cubit/theme_cubit.dart
@@ -0,0 +1,31 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+part 'theme_state.dart';
+
+class ThemeCubit extends HydratedCubit<ThemeModeState> {
+  ThemeCubit() : super(const ThemeModeState());
+
+  void getTheme(ThemeModeState state) {
+    emit(state);
+  }
+
+  @override
+  ThemeModeState? fromJson(Map<String, dynamic> json) {
+    switch (json['themeMode']) {
+      case 'ThemeMode.dark':
+        return const ThemeModeState(themeMode: ThemeMode.dark);
+      case 'ThemeMode.light':
+        return const ThemeModeState(themeMode: ThemeMode.light);
+      case 'ThemeMode.system':
+      default:
+        return const ThemeModeState(themeMode: ThemeMode.system);
+    }
+  }
+
+  @override
+  Map<String, String>? toJson(ThemeModeState state) {
+    return <String, String>{'themeMode': state.themeMode.toString()};
+  }
+}
diff --git a/lib/cubit/theme_state.dart b/lib/cubit/theme_state.dart
new file mode 100644
index 0000000000000000000000000000000000000000..e479a50f12fe72a35a1fd1722ff72afbb692a136
--- /dev/null
+++ b/lib/cubit/theme_state.dart
@@ -0,0 +1,15 @@
+part of 'theme_cubit.dart';
+
+@immutable
+class ThemeModeState extends Equatable {
+  const ThemeModeState({
+    this.themeMode,
+  });
+
+  final ThemeMode? themeMode;
+
+  @override
+  List<Object?> get props => <Object?>[
+        themeMode,
+      ];
+}
diff --git a/lib/game_chip.dart b/lib/game_chip.dart
deleted file mode 100644
index 22d94167f72cfd2aa493765d985df3cab5525678..0000000000000000000000000000000000000000
--- a/lib/game_chip.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-import 'package:flutter/material.dart';
-
-import 'package:puissance4/match_page.dart';
-
-class GameChip extends StatelessWidget {
-  const GameChip({
-    super.key,
-    this.translation,
-    this.color,
-  });
-
-  final Animation<double>? translation;
-  final Color? color;
-
-  @override
-  Widget build(BuildContext context) {
-    return Center(
-      child: Container(
-        transform: Matrix4.translationValues(
-          0,
-          ((translation?.value ?? 1) * 400) - 400,
-          0,
-        ),
-        height: 40,
-        width: 40,
-        child: Material(
-          shape: const CircleBorder(),
-          color: color == Color.red ? Colors.red : Colors.yellow,
-        ),
-      ),
-    );
-  }
-}
diff --git a/lib/hole_painter.dart b/lib/hole_painter.dart
deleted file mode 100644
index ebabafb557ea9d066a22ec3702b16a10e3acab77..0000000000000000000000000000000000000000
--- a/lib/hole_painter.dart
+++ /dev/null
@@ -1,39 +0,0 @@
-import 'dart:math';
-
-import 'package:flutter/material.dart';
-
-class HolePainter extends CustomPainter {
-  @override
-  void paint(Canvas canvas, Size size) {
-    final paint = Paint();
-    paint.color = Colors.blue;
-    paint.style = PaintingStyle.fill;
-
-    final center = Offset(size.height / 2, size.width / 2);
-
-    final circleBounds = Rect.fromCircle(center: center, radius: 20);
-
-    final topPath = Path()
-      ..moveTo(-1, -1)
-      ..lineTo(-1, (size.height / 2) + 1)
-      ..arcTo(circleBounds, -pi, pi, false)
-      ..lineTo(size.width + 1, (size.height / 2) + 1)
-      ..lineTo(size.width + 1, -1)
-      ..close();
-    final bottomPath = Path()
-      ..moveTo(-1, size.height)
-      ..lineTo(-1, (size.height / 2) - 1)
-      ..arcTo(circleBounds, pi, -pi, false)
-      ..lineTo(size.width + 1, (size.height / 2) - 1)
-      ..lineTo(size.width + 1, size.height + 1)
-      ..close();
-
-    canvas.drawPath(topPath, paint);
-    canvas.drawPath(bottomPath, paint);
-  }
-
-  @override
-  bool shouldRepaint(CustomPainter oldDelegate) {
-    return false;
-  }
-}
diff --git a/lib/home_page.dart b/lib/home_page.dart
deleted file mode 100644
index ab7d3253c02d22d9abdc6bb3fa8e5db5714a187f..0000000000000000000000000000000000000000
--- a/lib/home_page.dart
+++ /dev/null
@@ -1,89 +0,0 @@
-import 'dart:math';
-
-import 'package:flutter/material.dart';
-
-import 'package:puissance4/cpu.dart';
-import 'package:puissance4/match_page.dart';
-
-class HomePage extends StatelessWidget {
-  const HomePage({super.key});
-
-  @override
-  Widget build(BuildContext context) {
-    return Scaffold(
-      backgroundColor: Colors.blue,
-      body: Center(
-        child: Column(
-          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
-          crossAxisAlignment: CrossAxisAlignment.center,
-          mainAxisSize: MainAxisSize.max,
-          children: <Widget>[
-            TextButton(
-              style: TextButton.styleFrom(
-                backgroundColor: Colors.green,
-                padding: const EdgeInsets.all(15),
-              ),
-              child: Text(
-                '🧑 2 JOUEURS 🧑',
-                style:
-                    Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white),
-              ),
-              onPressed: () {
-                Navigator.pushNamed(
-                  context,
-                  '/match',
-                  arguments: {
-                    'mode': Mode.pvp,
-                  },
-                );
-              },
-            ),
-            TextButton(
-              style: TextButton.styleFrom(
-                backgroundColor: Colors.orange,
-                padding: const EdgeInsets.all(15),
-              ),
-              child: Text(
-                '🧑 1 JOUEUR 🤖',
-                style:
-                    Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white),
-              ),
-              onPressed: () {
-                Navigator.pushNamed(
-                  context,
-                  '/cpu-level',
-                  arguments: {
-                    'mode': Mode.pvc,
-                  },
-                );
-              },
-            ),
-            TextButton(
-              style: TextButton.styleFrom(
-                backgroundColor: Colors.white,
-                padding: const EdgeInsets.all(15),
-              ),
-              child: Text(
-                '🤖 DEMO 🤖',
-                style:
-                    Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.black),
-              ),
-              onPressed: () {
-                final harderCpu = HarderCpu(Random().nextBool() ? Color.red : Color.yellow);
-                Navigator.pushNamed(
-                  context,
-                  '/match',
-                  arguments: {
-                    'mode': Mode.demo,
-                    'cpu': harderCpu,
-                    'cpu2': HardestCpu(harderCpu.otherPlayer),
-                  },
-                );
-              },
-            ),
-          ],
-        ),
-      ),
-    );
-  }
-}
diff --git a/lib/main.dart b/lib/main.dart
index 8a2676ab28835640daa254bec4580c814cb69636..1ced97dba6ad18a34de0d9e79bebd65fb7011462 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,14 +1,42 @@
+import 'dart:io';
+
+import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:hive/hive.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+import 'package:path_provider/path_provider.dart';
 
-import 'package:puissance4/cpu_level_page.dart';
-import 'package:puissance4/home_page.dart';
-import 'package:puissance4/match_page.dart';
+import 'package:puissance4/config/theme.dart';
+import 'package:puissance4/cubit/game_cubit.dart';
+import 'package:puissance4/cubit/nav_cubit.dart';
+import 'package:puissance4/cubit/settings_game_cubit.dart';
+import 'package:puissance4/cubit/settings_global_cubit.dart';
+import 'package:puissance4/cubit/theme_cubit.dart';
+import 'package:puissance4/ui/skeleton.dart';
 
-void main() {
+void main() async {
+  // Initialize packages
   WidgetsFlutterBinding.ensureInitialized();
+  await EasyLocalization.ensureInitialized();
+  final Directory tmpDir = await getTemporaryDirectory();
+  Hive.init(tmpDir.toString());
+  HydratedBloc.storage = await HydratedStorage.build(
+    storageDirectory: tmpDir,
+  );
+
   SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
-      .then((value) => runApp(const MyApp()));
+      .then((value) => runApp(EasyLocalization(
+            path: 'assets/translations',
+            supportedLocales: const <Locale>[
+              Locale('en'),
+              Locale('fr'),
+            ],
+            fallbackLocale: const Locale('en'),
+            useFallbackTranslations: true,
+            child: const MyApp(),
+          )));
 }
 
 class MyApp extends StatelessWidget {
@@ -16,30 +44,58 @@ class MyApp extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return MaterialApp(
-      title: 'Puissance 4',
-      theme: ThemeData(
-        primarySwatch: Colors.blue,
-      ),
-      home: const HomePage(),
-      onGenerateRoute: (settings) {
-        final args = settings.arguments as Map<String, dynamic>;
-        if (settings.name == '/match') {
-          return MaterialPageRoute(
-            builder: (context) => MatchPage(
-              mode: args['mode'],
-              cpu: args['cpu'],
-              cpu2: args['cpu2'],
-            ),
-          );
-        } else if (settings.name == '/cpu-level') {
-          return MaterialPageRoute(
-            builder: (context) => const CpuLevelPage(),
-          );
-        }
+    final List<String> assets = getImagesAssets();
+    for (String asset in assets) {
+      precacheImage(AssetImage(asset), context);
+    }
+
+    return MultiBlocProvider(
+      providers: [
+        BlocProvider<NavCubit>(create: (context) => NavCubit()),
+        BlocProvider<ThemeCubit>(create: (context) => ThemeCubit()),
+        BlocProvider<GameCubit>(create: (context) => GameCubit()),
+        BlocProvider<GlobalSettingsCubit>(create: (context) => GlobalSettingsCubit()),
+        BlocProvider<GameSettingsCubit>(create: (context) => GameSettingsCubit()),
+      ],
+      child: BlocBuilder<ThemeCubit, ThemeModeState>(
+        builder: (BuildContext context, ThemeModeState state) {
+          return MaterialApp(
+            title: 'Puissance4',
+            home: const SkeletonScreen(),
+
+            // Theme stuff
+            theme: lightTheme,
+            darkTheme: darkTheme,
+            themeMode: state.themeMode,
 
-        return null;
-      },
+            // Localization stuff
+            localizationsDelegates: context.localizationDelegates,
+            supportedLocales: context.supportedLocales,
+            locale: context.locale,
+            debugShowCheckedModeBanner: false,
+          );
+        },
+      ),
     );
   }
+
+  List<String> getImagesAssets() {
+    final List<String> assets = [];
+
+    const List<String> gameImages = [
+      'button_back',
+      'button_delete_saved_game',
+      'button_resume_game',
+      'button_start',
+      'game_fail',
+      'game_win',
+      'placeholder',
+    ];
+
+    for (String image in gameImages) {
+      assets.add('assets/ui/$image.png');
+    }
+
+    return assets;
+  }
 }
diff --git a/lib/match_page.dart b/lib/match_page.dart
deleted file mode 100644
index b9a98bd838c379e7becc3ac925b821e5838c7abe..0000000000000000000000000000000000000000
--- a/lib/match_page.dart
+++ /dev/null
@@ -1,301 +0,0 @@
-import 'dart:math';
-
-import 'package:flutter/material.dart';
-
-import 'package:puissance4/board.dart';
-import 'package:puissance4/coordinate.dart';
-import 'package:puissance4/cpu.dart';
-import 'package:puissance4/game_chip.dart';
-import 'package:puissance4/hole_painter.dart';
-
-enum Color {
-  yellow,
-  red,
-}
-
-enum Mode {
-  pvp,
-  pvc,
-  demo,
-}
-
-class MatchPage extends StatefulWidget {
-  final Mode mode;
-  final Cpu? cpu;
-  final Cpu? cpu2;
-
-  const MatchPage({
-    super.key,
-    required this.mode,
-    required this.cpu,
-    required this.cpu2,
-  });
-
-  @override
-  MatchPageState createState() => MatchPageState();
-}
-
-class MatchPageState extends State<MatchPage> with TickerProviderStateMixin {
-  final board = Board();
-  Color? turn;
-  Color? winner;
-
-  List<List<Animation<double>?>> translations = List.generate(
-    7,
-    (i) => List.generate(
-      7,
-      (i) => null,
-    ),
-  );
-
-  @override
-  Widget build(BuildContext context) {
-    return Scaffold(
-      appBar: AppBar(
-        elevation: 0,
-      ),
-      backgroundColor: Colors.blue,
-      body: Padding(
-        padding: const EdgeInsets.all(16.0),
-        child: Flex(
-          direction: Axis.vertical,
-          mainAxisSize: MainAxisSize.max,
-          children: <Widget>[
-            Flexible(
-              flex: 2,
-              child: Container(
-                constraints: BoxConstraints.loose(
-                  const Size(
-                    500,
-                    532,
-                  ),
-                ),
-                child: Padding(
-                  padding: const EdgeInsets.only(top: 32.0),
-                  child: Stack(
-                    fit: StackFit.loose,
-                    children: <Widget>[
-                      Positioned.fill(
-                        child: Container(
-                          color: Colors.white,
-                        ),
-                      ),
-                      buildPieces(),
-                      buildBoard(),
-                    ],
-                  ),
-                ),
-              ),
-            ),
-            Flexible(
-              flex: 1,
-              child: Padding(
-                padding: const EdgeInsets.all(32.0),
-                child: winner != null
-                    ? Text(
-                        '${winner == Color.red ? 'RED' : 'YELLOW'} WINS',
-                        textAlign: TextAlign.center,
-                        style: Theme.of(context)
-                            .textTheme
-                            .titleLarge
-                            ?.copyWith(color: Colors.white),
-                      )
-                    : Column(
-                        children: <Widget>[
-                          Text(
-                            '${turn == Color.red ? 'RED' : 'YELLOW'} SPEAKS',
-                            textAlign: TextAlign.center,
-                            style: Theme.of(context)
-                                .textTheme
-                                .headlineSmall
-                                ?.copyWith(color: Colors.white),
-                          ),
-                          Padding(
-                            padding: const EdgeInsets.all(8.0),
-                            child: GameChip(
-                              color: turn,
-                            ),
-                          ),
-                          _buildPlayerName(context),
-                        ],
-                      ),
-              ),
-            ),
-          ],
-        ),
-      ),
-    );
-  }
-
-  Text _buildPlayerName(BuildContext context) {
-    String name;
-
-    if (widget.mode == Mode.pvc) {
-      if (turn == widget.cpu?.color) {
-        name = 'CPU - ${widget.cpu.toString()}';
-      } else {
-        name = 'USER';
-      }
-    } else if (widget.mode == Mode.pvp) {
-      if (turn == Color.red) {
-        name = 'PLAYER1';
-      } else {
-        name = 'PLAYER2';
-      }
-    } else {
-      if (turn == widget.cpu?.color) {
-        name = 'CPU1 - ${widget.cpu.toString()}';
-      } else {
-        name = 'CPU2 - ${widget.cpu2.toString()}';
-      }
-    }
-    return Text(
-      name,
-      textAlign: TextAlign.center,
-      style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.white),
-    );
-  }
-
-  @override
-  void initState() {
-    super.initState();
-    turn = Random().nextBool() ? Color.red : Color.yellow;
-    if (widget.mode == Mode.pvc && turn == widget.cpu?.color) {
-      cpuMove(widget.cpu);
-    } else if (widget.mode == Mode.demo) {
-      if (turn == widget.cpu?.color) {
-        cpuMove(widget.cpu);
-      } else {
-        cpuMove(widget.cpu2);
-      }
-    }
-  }
-
-  GridView buildPieces() {
-    return GridView.custom(
-      padding: const EdgeInsets.all(0),
-      shrinkWrap: true,
-      physics: const NeverScrollableScrollPhysics(),
-      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 7),
-      childrenDelegate: SliverChildBuilderDelegate(
-        (context, i) {
-          final col = i % 7;
-          final row = i ~/ 7;
-
-          if (board.getBox(Coordinate(col, row)) == null) {
-            return const SizedBox();
-          }
-
-          return GameChip(
-            translation: translations[col][row],
-            color: board.getBox(Coordinate(col, row)),
-          );
-        },
-        childCount: 49,
-      ),
-    );
-  }
-
-  GridView buildBoard() {
-    return GridView.custom(
-      padding: const EdgeInsets.all(0),
-      physics: const NeverScrollableScrollPhysics(),
-      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 7),
-      shrinkWrap: true,
-      childrenDelegate: SliverChildBuilderDelegate(
-        (context, i) {
-          final col = i % 7;
-
-          return GestureDetector(
-            onTap: () {
-              if (winner == null) {
-                userMove(col);
-              }
-            },
-            child: CustomPaint(
-              size: const Size(50, 50),
-              willChange: false,
-              painter: HolePainter(),
-            ),
-          );
-        },
-        childCount: 49,
-      ),
-    );
-  }
-
-  void userMove(int col) {
-    putChip(col);
-    if (winner == null && widget.mode == Mode.pvc) {
-      cpuMove(widget.cpu);
-    }
-  }
-
-  void cpuMove(Cpu? cpu) async {
-    int? col = await cpu?.chooseCol(board);
-    putChip(col);
-
-    if (winner == null && widget.mode == Mode.demo) {
-      if (turn == widget.cpu?.color) {
-        cpuMove(widget.cpu);
-      } else {
-        cpuMove(widget.cpu2);
-      }
-    }
-  }
-
-  void putChip(int? col) {
-    final target = board.getColumnTarget(col);
-    final player = turn;
-
-    if (target == -1) {
-      return;
-    }
-
-    final controller = AnimationController(
-      vsync: this,
-      duration: const Duration(seconds: 1),
-    )..addListener(() {
-        if (mounted) {
-          setState(() {});
-        }
-      });
-
-    if (mounted) {
-      setState(() {
-        board.setBox(Coordinate(col, target), turn);
-        turn = turn == Color.red ? Color.yellow : Color.red;
-      });
-    }
-
-    translations[col ?? 0][target] = Tween(
-      begin: 0.0,
-      end: 1.0,
-    ).animate(CurvedAnimation(
-      curve: Curves.bounceOut,
-      parent: controller,
-    ))
-      ..addStatusListener((status) {
-        if (status == AnimationStatus.completed) {
-          controller.dispose();
-        }
-      });
-
-    controller.forward().orCancel;
-
-    if (board.checkWinner(Coordinate(col, target), player)) {
-      showWinnerDialog(context, player);
-    }
-  }
-
-  void showWinnerDialog(BuildContext context, Color? player) {
-    setState(() {
-      winner = player;
-    });
-
-    Future.delayed(
-      const Duration(seconds: 5),
-      () => mounted ? Navigator.popUntil(context, (r) => r.isFirst) : null,
-    );
-  }
-}
diff --git a/lib/models/game/board.dart b/lib/models/game/board.dart
new file mode 100644
index 0000000000000000000000000000000000000000..335568aaa71d43319188230be88020f9328ffaaf
--- /dev/null
+++ b/lib/models/game/board.dart
@@ -0,0 +1,62 @@
+import 'package:puissance4/utils/tools.dart';
+
+typedef BoardCells = List<List<int>>;
+
+class Board {
+  Board({
+    required this.cells,
+  });
+
+  BoardCells cells = const [];
+
+  factory Board.createNull() {
+    return Board(
+      cells: [],
+    );
+  }
+
+  factory Board.createNew({
+    required BoardCells cells,
+  }) {
+    return Board(
+      cells: cells,
+    );
+  }
+
+  int getCellValue({required int row, required int col}) {
+    return cells[row][col];
+  }
+
+  void setCellValue({required int row, required int col, required int value}) {
+    cells[row][col] = value;
+  }
+
+  void dump() {
+    printlog('');
+    String line = '--';
+    for (int i = 0; i < cells[0].length; i++) {
+      line += '-';
+    }
+    printlog(line);
+    for (int rowIndex = 0; rowIndex < cells.length; rowIndex++) {
+      String currentLine = '';
+      for (int colIndex = 0; colIndex < cells[rowIndex].length; colIndex++) {
+        currentLine += getCellValue(row: rowIndex, col: colIndex).toString();
+      }
+      printlog('|$currentLine|');
+    }
+    printlog(line);
+    printlog('');
+  }
+
+  @override
+  String toString() {
+    return '$Board(${toJson()})';
+  }
+
+  Map<String, dynamic>? toJson() {
+    return <String, dynamic>{
+      'cells': cells,
+    };
+  }
+}
diff --git a/lib/models/game/game.dart b/lib/models/game/game.dart
new file mode 100644
index 0000000000000000000000000000000000000000..2d60cf380ef789be83b64bdf2b570dbcf0ab14d8
--- /dev/null
+++ b/lib/models/game/game.dart
@@ -0,0 +1,226 @@
+import 'package:puissance4/config/default_game_settings.dart';
+import 'package:puissance4/models/game/board.dart';
+import 'package:puissance4/models/settings/settings_game.dart';
+import 'package:puissance4/models/settings/settings_global.dart';
+import 'package:puissance4/utils/tools.dart';
+
+class Game {
+  Game({
+    // Settings
+    required this.gameSettings,
+    required this.globalSettings,
+
+    // State
+    this.isRunning = false,
+    this.isStarted = false,
+    this.isFinished = false,
+    this.animationInProgress = false,
+
+    // Base data
+    required this.board,
+    required this.sizeHorizontal,
+    required this.sizeVertical,
+
+    // Game data
+    required this.currentPlayer,
+  });
+
+  // Settings
+  final GameSettings gameSettings;
+  final GlobalSettings globalSettings;
+
+  // State
+  bool isRunning;
+  bool isStarted;
+  bool isFinished;
+  bool animationInProgress;
+
+  // Base data
+  Board board;
+  final int sizeHorizontal;
+  final int sizeVertical;
+
+  // Game data
+  int currentPlayer;
+
+  factory Game.createNull() {
+    return Game(
+      // Settings
+      gameSettings: GameSettings.createDefault(),
+      globalSettings: GlobalSettings.createDefault(),
+      // Base data
+      board: Board.createNull(),
+      sizeHorizontal: 0,
+      sizeVertical: 0,
+      // Game data
+      currentPlayer: 0,
+    );
+  }
+
+  factory Game.createNew({
+    GameSettings? gameSettings,
+    GlobalSettings? globalSettings,
+  }) {
+    final GameSettings newGameSettings = gameSettings ?? GameSettings.createDefault();
+    final GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault();
+
+    final int sizeHorizontal = int.parse(newGameSettings.size.split('x')[0]);
+    final int sizeVertical = int.parse(newGameSettings.size.split('x')[1]);
+
+    // Create empty board (it will be mined after first hit)
+    final BoardCells board = [];
+    for (int rowIndex = 0; rowIndex < sizeVertical; rowIndex++) {
+      final List<int> row = [];
+      for (int colIndex = 0; colIndex < sizeHorizontal; colIndex++) {
+        row.add(0);
+      }
+      board.add(row);
+    }
+
+    return Game(
+      // Settings
+      gameSettings: newGameSettings,
+      globalSettings: newGlobalSettings,
+      // State
+      isRunning: true,
+      // Base data
+      board: Board.createNew(cells: board),
+      sizeHorizontal: sizeHorizontal,
+      sizeVertical: sizeVertical,
+      // Game data
+      currentPlayer: 1,
+    );
+  }
+
+  bool get canBeResumed => isStarted && !isFinished;
+
+  // Ensure move is allowed
+  bool isMoveAllowed({required int col}) {
+    // ensure first row is empty on selected col
+    return board.getCellValue(row: 0, col: col) == 0;
+  }
+
+  bool connected4() {
+    // vertical
+    for (int row = 0; row < sizeVertical - 3; row++) {
+      for (int col = 0; col < sizeHorizontal; col++) {
+        if ((board.getCellValue(row: row, col: col) == currentPlayer) &&
+            (board.getCellValue(row: row + 1, col: col) == currentPlayer) &&
+            (board.getCellValue(row: row + 2, col: col) == currentPlayer) &&
+            (board.getCellValue(row: row + 3, col: col) == currentPlayer)) {
+          return true;
+        }
+      }
+    }
+
+    // horizontal
+    for (int row = 0; row < sizeVertical; row++) {
+      for (int col = 0; col < sizeHorizontal - 3; col++) {
+        if ((board.getCellValue(row: row, col: col) == currentPlayer) &&
+            (board.getCellValue(row: row, col: col + 1) == currentPlayer) &&
+            (board.getCellValue(row: row, col: col + 2) == currentPlayer) &&
+            (board.getCellValue(row: row, col: col + 3) == currentPlayer)) {
+          return true;
+        }
+      }
+    }
+
+    // diagonal down
+    for (int row = 0; row < sizeVertical - 3; row++) {
+      for (int col = 0; col < sizeHorizontal - 3; col++) {
+        if ((board.getCellValue(row: row, col: col) == currentPlayer) &&
+            (board.getCellValue(row: row + 1, col: col + 1) == currentPlayer) &&
+            (board.getCellValue(row: row + 2, col: col + 2) == currentPlayer) &&
+            (board.getCellValue(row: row + 3, col: col + 3) == currentPlayer)) {
+          return true;
+        }
+      }
+    }
+
+    // diagonal up
+    for (int row = 0; row < sizeVertical - 3; row++) {
+      for (int col = 0; col < sizeHorizontal - 3; col++) {
+        if ((board.getCellValue(row: row + 3, col: col) == currentPlayer) &&
+            (board.getCellValue(row: row + 2, col: col + 1) == currentPlayer) &&
+            (board.getCellValue(row: row + 1, col: col + 2) == currentPlayer) &&
+            (board.getCellValue(row: row, col: col + 3) == currentPlayer)) {
+          return true;
+        }
+      }
+    }
+
+    return false;
+  }
+
+  bool canPlay() {
+    List<int> allowedColumns = [];
+
+    for (int columnIndex = 0; columnIndex < sizeHorizontal; columnIndex++) {
+      if (isMoveAllowed(col: columnIndex)) {
+        allowedColumns.add(columnIndex);
+      }
+    }
+
+    return allowedColumns.isNotEmpty;
+  }
+
+  bool isPlayerHuman(int playerIndex) {
+    switch (gameSettings.gameMode) {
+      case DefaultGameSettings.gameModeHumanVsHuman:
+        return true;
+      case DefaultGameSettings.gameModeHumanVsRobot:
+        return (playerIndex == 1);
+      case DefaultGameSettings.gameModeRobotVsHuman:
+        return (playerIndex == 2);
+      case DefaultGameSettings.gameModeRobotVsRobot:
+        return false;
+      default:
+    }
+    return true;
+  }
+
+  bool isCurrentPlayerHuman() {
+    return isPlayerHuman(currentPlayer);
+  }
+
+  void dump() {
+    printlog('');
+    printlog('## Current game dump:');
+    printlog('');
+    printlog('$Game:');
+    printlog('  Settings');
+    gameSettings.dump();
+    globalSettings.dump();
+    printlog('  State');
+    printlog('    isRunning: $isRunning');
+    printlog('    isStarted: $isStarted');
+    printlog('    isFinished: $isFinished');
+    printlog('    animationInProgress: $animationInProgress');
+    board.dump();
+    printlog('  Game data');
+    printlog('    currentPlayer: $currentPlayer');
+    printlog('');
+  }
+
+  @override
+  String toString() {
+    return '$Game(${toJson()})';
+  }
+
+  Map<String, dynamic>? toJson() {
+    return <String, dynamic>{
+      // Settings
+      'gameSettings': gameSettings.toJson(),
+      'globalSettings': globalSettings.toJson(),
+      // State
+      'isRunning': isRunning,
+      'isStarted': isStarted,
+      'isFinished': isFinished,
+      'animationInProgress': animationInProgress,
+      // Base data
+      'board': board.toJson(),
+      // Game data
+      'currentPlayer': currentPlayer,
+    };
+  }
+}
diff --git a/lib/models/settings/settings_game.dart b/lib/models/settings/settings_game.dart
new file mode 100644
index 0000000000000000000000000000000000000000..42a2a2e4b5ea3fb8dffe97742051379601d58980
--- /dev/null
+++ b/lib/models/settings/settings_game.dart
@@ -0,0 +1,54 @@
+import 'package:puissance4/config/default_game_settings.dart';
+import 'package:puissance4/utils/tools.dart';
+
+class GameSettings {
+  final String gameMode;
+  final String size;
+
+  GameSettings({
+    required this.gameMode,
+    required this.size,
+  });
+
+  static String getGameModeValueFromUnsafe(String gameMode) {
+    if (DefaultGameSettings.allowedGameModeValues.contains(gameMode)) {
+      return gameMode;
+    }
+
+    return DefaultGameSettings.defaultGameModeValue;
+  }
+
+  static String getSizeValueFromUnsafe(String size) {
+    if (DefaultGameSettings.allowedSizeValues.contains(size)) {
+      return size;
+    }
+
+    return DefaultGameSettings.defaultSizeValue;
+  }
+
+  factory GameSettings.createDefault() {
+    return GameSettings(
+      gameMode: DefaultGameSettings.defaultGameModeValue,
+      size: DefaultGameSettings.defaultSizeValue,
+    );
+  }
+
+  void dump() {
+    printlog('$GameSettings:');
+    printlog('  ${DefaultGameSettings.parameterCodeGameMode}: $gameMode');
+    printlog('  ${DefaultGameSettings.parameterCodeSize}: $size');
+    printlog('');
+  }
+
+  @override
+  String toString() {
+    return '$GameSettings(${toJson()})';
+  }
+
+  Map<String, dynamic>? toJson() {
+    return <String, dynamic>{
+      DefaultGameSettings.parameterCodeGameMode: gameMode,
+      DefaultGameSettings.parameterCodeSize: size,
+    };
+  }
+}
diff --git a/lib/models/settings/settings_global.dart b/lib/models/settings/settings_global.dart
new file mode 100644
index 0000000000000000000000000000000000000000..cd6b2484aa263c3481a625a034bbebdd7423ad5a
--- /dev/null
+++ b/lib/models/settings/settings_global.dart
@@ -0,0 +1,41 @@
+import 'package:puissance4/config/default_global_settings.dart';
+import 'package:puissance4/utils/tools.dart';
+
+class GlobalSettings {
+  String skin;
+
+  GlobalSettings({
+    required this.skin,
+  });
+
+  static String getSkinValueFromUnsafe(String skin) {
+    if (DefaultGlobalSettings.allowedSkinValues.contains(skin)) {
+      return skin;
+    }
+
+    return DefaultGlobalSettings.defaultSkinValue;
+  }
+
+  factory GlobalSettings.createDefault() {
+    return GlobalSettings(
+      skin: DefaultGlobalSettings.defaultSkinValue,
+    );
+  }
+
+  void dump() {
+    printlog('$GlobalSettings:');
+    printlog('  ${DefaultGlobalSettings.parameterCodeSkin}: $skin');
+    printlog('');
+  }
+
+  @override
+  String toString() {
+    return '$GlobalSettings(${toJson()})';
+  }
+
+  Map<String, dynamic>? toJson() {
+    return <String, dynamic>{
+      DefaultGlobalSettings.parameterCodeSkin: skin,
+    };
+  }
+}
diff --git a/lib/player.dart b/lib/player.dart
deleted file mode 100644
index f404ecec61663e88164befae628885101445b222..0000000000000000000000000000000000000000
--- a/lib/player.dart
+++ /dev/null
@@ -1,7 +0,0 @@
-import 'package:puissance4/match_page.dart';
-
-abstract class Player {
-  final Color player;
-
-  Player(this.player);
-}
diff --git a/lib/robot/robot_player.dart b/lib/robot/robot_player.dart
new file mode 100644
index 0000000000000000000000000000000000000000..89fdc61e42d61ea7a10b941d5d057dbeb3615a3f
--- /dev/null
+++ b/lib/robot/robot_player.dart
@@ -0,0 +1,23 @@
+import 'package:puissance4/models/game/game.dart';
+
+class RobotPlayer {
+  static int? pickColumn(Game currentGame) {
+    List<int> allowedColumns = [];
+
+    for (int columnIndex = 0; columnIndex < currentGame.sizeHorizontal; columnIndex++) {
+      if (currentGame.isMoveAllowed(col: columnIndex)) {
+        allowedColumns.add(columnIndex);
+      }
+    }
+
+    if (allowedColumns.isEmpty) {
+      return null;
+    }
+
+    allowedColumns.shuffle();
+
+    final int pickedColumn = allowedColumns[0];
+
+    return pickedColumn;
+  }
+}
diff --git a/lib/ui/helpers/app_titles.dart b/lib/ui/helpers/app_titles.dart
new file mode 100644
index 0000000000000000000000000000000000000000..b98107b12fabc3114ebfbec994166b588abcf1ad
--- /dev/null
+++ b/lib/ui/helpers/app_titles.dart
@@ -0,0 +1,32 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+
+class AppHeader extends StatelessWidget {
+  const AppHeader({super.key, required this.text});
+
+  final String text;
+
+  @override
+  Widget build(BuildContext context) {
+    return Text(
+      tr(text),
+      textAlign: TextAlign.start,
+      style: Theme.of(context).textTheme.headlineMedium!.apply(fontWeightDelta: 2),
+    );
+  }
+}
+
+class AppTitle extends StatelessWidget {
+  const AppTitle({super.key, required this.text});
+
+  final String text;
+
+  @override
+  Widget build(BuildContext context) {
+    return Text(
+      tr(text),
+      textAlign: TextAlign.start,
+      style: Theme.of(context).textTheme.titleLarge!.apply(fontWeightDelta: 2),
+    );
+  }
+}
diff --git a/lib/ui/helpers/styled_button.dart b/lib/ui/helpers/styled_button.dart
new file mode 100644
index 0000000000000000000000000000000000000000..3dfe5c8a35d94746458e94e14cdf7bec9f835941
--- /dev/null
+++ b/lib/ui/helpers/styled_button.dart
@@ -0,0 +1,210 @@
+import 'package:auto_size_text/auto_size_text.dart';
+import 'package:flutter/material.dart';
+
+import 'package:puissance4/utils/color_extensions.dart';
+
+class StyledButton extends StatelessWidget {
+  const StyledButton({
+    super.key,
+    required this.color,
+    required this.onPressed,
+    this.onLongPress,
+    required this.child,
+  });
+
+  final Color color;
+  final VoidCallback? onPressed;
+  final VoidCallback? onLongPress;
+  final Widget child;
+
+  factory StyledButton.text({
+    Key? key,
+    required VoidCallback? onPressed,
+    VoidCallback? onLongPress,
+    required String caption,
+    required Color color,
+  }) {
+    final Widget captionWidget = AutoSizeText(
+      caption,
+      maxLines: 1,
+      style: TextStyle(
+        inherit: true,
+        fontWeight: FontWeight.w900,
+        color: color.darken(60),
+        shadows: [
+          Shadow(
+            blurRadius: 5.0,
+            color: color.lighten(60),
+            offset: const Offset(2, 2),
+          ),
+          Shadow(
+            blurRadius: 5.0,
+            color: color.lighten(60),
+            offset: const Offset(2, -2),
+          ),
+          Shadow(
+            blurRadius: 5.0,
+            color: color.lighten(60),
+            offset: const Offset(-2, 2),
+          ),
+          Shadow(
+            blurRadius: 5.0,
+            color: color.lighten(60),
+            offset: const Offset(-2, -2),
+          ),
+        ],
+      ),
+    );
+
+    return StyledButton(
+      color: color,
+      onPressed: onPressed,
+      onLongPress: onLongPress,
+      child: captionWidget,
+    );
+  }
+
+  factory StyledButton.icon({
+    Key? key,
+    required VoidCallback? onPressed,
+    VoidCallback? onLongPress,
+    required Icon icon,
+    required Color color,
+    required double iconSize,
+  }) {
+    return StyledButton(
+      color: color,
+      onPressed: onPressed,
+      onLongPress: onLongPress,
+      child: Icon(
+        icon.icon,
+        color: icon.color ?? color.darken(60),
+        size: iconSize,
+        shadows: [
+          Shadow(
+            blurRadius: 5.0,
+            color: color.lighten(60),
+            offset: const Offset(2, 2),
+          ),
+          Shadow(
+            blurRadius: 5.0,
+            color: color.lighten(60),
+            offset: const Offset(2, -2),
+          ),
+          Shadow(
+            blurRadius: 5.0,
+            color: color.lighten(60),
+            offset: const Offset(-2, 2),
+          ),
+          Shadow(
+            blurRadius: 5.0,
+            color: color.lighten(60),
+            offset: const Offset(-2, -2),
+          ),
+        ],
+      ),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    const double borderWidth = 4;
+    final Color borderColor = color.darken(40);
+    const double borderRadius = 10;
+
+    return Container(
+      margin: const EdgeInsets.all(2),
+      padding: const EdgeInsets.all(2),
+      decoration: BoxDecoration(
+        color: color,
+        border: Border.all(
+          color: borderColor,
+          width: borderWidth,
+        ),
+        borderRadius: BorderRadius.circular(borderRadius),
+      ),
+      child: CustomPaint(
+        painter: StyledButtonPainter(
+          baseColor: color,
+        ),
+        child: MaterialButton(
+          onPressed: onPressed,
+          onLongPress: onLongPress,
+          padding: const EdgeInsets.all(8),
+          materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+          minWidth: 40,
+          child: child,
+        ),
+      ),
+    );
+  }
+}
+
+class StyledButtonPainter extends CustomPainter {
+  StyledButtonPainter({
+    required this.baseColor,
+  });
+
+  final Color baseColor;
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final Color lightColor = baseColor.lighten(20);
+    final Color darkColor = baseColor.darken(20);
+
+    final Paint paint = Paint()..style = PaintingStyle.fill;
+
+    const double cornerRadius = 6;
+
+    Path topPath = Path()
+      ..moveTo(cornerRadius, 0)
+      ..lineTo(size.width - cornerRadius, 0)
+      ..arcToPoint(
+        Offset(size.width, cornerRadius),
+        radius: const Radius.circular(cornerRadius),
+      )
+      ..lineTo(size.width, size.height * .35)
+      ..quadraticBezierTo(
+        size.width * .4,
+        size.height * .1,
+        0,
+        size.height * .3,
+      )
+      ..lineTo(0, cornerRadius)
+      ..arcToPoint(
+        const Offset(cornerRadius, 0),
+        radius: const Radius.circular(cornerRadius),
+      );
+
+    Path bottomPath = Path()
+      ..moveTo(cornerRadius, size.height)
+      ..lineTo(size.width - cornerRadius, size.height)
+      ..arcToPoint(
+        Offset(size.width, size.height - cornerRadius),
+        radius: const Radius.circular(cornerRadius),
+        clockwise: false,
+      )
+      ..lineTo(size.width, size.height * .7)
+      ..quadraticBezierTo(
+        size.width * .6,
+        size.height * .9,
+        0,
+        size.height * .7,
+      )
+      ..lineTo(0, size.height - cornerRadius)
+      ..arcToPoint(
+        Offset(cornerRadius, size.height),
+        radius: const Radius.circular(cornerRadius),
+        clockwise: false,
+      );
+
+    paint.color = lightColor;
+    canvas.drawPath(topPath, paint);
+
+    paint.color = darkColor;
+    canvas.drawPath(bottomPath, paint);
+  }
+
+  @override
+  bool shouldRepaint(CustomPainter oldDelegate) => false;
+}
diff --git a/lib/ui/layouts/game_layout.dart b/lib/ui/layouts/game_layout.dart
new file mode 100644
index 0000000000000000000000000000000000000000..10d6660c07649787a68495dafe96dd5b72ea1643
--- /dev/null
+++ b/lib/ui/layouts/game_layout.dart
@@ -0,0 +1,38 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:puissance4/cubit/game_cubit.dart';
+import 'package:puissance4/models/game/game.dart';
+import 'package:puissance4/ui/widgets/game/game_board.dart';
+import 'package:puissance4/ui/widgets/game/game_bottom.dart';
+import 'package:puissance4/ui/widgets/game/game_end.dart';
+
+class GameLayout extends StatelessWidget {
+  const GameLayout({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameCubit, GameState>(
+      builder: (BuildContext context, GameState gameState) {
+        final Game currentGame = gameState.currentGame;
+
+        return Container(
+          alignment: AlignmentDirectional.topCenter,
+          padding: const EdgeInsets.all(4),
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              const SizedBox(height: 48),
+              const GameBoardWidget(),
+              const SizedBox(height: 16),
+              const GameBottomWidget(),
+              const Expanded(child: SizedBox.shrink()),
+              currentGame.isFinished ? const GameEndWidget() : const SizedBox.shrink(),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/layouts/parameters_layout.dart b/lib/ui/layouts/parameters_layout.dart
new file mode 100644
index 0000000000000000000000000000000000000000..7d3be8cb95dbc129a0551fbaab03ef911133f0bf
--- /dev/null
+++ b/lib/ui/layouts/parameters_layout.dart
@@ -0,0 +1,141 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:puissance4/config/default_game_settings.dart';
+import 'package:puissance4/config/default_global_settings.dart';
+import 'package:puissance4/cubit/settings_game_cubit.dart';
+import 'package:puissance4/cubit/settings_global_cubit.dart';
+import 'package:puissance4/ui/parameters/parameter_widget.dart';
+import 'package:puissance4/ui/widgets/actions/button_delete_saved_game.dart';
+import 'package:puissance4/ui/widgets/actions/button_game_start_new.dart';
+import 'package:puissance4/ui/widgets/actions/button_resume_saved_game.dart';
+
+class ParametersLayout extends StatelessWidget {
+  const ParametersLayout({super.key, required this.canResume});
+
+  final bool canResume;
+
+  final double separatorHeight = 8.0;
+
+  @override
+  Widget build(BuildContext context) {
+    final List<Widget> lines = [];
+
+    // Game settings
+    for (String code in DefaultGameSettings.availableParameters) {
+      lines.add(Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: buildParametersLine(
+          code: code,
+          isGlobal: false,
+        ),
+      ));
+
+      lines.add(SizedBox(height: separatorHeight));
+    }
+
+    lines.add(Expanded(
+      child: SizedBox(height: separatorHeight),
+    ));
+
+    if (canResume == false) {
+      // Start new game
+      lines.add(
+        const AspectRatio(
+          aspectRatio: 3,
+          child: StartNewGameButton(),
+        ),
+      );
+    } else {
+      // Resume game
+      lines.add(const AspectRatio(
+        aspectRatio: 3,
+        child: ResumeSavedGameButton(),
+      ));
+      // Delete saved game
+      lines.add(SizedBox.square(
+        dimension: MediaQuery.of(context).size.width / 5,
+        child: const DeleteSavedGameButton(),
+      ));
+    }
+
+    lines.add(SizedBox(height: separatorHeight));
+
+    // Global settings
+    for (String code in DefaultGlobalSettings.availableParameters) {
+      lines.add(Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: buildParametersLine(
+          code: code,
+          isGlobal: true,
+        ),
+      ));
+
+      lines.add(SizedBox(height: separatorHeight));
+    }
+
+    return Column(
+      children: lines,
+    );
+  }
+
+  List<Widget> buildParametersLine({
+    required String code,
+    required bool isGlobal,
+  }) {
+    final List<Widget> parameterButtons = [];
+
+    final List<String> availableValues = isGlobal
+        ? DefaultGlobalSettings.getAvailableValues(code)
+        : DefaultGameSettings.getAvailableValues(code);
+
+    if (availableValues.length <= 1) {
+      return [];
+    }
+
+    for (String value in availableValues) {
+      final Widget parameterButton = BlocBuilder<GameSettingsCubit, GameSettingsState>(
+        builder: (BuildContext context, GameSettingsState gameSettingsState) {
+          return BlocBuilder<GlobalSettingsCubit, GlobalSettingsState>(
+            builder: (BuildContext context, GlobalSettingsState globalSettingsState) {
+              final GameSettingsCubit gameSettingsCubit =
+                  BlocProvider.of<GameSettingsCubit>(context);
+              final GlobalSettingsCubit globalSettingsCubit =
+                  BlocProvider.of<GlobalSettingsCubit>(context);
+
+              final String currentValue = isGlobal
+                  ? globalSettingsCubit.getParameterValue(code)
+                  : gameSettingsCubit.getParameterValue(code);
+
+              final bool isSelected = (value == currentValue);
+
+              final double displayWidth = MediaQuery.of(context).size.width;
+              final double itemWidth = displayWidth / availableValues.length - 4;
+
+              return SizedBox.square(
+                dimension: itemWidth,
+                child: ParameterWidget(
+                  code: code,
+                  value: value,
+                  isSelected: isSelected,
+                  size: itemWidth,
+                  gameSettings: gameSettingsState.settings,
+                  globalSettings: globalSettingsState.settings,
+                  onPressed: () {
+                    isGlobal
+                        ? globalSettingsCubit.setParameterValue(code, value)
+                        : gameSettingsCubit.setParameterValue(code, value);
+                  },
+                ),
+              );
+            },
+          );
+        },
+      );
+
+      parameterButtons.add(parameterButton);
+    }
+
+    return parameterButtons;
+  }
+}
diff --git a/lib/ui/parameters/parameter_painter.dart b/lib/ui/parameters/parameter_painter.dart
new file mode 100644
index 0000000000000000000000000000000000000000..26d80a114e9a429abad2dee59edbf70c4b5adb56
--- /dev/null
+++ b/lib/ui/parameters/parameter_painter.dart
@@ -0,0 +1,116 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+
+import 'package:puissance4/config/default_game_settings.dart';
+import 'package:puissance4/models/settings/settings_game.dart';
+import 'package:puissance4/models/settings/settings_global.dart';
+import 'package:puissance4/utils/tools.dart';
+
+class ParameterPainter extends CustomPainter {
+  const ParameterPainter({
+    required this.code,
+    required this.value,
+    required this.gameSettings,
+    required this.globalSettings,
+  });
+
+  final String code;
+  final String value;
+  final GameSettings gameSettings;
+  final GlobalSettings globalSettings;
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    // force square
+    final double canvasSize = min(size.width, size.height);
+
+    // content
+    switch (code) {
+      case DefaultGameSettings.parameterCodeSize:
+        paintSizeParameterItem(canvas, canvasSize);
+        break;
+      default:
+        printlog('Unknown parameter: $code/$value');
+        paintUnknownParameterItem(canvas, canvasSize);
+    }
+  }
+
+  @override
+  bool shouldRepaint(CustomPainter oldDelegate) {
+    return false;
+  }
+
+  // "unknown" parameter -> simple block with text
+  void paintUnknownParameterItem(
+    final Canvas canvas,
+    final double size,
+  ) {
+    final paint = Paint();
+    paint.strokeJoin = StrokeJoin.round;
+    paint.strokeWidth = 3;
+
+    final textSpan = TextSpan(
+      text: '$code\n$value',
+      style: const TextStyle(
+        color: Colors.black,
+        fontSize: 18,
+        fontWeight: FontWeight.bold,
+      ),
+    );
+    final textPainter = TextPainter(
+      text: textSpan,
+      textDirection: TextDirection.ltr,
+      textAlign: TextAlign.center,
+    );
+    textPainter.layout();
+    textPainter.paint(
+      canvas,
+      Offset(
+        (size - textPainter.width) * 0.5,
+        (size - textPainter.height) * 0.5,
+      ),
+    );
+  }
+
+  void paintSizeParameterItem(
+    final Canvas canvas,
+    final double size,
+  ) {
+    int gridWidth = 1;
+
+    switch (value) {
+      case DefaultGameSettings.sizeValueMedium:
+        gridWidth = 3;
+        break;
+      default:
+        printlog('Wrong value for boardSize parameter value: $value');
+    }
+
+    final paint = Paint();
+    paint.strokeJoin = StrokeJoin.round;
+    paint.strokeWidth = 3 / 100 * size;
+
+    // Mini grid
+    final squareColor = Colors.grey.shade200;
+    final borderColor = Colors.grey.shade800;
+
+    final double cellSize = size / 7;
+    final double origin = (size - gridWidth * cellSize) / 2;
+
+    for (int row = 0; row < gridWidth; row++) {
+      for (int col = 0; col < gridWidth; col++) {
+        final Offset topLeft = Offset(origin + col * cellSize, origin + row * cellSize);
+        final Offset bottomRight = topLeft + Offset(cellSize, cellSize);
+
+        paint.color = squareColor;
+        paint.style = PaintingStyle.fill;
+        canvas.drawRect(Rect.fromPoints(topLeft, bottomRight), paint);
+
+        paint.color = borderColor;
+        paint.style = PaintingStyle.stroke;
+        canvas.drawRect(Rect.fromPoints(topLeft, bottomRight), paint);
+      }
+    }
+  }
+}
diff --git a/lib/ui/parameters/parameter_widget.dart b/lib/ui/parameters/parameter_widget.dart
new file mode 100644
index 0000000000000000000000000000000000000000..0e22ad2ac3a64ed4c134212864920f9bbc2e4655
--- /dev/null
+++ b/lib/ui/parameters/parameter_widget.dart
@@ -0,0 +1,133 @@
+import 'package:flutter/material.dart';
+
+import 'package:puissance4/config/default_game_settings.dart';
+import 'package:puissance4/models/settings/settings_game.dart';
+import 'package:puissance4/models/settings/settings_global.dart';
+import 'package:puissance4/ui/helpers/styled_button.dart';
+import 'package:puissance4/ui/parameters/parameter_painter.dart';
+import 'package:puissance4/utils/tools.dart';
+
+class ParameterWidget extends StatelessWidget {
+  const ParameterWidget({
+    super.key,
+    required this.code,
+    required this.value,
+    required this.isSelected,
+    required this.size,
+    required this.gameSettings,
+    required this.globalSettings,
+    required this.onPressed,
+  });
+
+  final String code;
+  final String value;
+  final bool isSelected;
+  final double size;
+  final GameSettings gameSettings;
+  final GlobalSettings globalSettings;
+  final VoidCallback onPressed;
+
+  static const Color buttonColorActive = Colors.blue;
+  static const Color buttonColorInactive = Colors.white;
+  static const double buttonBorderWidth = 4.0;
+  static const double buttonBorderRadius = 12.0;
+
+  @override
+  Widget build(BuildContext context) {
+    Widget content = const SizedBox.shrink();
+
+    switch (code) {
+      case DefaultGameSettings.parameterCodeGameMode:
+        content = getGameModeParameterItem();
+        break;
+      case DefaultGameSettings.parameterCodeSize:
+        content = getSizeParameterItem(value);
+        break;
+      default:
+        printlog('Unknown parameter: $code/$value');
+        content = getUnknownParameterItem();
+    }
+
+    final Color buttonColor = isSelected ? buttonColorActive : buttonColorInactive;
+
+    return Container(
+      decoration: BoxDecoration(
+        color: buttonColor,
+        borderRadius: BorderRadius.circular(buttonBorderRadius),
+        border: Border.all(
+          color: buttonColor,
+          width: buttonBorderWidth,
+        ),
+      ),
+      child: content,
+    );
+  }
+
+  // "unknown" parameter -> simple block with text
+  Widget getUnknownParameterItem() {
+    return StyledButton.text(
+      caption: '$code / $value',
+      color: Colors.grey,
+      onPressed: null,
+    );
+  }
+
+  Widget getGameModeParameterItem() {
+    String text = '';
+    Color baseColor = Colors.grey;
+
+    switch (value) {
+      case DefaultGameSettings.gameModeHumanVsHuman:
+        text = '🧑  🧑';
+        baseColor = Colors.green;
+        break;
+      case DefaultGameSettings.gameModeHumanVsRobot:
+        text = '🧑  🤖';
+        baseColor = Colors.pink;
+        break;
+      case DefaultGameSettings.gameModeRobotVsHuman:
+        text = '🤖  🧑';
+        baseColor = Colors.pink;
+        break;
+      case DefaultGameSettings.gameModeRobotVsRobot:
+        text = '🤖  🤖';
+        baseColor = Colors.brown;
+        break;
+      default:
+    }
+
+    return StyledButton.text(
+      caption: text,
+      color: baseColor,
+      onPressed: onPressed,
+    );
+  }
+
+  Widget getSizeParameterItem(final String value) {
+    Color backgroundColor = Colors.grey;
+
+    switch (value) {
+      case DefaultGameSettings.sizeValueMedium:
+        backgroundColor = Colors.orange;
+        break;
+      default:
+        printlog('Wrong value for boardSize parameter value: $value');
+    }
+
+    return StyledButton(
+      color: backgroundColor,
+      onPressed: onPressed,
+      child: CustomPaint(
+        size: Size(size, size),
+        willChange: false,
+        painter: ParameterPainter(
+          code: code,
+          value: value,
+          gameSettings: gameSettings,
+          globalSettings: globalSettings,
+        ),
+        isComplex: true,
+      ),
+    );
+  }
+}
diff --git a/lib/ui/screens/page_about.dart b/lib/ui/screens/page_about.dart
new file mode 100644
index 0000000000000000000000000000000000000000..6bb8b3077fd60d52678ff5632bd4c7a56dbc71db
--- /dev/null
+++ b/lib/ui/screens/page_about.dart
@@ -0,0 +1,41 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+
+import 'package:puissance4/ui/helpers/app_titles.dart';
+
+class PageAbout extends StatelessWidget {
+  const PageAbout({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(horizontal: 8),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.start,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        mainAxisSize: MainAxisSize.max,
+        children: <Widget>[
+          const SizedBox(height: 8),
+          const AppTitle(text: 'about_title'),
+          const Text('about_content').tr(),
+          FutureBuilder<PackageInfo>(
+            future: PackageInfo.fromPlatform(),
+            builder: (context, snapshot) {
+              switch (snapshot.connectionState) {
+                case ConnectionState.done:
+                  return const Text('about_version').tr(
+                    namedArgs: {
+                      'version': snapshot.data!.version,
+                    },
+                  );
+                default:
+                  return const SizedBox();
+              }
+            },
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/lib/ui/screens/page_game.dart b/lib/ui/screens/page_game.dart
new file mode 100644
index 0000000000000000000000000000000000000000..b183a162d6519dc16b24bd55853ae10e96b3dd36
--- /dev/null
+++ b/lib/ui/screens/page_game.dart
@@ -0,0 +1,24 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:puissance4/cubit/game_cubit.dart';
+import 'package:puissance4/models/game/game.dart';
+import 'package:puissance4/ui/layouts/parameters_layout.dart';
+import 'package:puissance4/ui/layouts/game_layout.dart';
+
+class PageGame extends StatelessWidget {
+  const PageGame({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameCubit, GameState>(
+      builder: (BuildContext context, GameState gameState) {
+        final Game currentGame = gameState.currentGame;
+
+        return currentGame.isRunning
+            ? const GameLayout()
+            : ParametersLayout(canResume: currentGame.canBeResumed);
+      },
+    );
+  }
+}
diff --git a/lib/ui/screens/page_settings.dart b/lib/ui/screens/page_settings.dart
new file mode 100644
index 0000000000000000000000000000000000000000..5fefd76be159ccf8c95869ae563f01847e36bcf6
--- /dev/null
+++ b/lib/ui/screens/page_settings.dart
@@ -0,0 +1,26 @@
+import 'package:flutter/material.dart';
+
+import 'package:puissance4/ui/helpers/app_titles.dart';
+import 'package:puissance4/ui/settings/settings_form.dart';
+
+class PageSettings extends StatelessWidget {
+  const PageSettings({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return const Padding(
+      padding: EdgeInsets.symmetric(horizontal: 8),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.start,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        mainAxisSize: MainAxisSize.max,
+        children: <Widget>[
+          SizedBox(height: 8),
+          AppTitle(text: 'settings_title'),
+          SizedBox(height: 8),
+          SettingsForm(),
+        ],
+      ),
+    );
+  }
+}
diff --git a/lib/ui/settings/settings_form.dart b/lib/ui/settings/settings_form.dart
new file mode 100644
index 0000000000000000000000000000000000000000..cbd1e496f8deacbaffd98a33db98a3f848db6688
--- /dev/null
+++ b/lib/ui/settings/settings_form.dart
@@ -0,0 +1,63 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:unicons/unicons.dart';
+
+import 'package:puissance4/ui/settings/theme_card.dart';
+
+class SettingsForm extends StatefulWidget {
+  const SettingsForm({super.key});
+
+  @override
+  State<SettingsForm> createState() => _SettingsFormState();
+}
+
+class _SettingsFormState extends State<SettingsForm> {
+  @override
+  void dispose() {
+    super.dispose();
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.start,
+      crossAxisAlignment: CrossAxisAlignment.start,
+      mainAxisSize: MainAxisSize.max,
+      children: <Widget>[
+        // Light/dark theme
+        Row(
+          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+          crossAxisAlignment: CrossAxisAlignment.center,
+          children: <Widget>[
+            const Text('settings_label_theme').tr(),
+            const Row(
+              mainAxisAlignment: MainAxisAlignment.end,
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: [
+                ThemeCard(
+                  mode: ThemeMode.system,
+                  icon: UniconsLine.cog,
+                ),
+                ThemeCard(
+                  mode: ThemeMode.light,
+                  icon: UniconsLine.sun,
+                ),
+                ThemeCard(
+                  mode: ThemeMode.dark,
+                  icon: UniconsLine.moon,
+                )
+              ],
+            ),
+          ],
+        ),
+
+        const SizedBox(height: 16),
+      ],
+    );
+  }
+}
diff --git a/lib/ui/settings/theme_card.dart b/lib/ui/settings/theme_card.dart
new file mode 100644
index 0000000000000000000000000000000000000000..70247310f825ab5d95095eb426ebc55d5a451308
--- /dev/null
+++ b/lib/ui/settings/theme_card.dart
@@ -0,0 +1,47 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:puissance4/cubit/theme_cubit.dart';
+
+class ThemeCard extends StatelessWidget {
+  const ThemeCard({
+    super.key,
+    required this.mode,
+    required this.icon,
+  });
+
+  final IconData icon;
+  final ThemeMode mode;
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<ThemeCubit, ThemeModeState>(
+      builder: (BuildContext context, ThemeModeState state) {
+        return Card(
+          elevation: 2,
+          shadowColor: Theme.of(context).colorScheme.shadow,
+          color: state.themeMode == mode
+              ? Theme.of(context).colorScheme.primary
+              : Theme.of(context).colorScheme.surface,
+          shape: const RoundedRectangleBorder(
+            borderRadius: BorderRadius.all(Radius.circular(12)),
+          ),
+          margin: const EdgeInsets.all(5),
+          child: InkWell(
+            onTap: () => BlocProvider.of<ThemeCubit>(context).getTheme(
+              ThemeModeState(themeMode: mode),
+            ),
+            borderRadius: const BorderRadius.all(Radius.circular(12)),
+            child: Icon(
+              icon,
+              size: 32,
+              color: state.themeMode != mode
+                  ? Theme.of(context).colorScheme.primary
+                  : Colors.white,
+            ),
+          ),
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/skeleton.dart b/lib/ui/skeleton.dart
new file mode 100644
index 0000000000000000000000000000000000000000..4a806ca2593550f205fde10a56cd47b39186e249
--- /dev/null
+++ b/lib/ui/skeleton.dart
@@ -0,0 +1,34 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:puissance4/config/menu.dart';
+import 'package:puissance4/cubit/nav_cubit.dart';
+import 'package:puissance4/ui/widgets/global_app_bar.dart';
+
+class SkeletonScreen extends StatelessWidget {
+  const SkeletonScreen({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: const GlobalAppBar(),
+      extendBodyBehindAppBar: false,
+      body: Material(
+        color: Theme.of(context).colorScheme.surface,
+        child: BlocBuilder<NavCubit, int>(
+          builder: (BuildContext context, int pageIndex) {
+            return Padding(
+              padding: const EdgeInsets.only(
+                top: 8,
+                left: 2,
+                right: 2,
+              ),
+              child: Menu.getPageWidget(pageIndex),
+            );
+          },
+        ),
+      ),
+      backgroundColor: Theme.of(context).colorScheme.surface,
+    );
+  }
+}
diff --git a/lib/ui/widgets/actions/button_delete_saved_game.dart b/lib/ui/widgets/actions/button_delete_saved_game.dart
new file mode 100644
index 0000000000000000000000000000000000000000..58e217ace6f0c2224277f685e695195ecd4555f7
--- /dev/null
+++ b/lib/ui/widgets/actions/button_delete_saved_game.dart
@@ -0,0 +1,23 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:puissance4/cubit/game_cubit.dart';
+import 'package:puissance4/ui/helpers/styled_button.dart';
+
+class DeleteSavedGameButton extends StatelessWidget {
+  const DeleteSavedGameButton({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return StyledButton(
+      color: Colors.grey,
+      onPressed: () {
+        BlocProvider.of<GameCubit>(context).deleteSavedGame();
+      },
+      child: const Image(
+        image: AssetImage('assets/ui/button_delete_saved_game.png'),
+        fit: BoxFit.fill,
+      ),
+    );
+  }
+}
diff --git a/lib/ui/widgets/actions/button_game_quit.dart b/lib/ui/widgets/actions/button_game_quit.dart
new file mode 100644
index 0000000000000000000000000000000000000000..1b955e6e6e598710ff2f34dcbaace8b28986f5fa
--- /dev/null
+++ b/lib/ui/widgets/actions/button_game_quit.dart
@@ -0,0 +1,23 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:puissance4/cubit/game_cubit.dart';
+import 'package:puissance4/ui/helpers/styled_button.dart';
+
+class QuitGameButton extends StatelessWidget {
+  const QuitGameButton({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return StyledButton(
+      color: Colors.red,
+      onPressed: () {
+        BlocProvider.of<GameCubit>(context).quitGame();
+      },
+      child: const Image(
+        image: AssetImage('assets/ui/button_back.png'),
+        fit: BoxFit.fill,
+      ),
+    );
+  }
+}
diff --git a/lib/ui/widgets/actions/button_game_start_new.dart b/lib/ui/widgets/actions/button_game_start_new.dart
new file mode 100644
index 0000000000000000000000000000000000000000..d66bd548bb4e9347d0bb129b61d7f1c3a1901653
--- /dev/null
+++ b/lib/ui/widgets/actions/button_game_start_new.dart
@@ -0,0 +1,36 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:puissance4/cubit/game_cubit.dart';
+import 'package:puissance4/cubit/settings_game_cubit.dart';
+import 'package:puissance4/cubit/settings_global_cubit.dart';
+import 'package:puissance4/ui/helpers/styled_button.dart';
+
+class StartNewGameButton extends StatelessWidget {
+  const StartNewGameButton({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameSettingsCubit, GameSettingsState>(
+      builder: (BuildContext context, GameSettingsState gameSettingsState) {
+        return BlocBuilder<GlobalSettingsCubit, GlobalSettingsState>(
+          builder: (BuildContext context, GlobalSettingsState globalSettingsState) {
+            return StyledButton(
+              color: Colors.blue,
+              onPressed: () {
+                BlocProvider.of<GameCubit>(context).startNewGame(
+                  gameSettings: gameSettingsState.settings,
+                  globalSettings: globalSettingsState.settings,
+                );
+              },
+              child: const Image(
+                image: AssetImage('assets/ui/button_start.png'),
+                fit: BoxFit.fill,
+              ),
+            );
+          },
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/actions/button_resume_saved_game.dart b/lib/ui/widgets/actions/button_resume_saved_game.dart
new file mode 100644
index 0000000000000000000000000000000000000000..d144b1d266382f18a16d6a45e1658eb22dd27411
--- /dev/null
+++ b/lib/ui/widgets/actions/button_resume_saved_game.dart
@@ -0,0 +1,23 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:puissance4/cubit/game_cubit.dart';
+import 'package:puissance4/ui/helpers/styled_button.dart';
+
+class ResumeSavedGameButton extends StatelessWidget {
+  const ResumeSavedGameButton({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return StyledButton(
+      color: Colors.blue,
+      onPressed: () {
+        BlocProvider.of<GameCubit>(context).resumeSavedGame();
+      },
+      child: const Image(
+        image: AssetImage('assets/ui/button_resume_game.png'),
+        fit: BoxFit.fill,
+      ),
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/game_board.dart b/lib/ui/widgets/game/game_board.dart
new file mode 100644
index 0000000000000000000000000000000000000000..2ca486f089836e7261e54e5153d9c15831af21f5
--- /dev/null
+++ b/lib/ui/widgets/game/game_board.dart
@@ -0,0 +1,64 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:puissance4/cubit/game_cubit.dart';
+import 'package:puissance4/ui/helpers/styled_button.dart';
+
+class GameBoardWidget extends StatelessWidget {
+  const GameBoardWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Center(
+      child: BlocBuilder<GameCubit, GameState>(
+        builder: (BuildContext context, GameState gameState) {
+          final Color borderColor = Theme.of(context).colorScheme.onSurface;
+
+          Widget cellContent({required int row, required int col}) {
+            final cellValue = gameState.currentGame.board.getCellValue(row: row, col: col);
+            final Color color =
+                cellValue == 0 ? Colors.grey : (cellValue == 1 ? Colors.yellow : Colors.red);
+
+            return AspectRatio(
+              aspectRatio: 1,
+              child: StyledButton(
+                color: color,
+                onPressed: () {
+                  BlocProvider.of<GameCubit>(context).tapOnColumn(col: col);
+                },
+                child: SizedBox.shrink(),
+              ),
+            );
+          }
+
+          return Container(
+            margin: const EdgeInsets.all(2),
+            padding: const EdgeInsets.all(2),
+            decoration: BoxDecoration(
+              color: borderColor,
+              borderRadius: BorderRadius.circular(8),
+              border: Border.all(
+                color: borderColor,
+                width: 2,
+              ),
+            ),
+            child: Table(
+              defaultColumnWidth: const IntrinsicColumnWidth(),
+              children: [
+                for (int row = 0; row < gameState.currentGame.sizeVertical; row++)
+                  TableRow(
+                    children: [
+                      for (int col = 0; col < gameState.currentGame.sizeHorizontal; col++)
+                        Column(
+                          children: [cellContent(row: row, col: col)],
+                        ),
+                    ],
+                  ),
+              ],
+            ),
+          );
+        },
+      ),
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/game_bottom.dart b/lib/ui/widgets/game/game_bottom.dart
new file mode 100644
index 0000000000000000000000000000000000000000..10574ea26226db727d4a3bd32737e7603bcba030
--- /dev/null
+++ b/lib/ui/widgets/game/game_bottom.dart
@@ -0,0 +1,29 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:puissance4/cubit/game_cubit.dart';
+import 'package:puissance4/models/game/game.dart';
+import 'package:puissance4/ui/helpers/styled_button.dart';
+
+class GameBottomWidget extends StatelessWidget {
+  const GameBottomWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameCubit, GameState>(
+      builder: (BuildContext context, GameState gameState) {
+        final Game currentGame = gameState.currentGame;
+
+        final Color color = currentGame.currentPlayer == 0
+            ? Colors.grey
+            : (currentGame.currentPlayer == 1 ? Colors.yellow : Colors.red);
+
+        return StyledButton(
+          color: color,
+          onPressed: () {},
+          child: SizedBox.shrink(),
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/game_end.dart b/lib/ui/widgets/game/game_end.dart
new file mode 100644
index 0000000000000000000000000000000000000000..053e6043d404a8d2d8b81fa2c6e8d67ed875653c
--- /dev/null
+++ b/lib/ui/widgets/game/game_end.dart
@@ -0,0 +1,74 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:puissance4/cubit/game_cubit.dart';
+import 'package:puissance4/models/game/game.dart';
+import 'package:puissance4/ui/widgets/actions/button_game_quit.dart';
+
+class GameEndWidget extends StatelessWidget {
+  const GameEndWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameCubit, GameState>(
+      builder: (BuildContext context, GameState gameState) {
+        final Game currentGame = gameState.currentGame;
+
+        final bool connected4 = currentGame.connected4();
+
+        final String player1ImageAsset = (currentGame.currentPlayer == 1 && connected4)
+            ? 'assets/ui/game_win.png'
+            : (currentGame.currentPlayer == 2 && connected4)
+                ? 'assets/ui/game_fail.png'
+                : 'assets/ui/game_draw.png';
+        final String player2ImageAsset = (currentGame.currentPlayer == 2 && connected4)
+            ? 'assets/ui/game_win.png'
+            : (currentGame.currentPlayer == 1 && connected4)
+                ? 'assets/ui/game_fail.png'
+                : 'assets/ui/game_draw.png';
+
+        final Image player1Image = Image(
+          image: AssetImage(player1ImageAsset),
+          fit: BoxFit.fill,
+        );
+
+        final Image player2Image = Image(
+          image: AssetImage(player2ImageAsset),
+          fit: BoxFit.fill,
+        );
+
+        return Container(
+          margin: const EdgeInsets.all(2),
+          padding: const EdgeInsets.all(2),
+          child: Table(
+            defaultColumnWidth: const IntrinsicColumnWidth(),
+            defaultVerticalAlignment: TableCellVerticalAlignment.bottom,
+            children: [
+              TableRow(
+                children: [
+                  Column(children: [player1Image]),
+                  Column(children: [player1Image]),
+                  Column(
+                    children: [
+                      currentGame.animationInProgress
+                          ? Image(
+                              image: const AssetImage('assets/ui/placeholder.png'),
+                              fit: BoxFit.fill,
+                            )
+                          : SizedBox.square(
+                              dimension: 70,
+                              child: QuitGameButton(),
+                            ),
+                    ],
+                  ),
+                  Column(children: [player2Image]),
+                  Column(children: [player2Image]),
+                ],
+              ),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/global_app_bar.dart b/lib/ui/widgets/global_app_bar.dart
new file mode 100644
index 0000000000000000000000000000000000000000..3782f87ecefecef3b8b989d2383d57b844971054
--- /dev/null
+++ b/lib/ui/widgets/global_app_bar.dart
@@ -0,0 +1,85 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:puissance4/config/menu.dart';
+import 'package:puissance4/cubit/game_cubit.dart';
+import 'package:puissance4/cubit/nav_cubit.dart';
+import 'package:puissance4/models/game/game.dart';
+import 'package:puissance4/ui/helpers/app_titles.dart';
+import 'package:puissance4/ui/helpers/styled_button.dart';
+
+class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget {
+  const GlobalAppBar({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameCubit, GameState>(
+      builder: (BuildContext context, GameState gameState) {
+        return BlocBuilder<NavCubit, int>(
+          builder: (BuildContext context, int pageIndex) {
+            final Game currentGame = gameState.currentGame;
+
+            final List<Widget> menuActions = [];
+
+            if (currentGame.isRunning && !currentGame.isFinished) {
+              menuActions.add(StyledButton(
+                color: Colors.red,
+                onPressed: () {},
+                onLongPress: () {
+                  BlocProvider.of<GameCubit>(context).quitGame();
+                },
+                child: const Image(
+                  image: AssetImage('assets/ui/button_back.png'),
+                  fit: BoxFit.fill,
+                ),
+              ));
+            } else {
+              if (pageIndex == Menu.indexGame) {
+                // go to Settings page
+                menuActions.add(ElevatedButton(
+                  onPressed: () {
+                    context.read<NavCubit>().goToSettingsPage();
+                  },
+                  style: ElevatedButton.styleFrom(
+                    shape: const CircleBorder(),
+                  ),
+                  child: Menu.menuItemSettings.icon,
+                ));
+
+                // go to About page
+                menuActions.add(ElevatedButton(
+                  onPressed: () {
+                    context.read<NavCubit>().goToAboutPage();
+                  },
+                  style: ElevatedButton.styleFrom(
+                    shape: const CircleBorder(),
+                  ),
+                  child: Menu.menuItemAbout.icon,
+                ));
+              } else {
+                // back to Home page
+                menuActions.add(ElevatedButton(
+                  onPressed: () {
+                    context.read<NavCubit>().goToGamePage();
+                  },
+                  style: ElevatedButton.styleFrom(
+                    shape: const CircleBorder(),
+                  ),
+                  child: Menu.menuItemGame.icon,
+                ));
+              }
+            }
+
+            return AppBar(
+              title: const AppHeader(text: 'app_name'),
+              actions: menuActions,
+            );
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Size get preferredSize => const Size.fromHeight(50);
+}
diff --git a/lib/utils/color_extensions.dart b/lib/utils/color_extensions.dart
new file mode 100644
index 0000000000000000000000000000000000000000..4e55e338f0d3ed98b233d1ef887b7b3e17e29d97
--- /dev/null
+++ b/lib/utils/color_extensions.dart
@@ -0,0 +1,33 @@
+import 'dart:ui';
+
+extension ColorExtension on Color {
+  Color darken([int percent = 40]) {
+    assert(1 <= percent && percent <= 100);
+    final value = 1 - percent / 100;
+    return Color.fromARGB(
+      alpha,
+      (red * value).round(),
+      (green * value).round(),
+      (blue * value).round(),
+    );
+  }
+
+  Color lighten([int percent = 40]) {
+    assert(1 <= percent && percent <= 100);
+    final value = percent / 100;
+    return Color.fromARGB(
+      alpha,
+      (red + ((255 - red) * value)).round(),
+      (green + ((255 - green) * value)).round(),
+      (blue + ((255 - blue) * value)).round(),
+    );
+  }
+
+  Color avg(Color other) {
+    final red = (this.red + other.red) ~/ 2;
+    final green = (this.green + other.green) ~/ 2;
+    final blue = (this.blue + other.blue) ~/ 2;
+    final alpha = (this.alpha + other.alpha) ~/ 2;
+    return Color.fromARGB(alpha, red, green, blue);
+  }
+}
diff --git a/pubspec.lock b/pubspec.lock
index f6caeef09201deb0f5cb2a81c1d6dd62fdb9c724..229631c1f86719287b9acdfad6debc0e9f46254d 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -1,6 +1,38 @@
 # Generated by pub
 # See https://dart.dev/tools/pub/glossary#lockfile
 packages:
+  args:
+    dependency: transitive
+    description:
+      name: args
+      sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.5.0"
+  async:
+    dependency: transitive
+    description:
+      name: async
+      sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.11.0"
+  auto_size_text:
+    dependency: "direct main"
+    description:
+      name: auto_size_text
+      sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.0"
+  bloc:
+    dependency: transitive
+    description:
+      name: bloc
+      sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
+      url: "https://pub.dev"
+    source: hosted
+    version: "8.1.4"
   characters:
     dependency: transitive
     description:
@@ -9,6 +41,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.3.0"
+  clock:
+    dependency: transitive
+    description:
+      name: clock
+      sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.1"
   collection:
     dependency: transitive
     description:
@@ -17,48 +57,362 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.18.0"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.5"
+  easy_localization:
+    dependency: "direct main"
+    description:
+      name: easy_localization
+      sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.7"
+  easy_logger:
+    dependency: transitive
+    description:
+      name: easy_logger
+      sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.0.2"
+  equatable:
+    dependency: "direct main"
+    description:
+      name: equatable
+      sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.5"
+  ffi:
+    dependency: transitive
+    description:
+      name: ffi
+      sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.3"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.1"
   flutter:
     dependency: "direct main"
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_bloc:
+    dependency: "direct main"
+    description:
+      name: flutter_bloc
+      sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
+      url: "https://pub.dev"
+    source: hosted
+    version: "8.1.6"
   flutter_lints:
     dependency: "direct dev"
     description:
       name: flutter_lints
-      sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
+      sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.1"
+    version: "5.0.0"
+  flutter_localizations:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_web_plugins:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  hive:
+    dependency: "direct main"
+    description:
+      name: hive
+      sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.3"
+  http:
+    dependency: transitive
+    description:
+      name: http
+      sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.2"
+  http_parser:
+    dependency: transitive
+    description:
+      name: http_parser
+      sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.0.2"
+  hydrated_bloc:
+    dependency: "direct main"
+    description:
+      name: hydrated_bloc
+      sha256: af35b357739fe41728df10bec03aad422cdc725a1e702e03af9d2a41ea05160c
+      url: "https://pub.dev"
+    source: hosted
+    version: "9.1.5"
+  intl:
+    dependency: transitive
+    description:
+      name: intl
+      sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.19.0"
   lints:
     dependency: transitive
     description:
       name: lints
-      sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
+      sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.0"
+    version: "5.0.0"
   material_color_utilities:
     dependency: transitive
     description:
       name: material_color_utilities
-      sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
+      sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
       url: "https://pub.dev"
     source: hosted
-    version: "0.8.0"
+    version: "0.11.1"
   meta:
     dependency: transitive
     description:
       name: meta
-      sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
+      sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.15.0"
+  nested:
+    dependency: transitive
+    description:
+      name: nested
+      sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.0"
+  package_info_plus:
+    dependency: "direct main"
+    description:
+      name: package_info_plus
+      sha256: "894f37107424311bdae3e476552229476777b8752c5a2a2369c0cb9a2d5442ef"
+      url: "https://pub.dev"
+    source: hosted
+    version: "8.0.3"
+  package_info_plus_platform_interface:
+    dependency: transitive
+    description:
+      name: package_info_plus_platform_interface
+      sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.1"
+  path:
+    dependency: transitive
+    description:
+      name: path
+      sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.9.0"
+  path_provider:
+    dependency: "direct main"
+    description:
+      name: path_provider
+      sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
       url: "https://pub.dev"
     source: hosted
-    version: "1.12.0"
+    version: "2.1.4"
+  path_provider_android:
+    dependency: transitive
+    description:
+      name: path_provider_android
+      sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.12"
+  path_provider_foundation:
+    dependency: transitive
+    description:
+      name: path_provider_foundation
+      sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.0"
+  path_provider_linux:
+    dependency: transitive
+    description:
+      name: path_provider_linux
+      sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.1"
+  path_provider_platform_interface:
+    dependency: transitive
+    description:
+      name: path_provider_platform_interface
+      sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  path_provider_windows:
+    dependency: transitive
+    description:
+      name: path_provider_windows
+      sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.0"
+  platform:
+    dependency: transitive
+    description:
+      name: platform
+      sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.5"
+  plugin_platform_interface:
+    dependency: transitive
+    description:
+      name: plugin_platform_interface
+      sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.8"
+  provider:
+    dependency: transitive
+    description:
+      name: provider
+      sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.1.2"
+  shared_preferences:
+    dependency: transitive
+    description:
+      name: shared_preferences
+      sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.2"
+  shared_preferences_android:
+    dependency: transitive
+    description:
+      name: shared_preferences_android
+      sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.3"
+  shared_preferences_foundation:
+    dependency: transitive
+    description:
+      name: shared_preferences_foundation
+      sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.5.3"
+  shared_preferences_linux:
+    dependency: transitive
+    description:
+      name: shared_preferences_linux
+      sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.1"
+  shared_preferences_platform_interface:
+    dependency: transitive
+    description:
+      name: shared_preferences_platform_interface
+      sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.1"
+  shared_preferences_web:
+    dependency: transitive
+    description:
+      name: shared_preferences_web
+      sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.2"
+  shared_preferences_windows:
+    dependency: transitive
+    description:
+      name: shared_preferences_windows
+      sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.1"
   sky_engine:
     dependency: transitive
     description: flutter
     source: sdk
     version: "0.0.99"
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.10.0"
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
+  synchronized:
+    dependency: transitive
+    description:
+      name: synchronized
+      sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.3.0+3"
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.1"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.2"
+  unicons:
+    dependency: "direct main"
+    description:
+      name: unicons
+      sha256: f3eab9d87c226415ef857cfd2167e1d12ad81ea1f5783b46cf644224fea4eab7
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.0"
   vector_math:
     dependency: transitive
     description:
@@ -67,5 +421,30 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.4"
+  web:
+    dependency: transitive
+    description:
+      name: web
+      sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.0"
+  win32:
+    dependency: transitive
+    description:
+      name: win32
+      sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec"
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.5.5"
+  xdg_directories:
+    dependency: transitive
+    description:
+      name: xdg_directories
+      sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.0"
 sdks:
-  dart: ">=3.3.0-0 <4.0.0"
+  dart: ">=3.5.0 <4.0.0"
+  flutter: ">=3.24.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 55712aa189e5a9257a161b0ae6e35df0a5cc304f..f4ab7adb621b861e688ccb18170e0afc43cc5593 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,17 +1,48 @@
 name: puissance4
 description: puissance4
-publish_to: 'none'
-version: 1.0.21+22
+
+publish_to: "none"
+
+version: 1.1.0+23
 
 environment:
-  sdk: '^3.0.0'
+  sdk: "^3.0.0"
 
 dependencies:
   flutter:
     sdk: flutter
 
+  # base
+  auto_size_text: ^3.0.0
+  easy_localization: ^3.0.1
+  equatable: ^2.0.5
+  flutter_bloc: ^8.1.1
+  hive: ^2.2.3
+  hydrated_bloc: ^9.0.0
+  package_info_plus: ^8.0.0
+  path_provider: ^2.0.11
+  unicons: ^3.0.0
+
+  # specific
+  # (none)
+
 dev_dependencies:
-  flutter_lints: ^3.0.1
+  flutter_lints: ^5.0.0
 
 flutter:
   uses-material-design: true
+  assets:
+    - assets/translations/
+    - assets/ui/
+
+  fonts:
+    - family: Nunito
+      fonts:
+        - asset: assets/fonts/Nunito-Bold.ttf
+          weight: 700
+        - asset: assets/fonts/Nunito-Medium.ttf
+          weight: 500
+        - asset: assets/fonts/Nunito-Regular.ttf
+          weight: 400
+        - asset: assets/fonts/Nunito-Light.ttf
+          weight: 300
diff --git a/icons/build_application_icons.sh b/resources/app/build_application_resources.sh
similarity index 56%
rename from icons/build_application_icons.sh
rename to resources/app/build_application_resources.sh
index 27dbe2647fe4e6d562fbd99451716d1b7d448570..1ace90d0e0029bf1704122d2b60bced59d5ed348 100755
--- a/icons/build_application_icons.sh
+++ b/resources/app/build_application_resources.sh
@@ -1,12 +1,21 @@
 #! /bin/bash
 
 # Check dependencies
-command -v inkscape >/dev/null 2>&1 || { echo >&2 "I require inkscape but it's not installed. Aborting."; exit 1; }
-command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not installed. Aborting."; exit 1; }
-command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; }
+command -v inkscape >/dev/null 2>&1 || {
+  echo >&2 "I require inkscape but it's not installed. Aborting."
+  exit 1
+}
+command -v scour >/dev/null 2>&1 || {
+  echo >&2 "I require scour but it's not installed. Aborting."
+  exit 1
+}
+command -v optipng >/dev/null 2>&1 || {
+  echo >&2 "I require optipng but it's not installed. Aborting."
+  exit 1
+}
 
 CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
-BASE_DIR="$(dirname "${CURRENT_DIR}")"
+BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")"
 
 SOURCE_ICON="${CURRENT_DIR}/icon.svg"
 SOURCE_FASTLANE="${CURRENT_DIR}/featureGraphic.svg"
@@ -31,14 +40,14 @@ function optimize_svg() {
 
   cp ${SVG} ${SVG}.tmp
   scour \
-      --remove-descriptive-elements \
-      --enable-id-stripping \
-      --enable-viewboxing \
-      --enable-comment-stripping \
-      --nindent=4 \
-      --quiet \
-      -i ${SVG}.tmp \
-      -o ${SVG}
+    --remove-descriptive-elements \
+    --enable-id-stripping \
+    --enable-viewboxing \
+    --enable-comment-stripping \
+    --nindent=4 \
+    --quiet \
+    -i ${SVG}.tmp \
+    -o ${SVG}
   rm ${SVG}.tmp
 }
 
@@ -57,10 +66,10 @@ function build_application_icon() {
   TARGET_PNG="${TARGET}.png"
 
   inkscape \
-      --export-width=${ICON_SIZE} \
-      --export-height=${ICON_SIZE} \
-      --export-filename=${TARGET_PNG} \
-      ${SOURCE_ICON}
+    --export-width=${ICON_SIZE} \
+    --export-height=${ICON_SIZE} \
+    --export-filename=${TARGET_PNG} \
+    ${SOURCE_ICON}
 
   optipng ${OPTIPNG_OPTIONS} ${TARGET_PNG}
 }
@@ -76,10 +85,10 @@ function build_fastlane_image() {
   TARGET_PNG="${TARGET}.png"
 
   inkscape \
-      --export-width=${WIDTH} \
-      --export-height=${HEIGHT} \
-      --export-filename=${TARGET_PNG} \
-      ${SOURCE_FASTLANE}
+    --export-width=${WIDTH} \
+    --export-height=${HEIGHT} \
+    --export-filename=${TARGET_PNG} \
+    ${SOURCE_FASTLANE}
 
   optipng ${OPTIPNG_OPTIONS} ${TARGET_PNG}
 }
@@ -94,24 +103,24 @@ function build_launch_image() {
   TARGET_PNG="${TARGET}.png"
 
   inkscape \
-      --export-width=${ICON_SIZE} \
-      --export-height=${ICON_SIZE} \
-      --export-filename=${TARGET_PNG} \
-      ${SOURCE_LAUNCH_IMAGE}
+    --export-width=${ICON_SIZE} \
+    --export-height=${ICON_SIZE} \
+    --export-filename=${TARGET_PNG} \
+    ${SOURCE_LAUNCH_IMAGE}
 
   optipng ${OPTIPNG_OPTIONS} ${TARGET_PNG}
 }
 
-build_application_icon  72 ${BASE_DIR}/android/app/src/main/res/mipmap-hdpi/ic_launcher
-build_application_icon  48 ${BASE_DIR}/android/app/src/main/res/mipmap-mdpi/ic_launcher
-build_application_icon  96 ${BASE_DIR}/android/app/src/main/res/mipmap-xhdpi/ic_launcher
+build_application_icon 72 ${BASE_DIR}/android/app/src/main/res/mipmap-hdpi/ic_launcher
+build_application_icon 48 ${BASE_DIR}/android/app/src/main/res/mipmap-mdpi/ic_launcher
+build_application_icon 96 ${BASE_DIR}/android/app/src/main/res/mipmap-xhdpi/ic_launcher
 build_application_icon 144 ${BASE_DIR}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher
 build_application_icon 192 ${BASE_DIR}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher
 build_application_icon 512 ${BASE_DIR}/fastlane/metadata/android/en-US/images/icon
 
-build_launch_image  72 ${BASE_DIR}/android/app/src/main/res/mipmap-hdpi/launch_image
-build_launch_image  48 ${BASE_DIR}/android/app/src/main/res/mipmap-mdpi/launch_image
-build_launch_image  96 ${BASE_DIR}/android/app/src/main/res/mipmap-xhdpi/launch_image
+build_launch_image 72 ${BASE_DIR}/android/app/src/main/res/mipmap-hdpi/launch_image
+build_launch_image 48 ${BASE_DIR}/android/app/src/main/res/mipmap-mdpi/launch_image
+build_launch_image 96 ${BASE_DIR}/android/app/src/main/res/mipmap-xhdpi/launch_image
 build_launch_image 144 ${BASE_DIR}/android/app/src/main/res/mipmap-xxhdpi/launch_image
 build_launch_image 192 ${BASE_DIR}/android/app/src/main/res/mipmap-xxxhdpi/launch_image
 
diff --git a/icons/featureGraphic.svg b/resources/app/featureGraphic.svg
similarity index 100%
rename from icons/featureGraphic.svg
rename to resources/app/featureGraphic.svg
diff --git a/icons/icon.svg b/resources/app/icon.svg
similarity index 100%
rename from icons/icon.svg
rename to resources/app/icon.svg
diff --git a/resources/build_resources.sh b/resources/build_resources.sh
new file mode 100755
index 0000000000000000000000000000000000000000..774953c5b885aae73f710aaa9d8b55a0d8dcc2c0
--- /dev/null
+++ b/resources/build_resources.sh
@@ -0,0 +1,6 @@
+#! /bin/bash
+
+CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
+
+${CURRENT_DIR}/app/build_application_resources.sh
+${CURRENT_DIR}/ui/build_ui_resources.sh
diff --git a/resources/ui/build_ui_resources.sh b/resources/ui/build_ui_resources.sh
new file mode 100755
index 0000000000000000000000000000000000000000..f6c3f33201ae81b1f43cf677bb76c91e04727f33
--- /dev/null
+++ b/resources/ui/build_ui_resources.sh
@@ -0,0 +1,144 @@
+#! /bin/bash
+
+# Check dependencies
+command -v inkscape >/dev/null 2>&1 || {
+  echo >&2 "I require inkscape but it's not installed. Aborting."
+  exit 1
+}
+command -v scour >/dev/null 2>&1 || {
+  echo >&2 "I require scour but it's not installed. Aborting."
+  exit 1
+}
+command -v optipng >/dev/null 2>&1 || {
+  echo >&2 "I require optipng but it's not installed. Aborting."
+  exit 1
+}
+
+CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
+BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")"
+ASSETS_DIR="${BASE_DIR}/assets"
+
+OPTIPNG_OPTIONS="-preserve -quiet -o7"
+IMAGE_SIZE=192
+
+#######################################################
+
+# Game images (svg files found in `images` folder)
+AVAILABLE_GAME_SVG_IMAGES=""
+AVAILABLE_GAME_PNG_IMAGES=""
+if [ -d "${CURRENT_DIR}/images" ]; then
+  AVAILABLE_GAME_SVG_IMAGES="$(find "${CURRENT_DIR}/images" -type f -name "*.svg" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort)"
+  AVAILABLE_GAME_PNG_IMAGES="$(find "${CURRENT_DIR}/images" -type f -name "*.png" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort)"
+fi
+
+# Skins (subfolders found in `skins` folder)
+AVAILABLE_SKINS=""
+if [ -d "${CURRENT_DIR}/skins" ]; then
+  AVAILABLE_SKINS="$(find "${CURRENT_DIR}/skins" -mindepth 1 -type d | awk -F/ '{print $NF}')"
+fi
+
+# Images per skin (svg files found recursively in `skins` folder and subfolders)
+SKIN_SVG_IMAGES=""
+SKIN_PNG_IMAGES=""
+if [ -d "${CURRENT_DIR}/skins" ]; then
+  SKIN_SVG_IMAGES="$(find "${CURRENT_DIR}/skins" -type f -name "*.svg" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort | uniq)"
+  SKIN_PNG_IMAGES="$(find "${CURRENT_DIR}/skins" -type f -name "*.png" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort | uniq)"
+fi
+
+#######################################################
+
+# optimize svg
+function optimize_svg() {
+  SOURCE="$1"
+
+  cp ${SOURCE} ${SOURCE}.tmp
+  scour \
+    --remove-descriptive-elements \
+    --enable-id-stripping \
+    --enable-viewboxing \
+    --enable-comment-stripping \
+    --nindent=4 \
+    --quiet \
+    -i ${SOURCE}.tmp \
+    -o ${SOURCE}
+  rm ${SOURCE}.tmp
+}
+
+# build png from svg
+function build_svg_image() {
+  SOURCE="$1"
+  TARGET="$2"
+
+  echo "Building ${TARGET}"
+
+  if [ ! -f "${SOURCE}" ]; then
+    echo "Missing file: ${SOURCE}"
+    exit 1
+  fi
+
+  optimize_svg "${SOURCE}"
+
+  mkdir -p "$(dirname "${TARGET}")"
+
+  inkscape \
+    --export-width=${IMAGE_SIZE} \
+    --export-height=${IMAGE_SIZE} \
+    --export-filename=${TARGET} \
+    "${SOURCE}"
+
+  optipng ${OPTIPNG_OPTIONS} "${TARGET}"
+}
+
+# build png from png
+function build_png_image() {
+  SOURCE="$1"
+  TARGET="$2"
+
+  echo "Building ${TARGET}"
+
+  if [ ! -f "${SOURCE}" ]; then
+    echo "Missing file: ${SOURCE}"
+    exit 1
+  fi
+
+  mkdir -p "$(dirname "${TARGET}")"
+
+  convert -resize ${IMAGE_SIZE}x${IMAGE_SIZE} "${SOURCE}" "${TARGET}"
+
+  optipng ${OPTIPNG_OPTIONS} "${TARGET}"
+}
+
+function build_images_for_skin() {
+  SKIN_CODE="$1"
+
+  # skin images
+  for SKIN_SVG_IMAGE in ${SKIN_SVG_IMAGES}; do
+    build_svg_image ${CURRENT_DIR}/skins/${SKIN_CODE}/${SKIN_SVG_IMAGE}.svg ${ASSETS_DIR}/skins/${SKIN_CODE}_${SKIN_SVG_IMAGE}.png
+  done
+  for SKIN_PNG_IMAGE in ${SKIN_PNG_IMAGES}; do
+    build_png_image ${CURRENT_DIR}/skins/${SKIN_CODE}/${SKIN_PNG_IMAGE}.png ${ASSETS_DIR}/skins/${SKIN_CODE}_${SKIN_PNG_IMAGE}.png
+  done
+}
+
+#######################################################
+
+# Delete existing generated images
+if [ -d "${ASSETS_DIR}/ui" ]; then
+  find ${ASSETS_DIR}/ui -type f -name "*.png" -delete
+fi
+if [ -d "${ASSETS_DIR}/skins" ]; then
+  find ${ASSETS_DIR}/skins -type f -name "*.png" -delete
+fi
+
+# build game images
+for GAME_SVG_IMAGE in ${AVAILABLE_GAME_SVG_IMAGES}; do
+  build_svg_image ${CURRENT_DIR}/images/${GAME_SVG_IMAGE}.svg ${ASSETS_DIR}/ui/${GAME_SVG_IMAGE}.png
+done
+for GAME_PNG_IMAGE in ${AVAILABLE_GAME_PNG_IMAGES}; do
+  build_png_image ${CURRENT_DIR}/images/${GAME_PNG_IMAGE}.png ${ASSETS_DIR}/ui/${GAME_PNG_IMAGE}.png
+done
+
+# build skins images
+for SKIN in ${AVAILABLE_SKINS}; do
+  build_images_for_skin "${SKIN}"
+done
diff --git a/resources/ui/images/button_back.svg b/resources/ui/images/button_back.svg
new file mode 100644
index 0000000000000000000000000000000000000000..018d8b734d2932028fbfce1643c4e888ff1b45b1
--- /dev/null
+++ b/resources/ui/images/button_back.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path transform="matrix(1.3783 .61747 -.61747 1.3783 45.198 93.762)" d="m11.645-14.603-44.77-4.6003 26.369-36.472z" fill="#fff" stroke="#950e4f" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.2832"/></svg>
diff --git a/resources/ui/images/button_delete_saved_game.svg b/resources/ui/images/button_delete_saved_game.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c3f872e434052a6b4e7036b530ced8e6233508e4
--- /dev/null
+++ b/resources/ui/images/button_delete_saved_game.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m76.652 23.303-3.6441 58.302c-0.28153 4.5103-4.0223 8.0241-8.5413 8.0241h-35.27c-4.5189 0-8.2598-3.5138-8.5413-8.0241l-3.6441-58.302h-5.4824c-1.7723 0-3.2093-1.437-3.2093-3.2093 0-1.773 1.437-3.2093 3.2093-3.2093h70.605c1.7723 0 3.2093 1.4363 3.2093 3.2093 0 1.7723-1.437 3.2093-3.2093 3.2093zm-6.8314 0h-45.979l3.0819 55.867c0.12535 2.268 2.0008 4.0433 4.2732 4.0433h31.268c2.2724 0 4.1478-1.7752 4.2732-4.0433zm-22.99 6.4188c1.6541 0 2.9952 1.3411 2.9952 2.9952v41.08c0 1.6541-1.3411 2.9952-2.9952 2.9952-1.6542 0-2.9952-1.3411-2.9952-2.9952v-41.08c0-1.6541 1.3411-2.9952 2.9952-2.9952zm-12.837 0c1.6756 0 3.0553 1.3181 3.1312 2.9921l1.8776 41.3c0.06665 1.4664-1.0681 2.7087-2.5345 2.7762-0.04011 0.0015-0.08024 0.0021-0.12108 0.0021-1.5595 0-2.8476-1.2193-2.9328-2.7774l-2.253-41.3c-0.08524-1.5646 1.114-2.9012 2.6779-2.9864 0.05157-0.0029 0.10317-0.0042 0.15474-0.0042zm25.675 0c1.5667 0 2.8361 1.2694 2.8361 2.8361 0 0.05156-6.87e-4 0.10317-0.0036 0.15474l-2.2416 41.088c-0.09171 1.6778-1.4786 2.991-3.1586 2.991-1.5667 0-2.8361-1.2694-2.8361-2.8361 0-0.05156 7.31e-4 -0.10315 0.0036-0.15474l2.2417-41.088c0.09172-1.6778 1.4786-2.991 3.1586-2.991zm-21.397-25.675h17.117c4.7265 0 8.5578 3.8313 8.5578 8.5578v4.2795h-34.231v-4.2795c0-4.7265 3.8313-8.5578 8.5578-8.5578zm0.42837 6.4188c-1.4184 0-2.5675 1.1491-2.5675 2.5675v3.8512h21.394v-3.8512c0-1.4184-1.1491-2.5675-2.5675-2.5675z" fill="#fff" fill-rule="evenodd" stroke="#050200"/></svg>
diff --git a/resources/ui/images/button_resume_game.svg b/resources/ui/images/button_resume_game.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2bf973276aefa564ecff7d6149899298344819f9
--- /dev/null
+++ b/resources/ui/images/button_resume_game.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-5.618)" fill="#fff" stroke="#105ea2" stroke-linecap="round" stroke-linejoin="round"><path transform="matrix(-1.3783 -.61747 .61747 -1.3783 55.567 -.086035)" d="m11.645-14.603-44.77-4.6003 26.369-36.472z" stroke-width="7.2832"/><path d="m15.535 12.852 2e-3 67.973z" stroke-width="11"/></g></svg>
diff --git a/resources/ui/images/button_start.svg b/resources/ui/images/button_start.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4d7634a9f3fb559e590ee965e1341ae2634bf80f
--- /dev/null
+++ b/resources/ui/images/button_start.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path transform="matrix(-1.3783 -.61747 .61747 -1.3783 46.954 -.086035)" d="m11.645-14.603-44.77-4.6003 26.369-36.472z" fill="#fff" stroke="#105ea2" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.2832"/></svg>
diff --git a/resources/ui/images/game_draw.svg b/resources/ui/images/game_draw.svg
new file mode 100644
index 0000000000000000000000000000000000000000..d988c4937ec1d00ce581b412ff4a859a0fd1862d
--- /dev/null
+++ b/resources/ui/images/game_draw.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"/>
diff --git a/resources/ui/images/game_fail.svg b/resources/ui/images/game_fail.svg
new file mode 100644
index 0000000000000000000000000000000000000000..d988c4937ec1d00ce581b412ff4a859a0fd1862d
--- /dev/null
+++ b/resources/ui/images/game_fail.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"/>
diff --git a/resources/ui/images/game_win.svg b/resources/ui/images/game_win.svg
new file mode 100644
index 0000000000000000000000000000000000000000..fe20923864d0c5d39168eced03038b65106a596b
--- /dev/null
+++ b/resources/ui/images/game_win.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.17604 0 0 .17604 7.9341 1.7716)"><path d="m101.92 496.35c-1.8555 0-3.7109-0.69532-5.1484-2.0898-2.9297-2.8438-3-7.5234-0.15234-10.453l9.1875-9.4648c2.8438-2.9297 7.5234-3 10.453-0.15625s3 7.5234 0.15625 10.453l-9.1914 9.4648c-1.4492 1.4961-3.375 2.2461-5.3047 2.2461z" fill="#ff4e61"/><path d="m201.65 133.26c-1.8516 0-3.7109-0.69531-5.1445-2.0898-2.9297-2.8438-3-7.5234-0.15625-10.449l9.1914-9.4688c2.8438-2.9297 7.5195-3 10.449-0.15625s3 7.5234 0.15625 10.453l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#ff4e61"/><path d="m413.8 100.39c-1.8555 0-3.7109-0.69141-5.1484-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1914-9.4688c2.8438-2.9258 7.5234-2.9961 10.453-0.15234 2.9297 2.8398 3 7.5234 0.15625 10.449l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#5c73bc"/><path d="m413.8 463.77c-1.8555 0-3.7109-0.69532-5.1484-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1914-9.4688c2.8438-2.9258 7.5234-3 10.453-0.15625s3 7.5234 0.15625 10.453l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#fa0"/><path d="m63.07 112.91c-1.8516 0-3.7109-0.69141-5.1445-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1914-9.4687c2.8438-2.9258 7.5234-2.9961 10.453-0.15234 2.9258 2.8438 2.9961 7.5234 0.15234 10.449l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#fa0"/><path d="m12.309 278.82c-1.8516 0-3.7109-0.69141-5.1445-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1875-9.4688c2.8438-2.9297 7.5234-3 10.453-0.15625 2.9297 2.8438 3 7.5234 0.15625 10.453l-9.1914 9.4688c-1.4453 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#2dc471"/><path d="m216.29 278.49-23.996 12.996c-6.2226 3.3711-13.496-2.0742-12.309-9.2148l4.582-27.523c0.47266-2.8359-0.4375-5.7266-2.4375-7.7344l-19.414-19.496c-5.0352-5.0547-2.2578-13.863 4.7031-14.906l26.824-4.0156c2.7656-0.41407 5.1524-2.1992 6.3867-4.7812l12-25.043c3.1133-6.4922 12.102-6.4922 15.215 0l11.996 25.043c1.2383 2.582 3.625 4.3672 6.3867 4.7812l26.828 4.0156c6.957 1.043 9.7344 9.8516 4.6992 14.906l-19.41 19.496c-2 2.0078-2.9141 4.8984-2.4414 7.7344l4.582 27.523c1.1914 7.1406-6.082 12.586-12.305 9.2148l-23.996-12.996c-2.4727-1.3398-5.4258-1.3398-7.8945 0z" fill="#ffd02f"/><path d="m220.24 512c-4.082 0-7.3906-3.3086-7.3906-7.3906v-115.59c0-4.082 3.3086-7.3945 7.3906-7.3945s7.3906 3.3125 7.3906 7.3945v115.59c0 4.082-3.3086 7.3906-7.3906 7.3906z" fill="#5c73bc"/><path d="m220.3 357.42h-0.11328c-4.082 0-7.3945-3.3125-7.3945-7.3945s3.3086-7.3906 7.3945-7.3906h0.11328c4.082 0 7.3906 3.3086 7.3906 7.3906s-3.3086 7.3945-7.3906 7.3945z" fill="#5c73bc"/><path d="m220.3 332h-0.14838c-4.082-0.0156-7.375-3.3398-7.3594-7.4219 0.0195-4.0742 3.3242-7.3594 7.3906-7.3594h0.14848c4.082 0.0156 7.375 3.3398 7.3594 7.4219-0.0156 4.0703-3.3242 7.3594-7.3906 7.3594z" fill="#fa0"/><path d="m87.234 230.89c-1.9297 0-3.8555-0.75-5.3047-2.2422l-79.34-81.738c-2.8438-2.9297-2.7773-7.6094 0.15234-10.449 2.9297-2.8438 7.6094-2.7734 10.453 0.15235l79.344 81.738c2.8438 2.9258 2.7734 7.6094-0.15625 10.449-1.4375 1.3945-3.293 2.0898-5.1484 2.0898z" fill="#ff4e61"/><path d="m113.95 258.5c-1.8633 0-3.7266-0.69922-5.1641-2.1055-2.9219-2.8516-2.9766-7.5312-0.125-10.453l0.082-0.082c2.8516-2.918 7.5312-2.9766 10.453-0.12109 2.9219 2.8516 2.9766 7.5312 0.12109 10.453l-0.0781 0.082c-1.4492 1.4805-3.3672 2.2266-5.2891 2.2266z" fill="#fa0"/><path d="m131.4 276.48c-1.8555 0-3.7109-0.69531-5.1484-2.0898-2.9258-2.8438-2.9961-7.5234-0.15235-10.449l0.0781-0.0859c2.8476-2.9297 7.5273-2.9961 10.453-0.15235 2.9297 2.8438 3 7.5234 0.15625 10.453l-0.082 0.082c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#5c73bc"/><path d="m353.24 227.99c-1.8555 0-3.7109-0.69141-5.1445-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l79.34-81.734c2.8438-2.9297 7.5234-3 10.453-0.15625 2.9297 2.8438 3 7.5234 0.15625 10.453l-79.344 81.734c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#fa0"/><path d="m326.52 255.6c-1.9141 0-3.8242-0.73828-5.2695-2.2109l-0.082-0.082c-2.8633-2.9141-2.8203-7.5938 0.0899-10.453 2.9141-2.8633 7.5938-2.8203 10.453 0.0898l0.082 0.082c2.8594 2.9141 2.8203 7.5938-0.0937 10.453-1.4375 1.4141-3.3086 2.1211-5.1797 2.1211z" fill="#ff4e61"/><path d="m309.07 273.58c-1.9297 0-3.8555-0.75-5.3047-2.2422l-0.082-0.082c-2.8398-2.9297-2.7734-7.6094 0.15625-10.453s7.6094-2.7734 10.453 0.15234l0.082 0.082c2.8398 2.9297 2.7734 7.6094-0.15625 10.453-1.4375 1.3945-3.293 2.0898-5.1484 2.0898z" fill="#fa0"/><path d="m300.65 116.69c-1.2422 0-2.5-0.3125-3.6523-0.97266-3.5469-2.0234-4.7812-6.5391-2.7578-10.082l56.863-99.652c2.0234-3.543 6.5352-4.7773 10.082-2.7539 3.5469 2.0234 4.7812 6.5391 2.7578 10.082l-56.863 99.652c-1.3633 2.3867-3.8594 3.7266-6.4297 3.7266z" fill="#62d38f"/><path d="m281.52 150.33c-1.293 0-2.5977-0.33593-3.7891-1.0469l-0.0976-0.0586c-3.5-2.0938-4.6445-6.6328-2.5469-10.137 2.0938-3.5078 6.6328-4.6445 10.137-2.5508l0.0977 0.0586c3.5039 2.0938 4.6445 6.6328 2.5508 10.137-1.3867 2.3164-3.8359 3.5976-6.3516 3.5976z" fill="#fa0"/><path d="m269.02 172.25c-1.3008 0-2.6172-0.34375-3.8086-1.0625l-0.0977-0.0586c-3.4961-2.1094-4.6211-6.6523-2.5156-10.148 2.1094-3.4961 6.6523-4.6172 10.148-2.5117l0.0976 0.0586c3.4961 2.1094 4.6211 6.6523 2.5117 10.148-1.3867 2.3008-3.832 3.5742-6.3359 3.5742z" fill="#2dc471"/><path d="m139.96 116.69c-2.5703 0-5.0664-1.3398-6.4297-3.7305l-56.863-99.648c-2.0234-3.5469-0.78906-8.0586 2.7539-10.082 3.5469-2.0234 8.0625-0.79297 10.086 2.7539l56.863 99.648c2.0234 3.5469 0.78906 8.0625-2.7539 10.086-1.1562 0.66016-2.4141 0.97266-3.6562 0.97266z" fill="#5c73bc"/><path d="m159.09 150.33c-2.5078 0-4.957-1.2773-6.3438-3.582-2.1016-3.5-0.96875-8.043 2.5273-10.145l0.10157-0.0586c3.5-2.1016 8.0391-0.97266 10.141 2.5273 2.1055 3.5 0.97266 8.0391-2.5273 10.145l-0.0977 0.0586c-1.1914 0.71484-2.5039 1.0547-3.8008 1.0547z" fill="#ff4e61"/><path d="m171.6 172.25c-2.5 0-4.9375-1.2656-6.3281-3.5625-2.1172-3.4922-1-8.0352 2.4883-10.152l0.0977-0.0586c3.4961-2.1133 8.0391-1 10.156 2.4922 2.1133 3.4922 1 8.0352-2.4922 10.152l-0.0977 0.0586c-1.1992 0.72656-2.5195 1.0703-3.8242 1.0703z" fill="#fa0"/><path d="m402.14 357.28-15.523 11.602c-4.0234 3.0117-9.6523-0.043-9.5234-5.1641l0.5039-19.75c0.0508-2.0352-0.87109-3.9648-2.4688-5.1641l-15.508-11.621c-4.0234-3.0156-2.9453-9.4726 1.8242-10.93l18.391-5.6094c1.8906-0.57812 3.3906-2.082 4-4.0156l5.9375-18.785c1.5391-4.875 7.8359-5.8125 10.652-1.5898l10.863 16.285c1.1211 1.6758 2.9688 2.6797 4.9414 2.6797l19.18 0.0117c4.9766 4e-3 7.7891 5.8828 4.7578 9.9492l-11.676 15.672c-1.2031 1.6172-1.5586 3.7383-0.94922 5.6719l5.918 18.797c1.5312 4.875-3.0273 9.4453-7.7148 7.7344l-18.078-6.5977c-1.8594-0.67969-3.9258-0.37109-5.5273 0.82422z" fill="#ffd02f"/><path d="m261.51 512c-4.082 0-7.3906-3.3086-7.3906-7.3906 0-57.23 22.832-95.922 41.984-118.3 20.828-24.332 41.613-35.023 42.488-35.469 3.6406-1.8477 8.0898-0.39063 9.9336 3.2539 1.8438 3.6367 0.39453 8.0781-3.2422 9.9297-0.3125 0.16016-19.5 10.164-38.367 32.395-25.227 29.719-38.016 66.121-38.016 108.2 0 4.082-3.3086 7.3906-7.3906 7.3906z" fill="#ff4e61"/><path d="m102.86 397.35 11.766 15.605c3.0547 4.0469 9.2852 2.7305 10.547-2.2266l4.8633-19.113c0.5-1.9648 1.9102-3.5547 3.7695-4.2461l18.039-6.707c4.6797-1.7383 5.3906-8.25 1.207-11.016l-16.141-10.672c-1.6602-1.1016-2.6914-2.9726-2.7578-5.0039l-0.61719-19.75c-0.15625-5.1211-5.9492-7.832-9.7969-4.5859l-14.84 12.516c-1.5312 1.2891-3.5781 1.7227-5.4726 1.1562l-18.422-5.5c-4.7773-1.4258-9.0703 3.4102-7.2617 8.1836l6.9688 18.41c0.71875 1.8945 0.48438 4.0352-0.625 5.7188l-10.77 16.348c-2.793 4.2422 0.34375 9.9375 5.3125 9.6445l19.145-1.1406c1.9727-0.11719 3.875 0.77343 5.0859 2.3789z" fill="#ffd02f"/><path d="m179.02 512c-4.082 0-7.3906-3.3086-7.3906-7.3906 0-30.059-6.6797-57.559-19.852-81.734-1.9531-3.5859-0.62891-8.0742 2.957-10.027 3.5859-1.9531 8.0742-0.62891 10.027 2.9531 14.363 26.375 21.648 56.254 21.648 88.809 0 4.082-3.3086 7.3906-7.3906 7.3906z" fill="#fa0"/><path d="m268.93 55.898c0-11.285-8.8828-20.434-19.836-20.434-10.957 0-19.836 9.1484-19.836 20.434 0 11.285 8.8789 20.438 19.836 20.438 10.953 0 19.836-9.1523 19.836-20.438z" fill="#ffd02f"/><path d="m373.08 446.81c0-11.285-8.8789-20.434-19.832-20.434-10.957 0-19.836 9.1484-19.836 20.434s8.8789 20.434 19.836 20.434c10.953 0 19.832-9.1484 19.832-20.434z" fill="#5c73bc"/><path d="m44.129 450.86c0-9.0508-7.1211-16.387-15.91-16.387-8.7852 0-15.906 7.3359-15.906 16.387 0 9.0547 7.1211 16.391 15.906 16.391 8.7891 0 15.91-7.3359 15.91-16.391z" fill="#62d38f"/><path d="m88.172 288.35c0-9.0508-7.1211-16.387-15.91-16.387-8.7852 0-15.906 7.3359-15.906 16.387s7.1211 16.391 15.906 16.391c8.7891 0 15.91-7.3398 15.91-16.391z" fill="#5c73bc"/><g fill="#ff4e61"><path d="m210.84 16.391c0-9.0547-7.1211-16.391-15.906-16.391-8.7891 0-15.91 7.3359-15.91 16.391 0 9.0508 7.1211 16.387 15.91 16.387 8.7852 0 15.906-7.3359 15.906-16.387z"/><path d="m365.23 152.88c0-9.0508-7.125-16.391-15.91-16.391-8.7852 0-15.91 7.3398-15.91 16.391s7.125 16.387 15.91 16.387c8.7852 0 15.91-7.3359 15.91-16.387z"/><path d="m139.96 32.746c-1.8555 0-3.7109-0.69141-5.1484-2.0898-2.9297-2.8438-3-7.5195-0.15625-10.449l9.1914-9.4688c2.8438-2.9297 7.5234-3 10.449-0.15625 2.9297 2.8438 3 7.5234 0.15625 10.453l-9.1875 9.4688c-1.4492 1.4922-3.3789 2.2422-5.3047 2.2422z"/></g></g></svg>
diff --git a/resources/ui/images/placeholder.svg b/resources/ui/images/placeholder.svg
new file mode 100644
index 0000000000000000000000000000000000000000..23ace81fbb82a8409cc0710c0f7bddd6381f7256
--- /dev/null
+++ b/resources/ui/images/placeholder.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"/>