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/icons/button_restart.png b/assets/icons/button_restart.png
deleted file mode 100644
index ea561bc7bc18e930e84c7b768d12eb4963a778a9..0000000000000000000000000000000000000000
Binary files a/assets/icons/button_restart.png and /dev/null differ
diff --git a/assets/skins/default_tile.png b/assets/skins/default_tile.png
deleted file mode 100644
index 586de0573e7f77d97c16942f603b8ee6d9c7d7bc..0000000000000000000000000000000000000000
Binary files a/assets/skins/default_tile.png and /dev/null differ
diff --git a/assets/translations/en.json b/assets/translations/en.json
new file mode 100644
index 0000000000000000000000000000000000000000..87863ba812d671bc80c6cba6b3caa2e25520e340
--- /dev/null
+++ b/assets/translations/en.json
@@ -0,0 +1,12 @@
+{
+  "app_name": "Reversi",
+
+  "settings_title": "Settings",
+  "settings_label_theme": "Theme mode",
+
+  "about_title": "Informations",
+  "about_content": "Reversi",
+  "about_version": "Version: {version}",
+
+  "": ""
+}
diff --git a/assets/translations/fr.json b/assets/translations/fr.json
new file mode 100644
index 0000000000000000000000000000000000000000..2340d9e9b3cc2d6598b119ffe7a363efe791d793
--- /dev/null
+++ b/assets/translations/fr.json
@@ -0,0 +1,12 @@
+{
+  "app_name": "Reversi",
+
+  "settings_title": "Réglages",
+  "settings_label_theme": "Thème de couleurs",
+
+  "about_title": "Informations",
+  "about_content": "Reversi.",
+  "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/icons/empty.png b/assets/ui/empty.png
similarity index 62%
rename from assets/icons/empty.png
rename to assets/ui/empty.png
index 047508dc7427561315ec1c3834f25ab888324c06..814df31be6ddc4275ebe4490c79365578dbef1f0 100644
Binary files a/assets/icons/empty.png and b/assets/ui/empty.png differ
diff --git a/assets/ui/game_fail.png b/assets/ui/game_fail.png
new file mode 100644
index 0000000000000000000000000000000000000000..93f2801f9d6bb2ce508e1293cd64d6ff2e9970ec
Binary files /dev/null and b/assets/ui/game_fail.png differ
diff --git a/assets/icons/game_win.png b/assets/ui/game_win.png
similarity index 100%
rename from assets/icons/game_win.png
rename to assets/ui/game_win.png
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/20.txt b/fastlane/metadata/android/en-US/changelogs/20.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d3b792db0729b6b6a06eba353000bc505e0d21f5
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/20.txt
@@ -0,0 +1 @@
+Clean / improve / update code and UI..
diff --git a/fastlane/metadata/android/fr-FR/changelogs/20.txt b/fastlane/metadata/android/fr-FR/changelogs/20.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8e88019e5d88c4e6e123ad73c0c1c0dce6e4ff70
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/20.txt
@@ -0,0 +1 @@
+Nettoyage / amélioration / mise à jour de code et de l'interface.
diff --git a/icons/button_back.svg b/icons/button_back.svg
new file mode 100644
index 0000000000000000000000000000000000000000..018d8b734d2932028fbfce1643c4e888ff1b45b1
--- /dev/null
+++ b/icons/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/icons/button_delete_saved_game.svg b/icons/button_delete_saved_game.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c3f872e434052a6b4e7036b530ced8e6233508e4
--- /dev/null
+++ b/icons/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/icons/button_resume_game.svg b/icons/button_resume_game.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2bf973276aefa564ecff7d6149899298344819f9
--- /dev/null
+++ b/icons/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/icons/button_start.svg b/icons/button_start.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4d7634a9f3fb559e590ee965e1341ae2634bf80f
--- /dev/null
+++ b/icons/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/lib/game_board_scorer.dart b/lib/ai/game_board_scorer.dart
similarity index 97%
rename from lib/game_board_scorer.dart
rename to lib/ai/game_board_scorer.dart
index ac314aa27145fc1e1a058e478025bdfab36bae62..1f0d9502921dfba95b57337073ff3bbd70584e76 100644
--- a/lib/game_board_scorer.dart
+++ b/lib/ai/game_board_scorer.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'game_board.dart';
+import 'package:reversi/models/game/game_board.dart';
 
 class GameBoardScorer {
   // Values for each position on the board.
diff --git a/lib/move_finder.dart b/lib/ai/move_finder.dart
similarity index 85%
rename from lib/move_finder.dart
rename to lib/ai/move_finder.dart
index c5e97824a5dba0ba9ba05c54bd30baceab95c929..ca52de253a76250d4da6400d1ca229f31df307e5 100644
--- a/lib/move_finder.dart
+++ b/lib/ai/move_finder.dart
@@ -6,8 +6,8 @@ import 'dart:async';
 
 import 'package:flutter/foundation.dart';
 
-import 'game_board.dart';
-import 'game_board_scorer.dart';
+import 'package:reversi/ai/game_board_scorer.dart';
+import 'package:reversi/models/game/game_board.dart';
 
 class MoveSearchArgs {
   MoveSearchArgs({required this.board, required this.player, required this.numPlies});
@@ -28,8 +28,7 @@ class ScoredMove {
 // The [compute] function requires a top-level method as its first argument.
 // This is that method for [MoveFinder].
 Position? _findNextMove(MoveSearchArgs args) {
-  final bestMove =
-      _performSearchPly(args.board, args.player, args.player, args.numPlies - 1);
+  final bestMove = _performSearchPly(args.board, args.player, args.player, args.numPlies - 1);
   return bestMove?.move;
 }
 
@@ -47,15 +46,12 @@ ScoredMove? _performSearchPly(
     return null;
   }
 
-  var score =
-      (scoringPlayer == player) ? GameBoardScorer.minScore : GameBoardScorer.maxScore;
+  var score = (scoringPlayer == player) ? GameBoardScorer.minScore : GameBoardScorer.maxScore;
   ScoredMove? bestMove;
 
   for (var i = 0; i < availableMoves.length; i++) {
-    final newBoard =
-        board.updateForMove(availableMoves[i].x, availableMoves[i].y, player);
-    if (pliesRemaining > 0 &&
-        newBoard.getMovesForPlayer(getOpponent(player)).isNotEmpty) {
+    final newBoard = board.updateForMove(availableMoves[i].x, availableMoves[i].y, player);
+    if (pliesRemaining > 0 && newBoard.getMovesForPlayer(getOpponent(player)).isNotEmpty) {
       // Opponent has next turn.
       score = _performSearchPly(
             newBoard,
diff --git a/lib/config/default_game_settings.dart b/lib/config/default_game_settings.dart
new file mode 100644
index 0000000000000000000000000000000000000000..00143db91b64210d3c22e0996cd8b245f778b3a5
--- /dev/null
+++ b/lib/config/default_game_settings.dart
@@ -0,0 +1,40 @@
+import 'package:reversi/utils/tools.dart';
+
+class DefaultGameSettings {
+  // available game parameters codes
+  static const String parameterCodeGameMode = 'gameMode';
+  static const String parameterCodeDifficultyLevel = 'difficultyLevel';
+  static const List<String> availableParameters = [
+    parameterCodeGameMode,
+    parameterCodeDifficultyLevel,
+  ];
+
+  // game mode: available values
+  static const String gameModeValueMedium = 'human-vs-cpu';
+  static const List<String> allowedGameModeValues = [
+    gameModeValueMedium,
+  ];
+  // game mode: default value
+  static const String defaultGameModeValue = gameModeValueMedium;
+
+  // difficulty level: available values
+  static const String difficultyLevelMedium = 'medium';
+  static const List<String> allowedDifficultyLevelValues = [
+    difficultyLevelMedium,
+  ];
+  // difficulty level: default value
+  static const String difficultyLevelValue = difficultyLevelMedium;
+
+  // available values from parameter code
+  static List<String> getAvailableValues(String parameterCode) {
+    switch (parameterCode) {
+      case parameterCodeGameMode:
+        return DefaultGameSettings.allowedGameModeValues;
+      case parameterCodeDifficultyLevel:
+        return DefaultGameSettings.allowedDifficultyLevelValues;
+    }
+
+    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..2de24e5d146fecb47acfccf2a125828e74d0cf19
--- /dev/null
+++ b/lib/config/default_global_settings.dart
@@ -0,0 +1,28 @@
+import 'package:reversi/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..b09d6841a3e2d01a875f7ebd33bc246ad02016fb
--- /dev/null
+++ b/lib/config/menu.dart
@@ -0,0 +1,52 @@
+import 'package:flutter/material.dart';
+import 'package:unicons/unicons.dart';
+
+import 'package:reversi/ui/screens/page_about.dart';
+import 'package:reversi/ui/screens/page_game.dart';
+import 'package:reversi/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/styling.dart b/lib/config/styling.dart
similarity index 92%
rename from lib/styling.dart
rename to lib/config/styling.dart
index 6372e69382566eaa5a48cc14e5a8be4f0bf4f7cf..888f74f88e1b248e7c1d7d85200ac21b5f7b1184 100644
--- a/lib/styling.dart
+++ b/lib/config/styling.dart
@@ -1,6 +1,6 @@
 import 'package:flutter/widgets.dart';
 
-import 'game_board.dart';
+import 'package:reversi/models/game/game_board.dart';
 
 abstract class Styling {
   // **** GRADIENTS AND COLORS ****
@@ -26,8 +26,7 @@ abstract class Styling {
     ),
   );
 
-  static const BoxDecoration mainWidgetDecoration =
-      BoxDecoration(color: Color(0xffffffff));
+  static const BoxDecoration mainWidgetDecoration = BoxDecoration(color: Color(0xffffffff));
 
   static const thinkingColor = Color(0xff2196f3);
 
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/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart
new file mode 100644
index 0000000000000000000000000000000000000000..face7682f0b74048fde6d297245d5ccd08fad3d2
--- /dev/null
+++ b/lib/cubit/game_cubit.dart
@@ -0,0 +1,90 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:reversi/models/game/game.dart';
+import 'package:reversi/models/settings/settings_game.dart';
+import 'package:reversi/models/settings/settings_global.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
+      gameModel: state.currentGame.gameModel,
+      // Game data
+      score: state.currentGame.score,
+    );
+    // 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();
+  }
+
+  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();
+  }
+
+  @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..141a1f6785c7d3d8281f085fc127d75d78313563
--- /dev/null
+++ b/lib/cubit/nav_cubit.dart
@@ -0,0 +1,37 @@
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:reversi/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..ecfa21cc7d9a83d652421c52e148c3bcf09e0ae4
--- /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:reversi/config/default_game_settings.dart';
+import 'package:reversi/models/settings/settings_game.dart';
+
+part 'settings_game_state.dart';
+
+class GameSettingsCubit extends HydratedCubit<GameSettingsState> {
+  GameSettingsCubit() : super(GameSettingsState(settings: GameSettings.createDefault()));
+
+  void setValues({
+    String? itemsCount,
+    String? timerValue,
+  }) {
+    emit(
+      GameSettingsState(
+        settings: GameSettings(
+          itemsCount: itemsCount ?? state.settings.itemsCount,
+          timerValue: timerValue ?? state.settings.timerValue,
+        ),
+      ),
+    );
+  }
+
+  String getParameterValue(String code) {
+    switch (code) {
+      case DefaultGameSettings.parameterCodeGameMode:
+        return GameSettings.getItemsCountValueFromUnsafe(state.settings.itemsCount);
+      case DefaultGameSettings.parameterCodeDifficultyLevel:
+        return GameSettings.getTimerValueFromUnsafe(state.settings.timerValue);
+    }
+
+    return '';
+  }
+
+  void setParameterValue(String code, String value) {
+    final String itemsCount = code == DefaultGameSettings.parameterCodeGameMode
+        ? value
+        : getParameterValue(DefaultGameSettings.parameterCodeGameMode);
+    final String timerValue = code == DefaultGameSettings.parameterCodeDifficultyLevel
+        ? value
+        : getParameterValue(DefaultGameSettings.parameterCodeDifficultyLevel);
+
+    setValues(
+      itemsCount: itemsCount,
+      timerValue: timerValue,
+    );
+  }
+
+  @override
+  GameSettingsState? fromJson(Map<String, dynamic> json) {
+    final String itemsCount = json[DefaultGameSettings.parameterCodeGameMode] as String;
+    final String timerValue = json[DefaultGameSettings.parameterCodeDifficultyLevel] as String;
+
+    return GameSettingsState(
+      settings: GameSettings(
+        itemsCount: itemsCount,
+        timerValue: timerValue,
+      ),
+    );
+  }
+
+  @override
+  Map<String, dynamic>? toJson(GameSettingsState state) {
+    return <String, dynamic>{
+      DefaultGameSettings.parameterCodeGameMode: state.settings.itemsCount,
+      DefaultGameSettings.parameterCodeDifficultyLevel: state.settings.timerValue,
+    };
+  }
+}
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..cb4094b21e3ca1fdfb44e621f7cc341947276caf
--- /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:reversi/config/default_global_settings.dart';
+import 'package:reversi/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/game_engine.dart b/lib/game/game_engine.dart
new file mode 100644
index 0000000000000000000000000000000000000000..4be8afc2d89347a1fd5fb97be1dc9295c9b14cb6
--- /dev/null
+++ b/lib/game/game_engine.dart
@@ -0,0 +1,176 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:flutter/material.dart';
+
+import 'package:reversi/ai/move_finder.dart';
+import 'package:reversi/config/styling.dart';
+import 'package:reversi/models/game/game_board.dart';
+import 'package:reversi/models/game/game_model.dart';
+import 'package:reversi/ui/widgets/actions/button_game_quit.dart';
+import 'package:reversi/ui/widgets/game/game_board_display.dart';
+import 'package:reversi/ui/widgets/game/score_box.dart';
+import 'package:reversi/ui/widgets/game/thinking_indicator.dart';
+
+/// The [GameEngine] Widget represents the entire game
+/// display, from scores to board state and everything in between.
+class GameEngine extends StatefulWidget {
+  const GameEngine({super.key});
+
+  @override
+  State createState() => _GameEngineState();
+}
+
+/// State class for [GameEngine].
+///
+/// The game is modeled as a [Stream] of immutable instances of [GameModel].
+/// Each move by the player or CPU results in a new [GameModel], which is
+/// sent downstream. [GameEngine] uses a [StreamBuilder] wired up to that stream
+/// of models to build out its [Widget] tree.
+class _GameEngineState extends State<GameEngine> {
+  final StreamController<GameModel> _userMovesController = StreamController<GameModel>();
+  final StreamController<GameModel> _restartController = StreamController<GameModel>();
+  Stream<GameModel>? _modelStream;
+
+  _GameEngineState() {
+    // Below is the combination of streams that controls the flow of the game.
+    // There are two streams of models produced by player interaction (either by
+    // restarting the game, which produces a brand new game model and sends it
+    // downstream, or tapping on one of the board locations to play a piece, and
+    // which creates a new board model with the result of the move and sends it
+    // downstream. The StreamGroup combines these into a single stream, then
+    // does a little trick with asyncExpand.
+    //
+    // The function used in asyncExpand checks to see if it's the CPU's turn
+    // (white), and if so creates a [MoveFinder] to look for the best move. It
+    // awaits the calculation, and then creates a new [GameModel] with the
+    // result of that move and sends it downstream by yielding it. If it's still
+    // the CPU's turn after making that move (which can happen in reversi), this
+    // is repeated.
+    //
+    // The final stream of models that exits the asyncExpand call is a
+    // combination of "new game" models, models with the results of player
+    // moves, and models with the results of CPU moves. These are fed into the
+    // StreamBuilder in [build], and used to create the widgets that comprise
+    // the game's display.
+    _modelStream = StreamGroup.merge([
+      _userMovesController.stream,
+      _restartController.stream,
+    ]).asyncExpand((model) async* {
+      yield model;
+
+      var newModel = model;
+
+      while (newModel.player == PieceType.white) {
+        final finder = MoveFinder(newModel.board);
+
+        final move = await Future.delayed(const Duration(milliseconds: 2000), () {
+          return finder.findNextMove(newModel.player, 5);
+        });
+        if (move != null) {
+          newModel = newModel.updateForMove(move.x, move.y);
+          yield newModel;
+        }
+      }
+    });
+  }
+
+  // Thou shalt tidy up thy stream controllers.
+  @override
+  void dispose() {
+    _userMovesController.close();
+    _restartController.close();
+    super.dispose();
+  }
+
+  /// The build method mostly just sets up the StreamBuilder and leaves the
+  /// details to _buildWidgets.
+  @override
+  Widget build(BuildContext context) {
+    return StreamBuilder<GameModel>(
+      stream: _modelStream,
+      builder: (context, snapshot) {
+        return _buildWidgets(
+          snapshot.hasData ? snapshot.data! : GameModel(board: GameBoard()),
+        );
+      },
+    );
+  }
+
+  // Called when the user taps on the game's board display. If it's the player's
+  // turn, this method will attempt to make the move, creating a new GameModel
+  // in the process.
+  void _attemptUserMove(GameModel model, int x, int y) {
+    if (model.player == PieceType.black && model.board.isLegalMove(x, y, model.player)) {
+      _userMovesController.add(model.updateForMove(x, y));
+    }
+  }
+
+  // Builds out the Widget tree using the most recent GameModel from the stream.
+  Widget _buildWidgets(GameModel model) {
+    return Column(
+      children: [
+        Row(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            const Spacer(flex: 1),
+            ScoreBox(player: PieceType.black, model: model),
+            const Spacer(flex: 4),
+            ScoreBox(player: PieceType.white, model: model),
+            const Spacer(flex: 1),
+          ],
+        ),
+        const SizedBox(height: 8),
+        GameBoardDisplay(
+          context: context,
+          model: model,
+          callback: _attemptUserMove,
+        ),
+        const SizedBox(height: 8),
+        ThinkingIndicator(
+          color: Styling.thinkingColor,
+          height: Styling.thinkingSize,
+          visible: model.player == PieceType.white,
+        ),
+        model.gameIsOver ? _buildGameEnd(model) : SizedBox.shrink()
+      ],
+    );
+  }
+
+  Widget _buildGameEnd(GameModel model) {
+    Image decorationImage = Image(
+      image: AssetImage(model.gameResultString == 'black'
+          ? 'assets/ui/game_win.png'
+          : 'assets/ui/placeholder.png'),
+      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: [decorationImage],
+              ),
+              Column(
+                children: [QuitGameButton()],
+              ),
+              Column(
+                children: [decorationImage],
+              ),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/lib/main.dart b/lib/main.dart
index a248c75c34922242245401157ea2b47dde5ccd66..10cc981cf005be1187a19262d802cbdf415639be 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,343 +1,114 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
+import 'dart:io';
 
-import 'dart:async';
-
-import 'package:async/async.dart';
+import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter/services.dart' show SystemChrome, DeviceOrientation;
-
-import 'game_board.dart';
-import 'game_model.dart';
-import 'move_finder.dart';
-import 'styling.dart';
-import 'thinking_indicator.dart';
-
-/// Main function for the app. Turns off the system overlays and locks portrait
-/// orientation for a more game-like UI, and then runs the [Widget] tree.
-void main() {
+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:reversi/config/default_global_settings.dart';
+import 'package:reversi/config/theme.dart';
+import 'package:reversi/cubit/game_cubit.dart';
+import 'package:reversi/cubit/nav_cubit.dart';
+import 'package:reversi/cubit/settings_game_cubit.dart';
+import 'package:reversi/cubit/settings_global_cubit.dart';
+import 'package:reversi/cubit/theme_cubit.dart';
+import 'package:reversi/ui/skeleton.dart';
+
+void main() async {
+  // Initialize packages
   WidgetsFlutterBinding.ensureInitialized();
-
-  SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
-
-  runApp(const FlutterFlipApp());
-}
-
-/// The App class. Unlike many Flutter apps, this one does not use Material
-/// widgets, so there's no [MaterialApp] or [Theme] objects.
-class FlutterFlipApp extends StatelessWidget {
-  const FlutterFlipApp({super.key});
-
-  @override
-  Widget build(BuildContext context) {
-    return MaterialApp(
-      debugShowCheckedModeBanner: false,
-      theme: ThemeData(
-        primaryColor: Colors.blue,
-        visualDensity: VisualDensity.adaptivePlatformDensity,
-      ),
-      home: const GameScreen(),
-    );
-  }
-}
-
-/// The [GameScreen] Widget represents the entire game
-/// display, from scores to board state and everything in between.
-class GameScreen extends StatefulWidget {
-  const GameScreen({super.key});
-
-  @override
-  State createState() => _GameScreenState();
+  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(EasyLocalization(
+            path: 'assets/translations',
+            supportedLocales: const <Locale>[
+              Locale('en'),
+              Locale('fr'),
+            ],
+            fallbackLocale: const Locale('en'),
+            useFallbackTranslations: true,
+            child: const MyApp(),
+          )));
 }
 
-/// State class for [GameScreen].
-///
-/// The game is modeled as a [Stream] of immutable instances of [GameModel].
-/// Each move by the player or CPU results in a new [GameModel], which is
-/// sent downstream. [GameScreen] uses a [StreamBuilder] wired up to that stream
-/// of models to build out its [Widget] tree.
-class _GameScreenState extends State<GameScreen> {
-  final StreamController<GameModel> _userMovesController = StreamController<GameModel>();
-  final StreamController<GameModel> _restartController = StreamController<GameModel>();
-  Stream<GameModel>? _modelStream;
-
-  _GameScreenState() {
-    // Below is the combination of streams that controls the flow of the game.
-    // There are two streams of models produced by player interaction (either by
-    // restarting the game, which produces a brand new game model and sends it
-    // downstream, or tapping on one of the board locations to play a piece, and
-    // which creates a new board model with the result of the move and sends it
-    // downstream. The StreamGroup combines these into a single stream, then
-    // does a little trick with asyncExpand.
-    //
-    // The function used in asyncExpand checks to see if it's the CPU's turn
-    // (white), and if so creates a [MoveFinder] to look for the best move. It
-    // awaits the calculation, and then creates a new [GameModel] with the
-    // result of that move and sends it downstream by yielding it. If it's still
-    // the CPU's turn after making that move (which can happen in reversi), this
-    // is repeated.
-    //
-    // The final stream of models that exits the asyncExpand call is a
-    // combination of "new game" models, models with the results of player
-    // moves, and models with the results of CPU moves. These are fed into the
-    // StreamBuilder in [build], and used to create the widgets that comprise
-    // the game's display.
-    _modelStream = StreamGroup.merge([
-      _userMovesController.stream,
-      _restartController.stream,
-    ]).asyncExpand((model) async* {
-      yield model;
-
-      var newModel = model;
-
-      while (newModel.player == PieceType.white) {
-        final finder = MoveFinder(newModel.board);
-
-        final move = await Future.delayed(const Duration(milliseconds: 2000), () {
-          return finder.findNextMove(newModel.player, 5);
-        });
-        if (move != null) {
-          newModel = newModel.updateForMove(move.x, move.y);
-          yield newModel;
-        }
-      }
-    });
-  }
+class MyApp extends StatelessWidget {
+  const MyApp({super.key});
 
-  // Thou shalt tidy up thy stream controllers.
-  @override
-  void dispose() {
-    _userMovesController.close();
-    _restartController.close();
-    super.dispose();
-  }
-
-  /// The build method mostly just sets up the StreamBuilder and leaves the
-  /// details to _buildWidgets.
   @override
   Widget build(BuildContext context) {
-    return StreamBuilder<GameModel>(
-      stream: _modelStream,
-      builder: (context, snapshot) {
-        return _buildWidgets(
-          context,
-          snapshot.hasData ? snapshot.data! : GameModel(board: GameBoard()),
-        );
-      },
-    );
-  }
-
-  // Called when the user taps on the game's board display. If it's the player's
-  // turn, this method will attempt to make the move, creating a new GameModel
-  // in the process.
-  void _attemptUserMove(GameModel model, int x, int y) {
-    if (model.player == PieceType.black && model.board.isLegalMove(x, y, model.player)) {
-      _userMovesController.add(model.updateForMove(x, y));
+    final List<String> assets = getImagesAssets();
+    for (String asset in assets) {
+      precacheImage(AssetImage(asset), context);
     }
-  }
 
-  Widget _buildScoreBox(PieceType player, GameModel model) {
-    var assetImageCode = player == PieceType.black ? 'black' : 'white';
-    String assetImageName =
-        'assets/skins/${model.skin}_tile_$assetImageCode.png';
-
-    var scoreText =
-        player == PieceType.black ? '${model.blackScore}' : '${model.whiteScore}';
-
-    return Container(
-      padding: const EdgeInsets.symmetric(
-        vertical: 3.0,
-        horizontal: 25.0,
-      ),
-      decoration: (model.player == player)
-          ? Styling.activePlayerIndicator
-          : Styling.inactivePlayerIndicator,
-      child: Row(
-        children: <Widget>[
-          SizedBox(
-            width: 25.0,
-            height: 25.0,
-            child: Image(
-              image: AssetImage(assetImageName),
-              fit: BoxFit.fill,
-            ),
-          ),
-          const SizedBox(
-            width: 10.0,
-          ),
-          Text(
-            scoreText,
-            textAlign: TextAlign.center,
-            style: const TextStyle(
-              fontSize: 35.0,
-              color: Color(0xff000000),
-            ),
-          ),
-        ],
-      ),
-    );
-  }
-
-  Widget _buildScoreBoxes(GameModel model) {
-    return Row(
-      mainAxisAlignment: MainAxisAlignment.center,
-      children: [
-        const Spacer(flex: 1),
-        _buildScoreBox(PieceType.black, model),
-        const Spacer(flex: 4),
-        _buildScoreBox(PieceType.white, model),
-        const Spacer(flex: 1),
+    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: 'Reversi',
+            home: const SkeletonScreen(),
+
+            // Theme stuff
+            theme: lightTheme,
+            darkTheme: darkTheme,
+            themeMode: state.themeMode,
+
+            // Localization stuff
+            localizationsDelegates: context.localizationDelegates,
+            supportedLocales: context.supportedLocales,
+            locale: context.locale,
+            debugShowCheckedModeBanner: false,
+          );
+        },
+      ),
     );
   }
 
-  Table _buildGameBoardDisplay(BuildContext context, GameModel model) {
-    final rows = <TableRow>[];
-
-    for (var y = 0; y < GameBoard.height; y++) {
-      final cells = <Column>[];
-
-      for (var x = 0; x < GameBoard.width; x++) {
-        PieceType pieceType = model.board.getPieceAtLocation(x, y);
-        String? assetImageCode = Styling.assetImageCodes[pieceType];
-        String assetImageName = 'assets/skins/${model.skin}_tile_${assetImageCode ?? 'empty'}.png';
+  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');
+    }
 
-        Column cell = Column(
-          children: [
-            Container(
-              decoration: Styling.boardCellDecoration,
-              child: SizedBox(
-                child: GestureDetector(
-                  child: AnimatedSwitcher(
-                    duration: const Duration(milliseconds: 500),
-                    transitionBuilder: (Widget child, Animation<double> animation) {
-                      return ScaleTransition(scale: animation, child: child);
-                    },
-                    child: Image(
-                      image: AssetImage(assetImageName),
-                      fit: BoxFit.fill,
-                      key: ValueKey<int>(pieceType == PieceType.empty
-                          ? 0
-                          : (pieceType == PieceType.black ? 1 : 2)),
-                    ),
-                  ),
-                  onTap: () {
-                    _attemptUserMove(model, x, y);
-                  },
-                ),
-              ),
-            )
-          ],
-        );
+    final List<String> skinImages = [
+      'tile_black',
+      'tile_empty',
+      'tile_white',
+    ];
 
-        cells.add(cell);
+    for (String skin in DefaultGlobalSettings.allowedSkinValues) {
+      for (String image in skinImages) {
+        assets.add('assets/skins/${skin}_$image.png');
       }
-
-      rows.add(TableRow(children: cells));
     }
 
-    return Table(defaultColumnWidth: const IntrinsicColumnWidth(), children: rows);
-  }
-
-  Widget _buildThinkingIndicator(GameModel model) {
-    return ThinkingIndicator(
-      color: Styling.thinkingColor,
-      height: Styling.thinkingSize,
-      visible: model.player == PieceType.white,
-    );
-  }
-
-  Widget _buildRestartGameWidget() {
-    return Container(
-      padding: const EdgeInsets.symmetric(
-        vertical: 5.0,
-        horizontal: 15.0,
-      ),
-      child: const Image(
-        image: AssetImage('assets/icons/button_restart.png'),
-        fit: BoxFit.fill,
-      ),
-    );
-  }
-
-  Widget _buildEndGameWidget(GameModel model) {
-    Image decorationImage = Image(
-      image: AssetImage(model.gameResultString == 'black'
-          ? 'assets/icons/game_win.png'
-          : 'assets/icons/empty.png'),
-      fit: BoxFit.fill,
-    );
-
-    return Container(
-        margin: const EdgeInsets.all(2),
-        padding: const EdgeInsets.all(2),
-        child: Table(defaultColumnWidth: const IntrinsicColumnWidth(), children: [
-          TableRow(
-            children: [
-              Column(children: [decorationImage]),
-              Column(children: [decorationImage]),
-              Column(children: [
-                GestureDetector(
-                  onTap: () {
-                    _restartController.add(
-                      GameModel(board: GameBoard()),
-                    );
-                  },
-                  child: _buildRestartGameWidget(),
-                )
-              ]),
-              Column(children: [decorationImage]),
-              Column(children: [decorationImage]),
-            ],
-          ),
-        ]));
-  }
-
-  // Builds out the Widget tree using the most recent GameModel from the stream.
-  Widget _buildWidgets(BuildContext context, GameModel model) {
-    return Scaffold(
-      appBar: AppBar(
-        actions: [
-          TextButton(
-            child: Container(
-              decoration: BoxDecoration(
-                borderRadius: BorderRadius.circular(4),
-                border: Border.all(
-                  color: Colors.blue,
-                  width: 4,
-                ),
-              ),
-              margin: const EdgeInsets.all(8),
-              child: const Image(
-                image: AssetImage('assets/icons/button_restart.png'),
-                fit: BoxFit.fill,
-              ),
-            ),
-            onPressed: () => _restartController.add(GameModel(board: GameBoard())),
-          )
-        ],
-      ),
-      body: Container(
-        padding: const EdgeInsets.only(
-          top: 5.0,
-          left: 5.0,
-          right: 5.0,
-        ),
-        decoration: Styling.mainWidgetDecoration,
-        child: SafeArea(
-          child: Column(
-            children: [
-              _buildScoreBoxes(model),
-              const SizedBox(height: 5),
-              _buildGameBoardDisplay(context, model),
-              const SizedBox(height: 5),
-              _buildThinkingIndicator(model),
-              if (model.gameIsOver) _buildEndGameWidget(model),
-            ],
-          ),
-        ),
-      ),
-    );
+    return assets;
   }
 }
diff --git a/lib/models/game/game.dart b/lib/models/game/game.dart
new file mode 100644
index 0000000000000000000000000000000000000000..22b2fd83a4de1d7741d4a7249b39fa0a7a18395b
--- /dev/null
+++ b/lib/models/game/game.dart
@@ -0,0 +1,117 @@
+import 'package:reversi/models/game/game_board.dart';
+import 'package:reversi/models/game/game_model.dart';
+import 'package:reversi/models/settings/settings_game.dart';
+import 'package:reversi/models/settings/settings_global.dart';
+import 'package:reversi/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.gameModel,
+
+    // Game data
+    this.score = 0,
+  });
+
+  // Settings
+  final GameSettings gameSettings;
+  final GlobalSettings globalSettings;
+
+  // State
+  bool isRunning;
+  bool isStarted;
+  bool isFinished;
+  bool animationInProgress;
+
+  // Base data
+  final GameModel gameModel;
+
+  // Game data
+  int score;
+
+  factory Game.createNull() {
+    return Game(
+      // Settings
+      gameSettings: GameSettings.createDefault(),
+      globalSettings: GlobalSettings.createDefault(),
+      // Base data
+      gameModel: GameModel(board: GameBoard()),
+      // Game data
+      score: 0,
+    );
+  }
+
+  factory Game.createNew({
+    GameSettings? gameSettings,
+    GlobalSettings? globalSettings,
+  }) {
+    final GameSettings newGameSettings = gameSettings ?? GameSettings.createDefault();
+    final GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault();
+
+    return Game(
+      // Settings
+      gameSettings: newGameSettings,
+      globalSettings: newGlobalSettings,
+      // State
+      isRunning: true,
+      // Base data
+      gameModel: GameModel(board: GameBoard()),
+      // Game data
+      score: 0,
+    );
+  }
+
+  bool get canBeResumed => isStarted && !isFinished;
+
+  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');
+    printlog('  Base data');
+    printlog('    gameModel: $gameModel');
+    printlog('  Game data');
+    printlog('    score: $score');
+    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
+      //   'gameModel': gameModel,
+      // Game data
+      'score': score,
+    };
+  }
+}
diff --git a/lib/game_board.dart b/lib/models/game/game_board.dart
similarity index 100%
rename from lib/game_board.dart
rename to lib/models/game/game_board.dart
diff --git a/lib/game_model.dart b/lib/models/game/game_model.dart
similarity index 88%
rename from lib/game_model.dart
rename to lib/models/game/game_model.dart
index b20952a568522a3dac95b4824b551cd61d32e747..aa31385e62f2c920f64a7240a3a106a3c9f383a8 100644
--- a/lib/game_model.dart
+++ b/lib/models/game/game_model.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'game_board.dart';
+import 'package:reversi/models/game/game_board.dart';
 
 /// A model representing the state of a game of reversi. It's a board and the
 /// player who's next to go, essentially.
@@ -39,7 +39,10 @@ class GameModel {
   /// returned. If unsuccessful, null is returned.
   GameModel updateForMove(int x, int y) {
     if (!board.isLegalMove(x, y, player)) {
-      return GameModel(board: board, player: player);
+      return GameModel(
+        board: board,
+        player: player,
+      );
     }
 
     final newBoard = board.updateForMove(x, y, player);
@@ -53,6 +56,9 @@ class GameModel {
       nextPlayer = PieceType.empty;
     }
 
-    return GameModel(board: newBoard, player: nextPlayer);
+    return GameModel(
+      board: newBoard,
+      player: nextPlayer,
+    );
   }
 }
diff --git a/lib/models/settings/settings_game.dart b/lib/models/settings/settings_game.dart
new file mode 100644
index 0000000000000000000000000000000000000000..68831589adedbd206dd7ffe417c7ddce893f23b8
--- /dev/null
+++ b/lib/models/settings/settings_game.dart
@@ -0,0 +1,58 @@
+import 'package:reversi/config/default_game_settings.dart';
+import 'package:reversi/utils/tools.dart';
+
+class GameSettings {
+  final String itemsCount;
+  final String timerValue;
+
+  GameSettings({
+    required this.itemsCount,
+    required this.timerValue,
+  });
+
+  // Getters to convert String to int
+  int get itemsCountValue => int.parse(itemsCount);
+  int get timerCountValue => int.parse(timerValue);
+
+  static String getItemsCountValueFromUnsafe(String itemsCount) {
+    if (DefaultGameSettings.allowedGameModeValues.contains(itemsCount)) {
+      return itemsCount;
+    }
+
+    return DefaultGameSettings.defaultGameModeValue;
+  }
+
+  static String getTimerValueFromUnsafe(String timerValue) {
+    if (DefaultGameSettings.allowedDifficultyLevelValues.contains(timerValue)) {
+      return timerValue;
+    }
+
+    return DefaultGameSettings.difficultyLevelValue;
+  }
+
+  factory GameSettings.createDefault() {
+    return GameSettings(
+      itemsCount: DefaultGameSettings.defaultGameModeValue,
+      timerValue: DefaultGameSettings.difficultyLevelValue,
+    );
+  }
+
+  void dump() {
+    printlog('$GameSettings:');
+    printlog('  ${DefaultGameSettings.parameterCodeGameMode}: $itemsCount');
+    printlog('  ${DefaultGameSettings.parameterCodeDifficultyLevel}: $timerValue');
+    printlog('');
+  }
+
+  @override
+  String toString() {
+    return '$GameSettings(${toJson()})';
+  }
+
+  Map<String, dynamic>? toJson() {
+    return <String, dynamic>{
+      DefaultGameSettings.parameterCodeGameMode: itemsCount,
+      DefaultGameSettings.parameterCodeDifficultyLevel: timerValue,
+    };
+  }
+}
diff --git a/lib/models/settings/settings_global.dart b/lib/models/settings/settings_global.dart
new file mode 100644
index 0000000000000000000000000000000000000000..1260f1fb345b52dd649cab8698765c64eb838935
--- /dev/null
+++ b/lib/models/settings/settings_global.dart
@@ -0,0 +1,41 @@
+import 'package:reversi/config/default_global_settings.dart';
+import 'package:reversi/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/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..c79eada33578b6786a372cc0a4401f088eaa003c
--- /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:reversi/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..45d1958818a712cb090ecc8a4f0439b337cfceae
--- /dev/null
+++ b/lib/ui/layouts/game_layout.dart
@@ -0,0 +1,23 @@
+import 'package:flutter/material.dart';
+
+import 'package:reversi/ui/widgets/game/game_board.dart';
+
+class GameLayout extends StatelessWidget {
+  const GameLayout({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      alignment: AlignmentDirectional.topCenter,
+      padding: const EdgeInsets.all(4),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.start,
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          const GameBoardWidget(),
+          const Expanded(child: SizedBox.shrink()),
+        ],
+      ),
+    );
+  }
+}
diff --git a/lib/ui/layouts/parameters_layout.dart b/lib/ui/layouts/parameters_layout.dart
new file mode 100644
index 0000000000000000000000000000000000000000..55acc0b5d209bd30097485811a7dd78de9b0e9ec
--- /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:reversi/config/default_game_settings.dart';
+import 'package:reversi/config/default_global_settings.dart';
+import 'package:reversi/cubit/settings_game_cubit.dart';
+import 'package:reversi/cubit/settings_global_cubit.dart';
+import 'package:reversi/ui/parameters/parameter_widget.dart';
+import 'package:reversi/ui/widgets/actions/button_delete_saved_game.dart';
+import 'package:reversi/ui/widgets/actions/button_game_start_new.dart';
+import 'package:reversi/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..cc7b3edf4fd047b6275274498f016e33941f5f45
--- /dev/null
+++ b/lib/ui/parameters/parameter_painter.dart
@@ -0,0 +1,71 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:reversi/models/settings/settings_game.dart';
+import 'package:reversi/models/settings/settings_global.dart';
+
+import 'package:reversi/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) {
+      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,
+      ),
+    );
+  }
+}
diff --git a/lib/ui/parameters/parameter_widget.dart b/lib/ui/parameters/parameter_widget.dart
new file mode 100644
index 0000000000000000000000000000000000000000..6a6941d89607e41bb7f968b7a60208442ac76b0a
--- /dev/null
+++ b/lib/ui/parameters/parameter_widget.dart
@@ -0,0 +1,66 @@
+import 'package:flutter/material.dart';
+import 'package:reversi/models/settings/settings_game.dart';
+import 'package:reversi/models/settings/settings_global.dart';
+
+import 'package:reversi/ui/helpers/styled_button.dart';
+import 'package:reversi/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) {
+      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,
+    );
+  }
+}
diff --git a/lib/ui/screens/page_about.dart b/lib/ui/screens/page_about.dart
new file mode 100644
index 0000000000000000000000000000000000000000..1706939b8183cd797d296a846c8e0c2602343050
--- /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:reversi/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..8605ef24fb42bd12781ae310d997427493eb30fb
--- /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:reversi/cubit/game_cubit.dart';
+import 'package:reversi/models/game/game.dart';
+import 'package:reversi/ui/layouts/parameters_layout.dart';
+import 'package:reversi/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..3df22a1bb87618619ddcaa5d8fd118bef17137b1
--- /dev/null
+++ b/lib/ui/screens/page_settings.dart
@@ -0,0 +1,26 @@
+import 'package:flutter/material.dart';
+
+import 'package:reversi/ui/helpers/app_titles.dart';
+import 'package:reversi/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..e7df8b8bd7043bede0900b9d63b913720c9e8b19
--- /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:reversi/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..7581a2d3199384dfea7efb78741c519fb22ecac7
--- /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:reversi/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..a460301d4eb07994f60c7fabed5cb62968060d10
--- /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:reversi/config/menu.dart';
+import 'package:reversi/cubit/nav_cubit.dart';
+import 'package:reversi/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..ddc09a1d0e0a1cce905fde156a2efe11952db330
--- /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:reversi/cubit/game_cubit.dart';
+import 'package:reversi/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..fbe48a34cf300c4d9e0650ffe31ff6e0aa37ab1e
--- /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:reversi/cubit/game_cubit.dart';
+import 'package:reversi/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..9e88d264825404f4f3a5c1515ef1bdbabb73b3cd
--- /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:reversi/cubit/game_cubit.dart';
+import 'package:reversi/cubit/settings_game_cubit.dart';
+import 'package:reversi/cubit/settings_global_cubit.dart';
+import 'package:reversi/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..d28b455eed3747dd4e9422c186176146ae48e00d
--- /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:reversi/cubit/game_cubit.dart';
+import 'package:reversi/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..5c8eb368149d15f5675774579a93d9be1c17a31e
--- /dev/null
+++ b/lib/ui/widgets/game/game_board.dart
@@ -0,0 +1,25 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:reversi/cubit/game_cubit.dart';
+import 'package:reversi/game/game_engine.dart';
+import 'package:reversi/models/game/game.dart';
+import 'package:reversi/utils/tools.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 Game currentGame = gameState.currentGame;
+          printlog(currentGame.toString());
+
+          return const GameEngine();
+        },
+      ),
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/game_board_display.dart b/lib/ui/widgets/game/game_board_display.dart
new file mode 100644
index 0000000000000000000000000000000000000000..3bbb2a5e11dd3b6da94e6adae1c9807ecb837806
--- /dev/null
+++ b/lib/ui/widgets/game/game_board_display.dart
@@ -0,0 +1,78 @@
+import 'package:flutter/material.dart';
+
+import 'package:reversi/models/game/game_board.dart';
+import 'package:reversi/models/game/game_model.dart';
+import 'package:reversi/config/styling.dart';
+
+class GameBoardDisplay extends StatelessWidget {
+  const GameBoardDisplay({
+    super.key,
+    required this.context,
+    required this.model,
+    required this.callback,
+  });
+
+  final BuildContext context;
+  final GameModel model;
+  final Function callback;
+
+  @override
+  Widget build(BuildContext context) {
+    final rows = <TableRow>[];
+
+    for (int y = 0; y < GameBoard.height; y++) {
+      final cells = <Column>[];
+
+      for (int x = 0; x < GameBoard.width; x++) {
+        PieceType pieceType = model.board.getPieceAtLocation(x, y);
+        String? assetImageCode = Styling.assetImageCodes[pieceType];
+        String assetImageName =
+            'assets/skins/${model.skin}_tile_${assetImageCode ?? 'empty'}.png';
+
+        Column cell = Column(
+          children: [
+            Container(
+              decoration: Styling.boardCellDecoration,
+              child: SizedBox(
+                child: GestureDetector(
+                  child: AnimatedSwitcher(
+                    duration: const Duration(milliseconds: 250),
+                    transitionBuilder: (Widget child, Animation<double> animation) {
+                      return ScaleTransition(
+                        scale: animation,
+                        child: child,
+                      );
+                    },
+                    child: Image(
+                      image: AssetImage(assetImageName),
+                      fit: BoxFit.fill,
+                      key: ValueKey<int>(
+                        pieceType == PieceType.empty
+                            ? 0
+                            : (pieceType == PieceType.black ? 1 : 2),
+                      ),
+                    ),
+                  ),
+                  onTap: () {
+                    callback(model, x, y);
+                  },
+                ),
+              ),
+            )
+          ],
+        );
+
+        cells.add(cell);
+      }
+
+      rows.add(TableRow(
+        children: cells,
+      ));
+    }
+
+    return Table(
+      defaultColumnWidth: const IntrinsicColumnWidth(),
+      children: rows,
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/score_box.dart b/lib/ui/widgets/game/score_box.dart
new file mode 100644
index 0000000000000000000000000000000000000000..d1916a728766ffb6711e95b9be91f548022b4b72
--- /dev/null
+++ b/lib/ui/widgets/game/score_box.dart
@@ -0,0 +1,57 @@
+import 'package:flutter/material.dart';
+
+import 'package:reversi/models/game/game_board.dart';
+import 'package:reversi/models/game/game_model.dart';
+import 'package:reversi/config/styling.dart';
+
+class ScoreBox extends StatelessWidget {
+  const ScoreBox({
+    super.key,
+    required this.player,
+    required this.model,
+  });
+
+  final PieceType player;
+  final GameModel model;
+
+  @override
+  Widget build(BuildContext context) {
+    var assetImageCode = player == PieceType.black ? 'black' : 'white';
+    String assetImageName = 'assets/skins/${model.skin}_tile_$assetImageCode.png';
+
+    var scoreText = player == PieceType.black ? '${model.blackScore}' : '${model.whiteScore}';
+
+    return Container(
+      padding: const EdgeInsets.symmetric(
+        vertical: 3.0,
+        horizontal: 25.0,
+      ),
+      decoration: (model.player == player)
+          ? Styling.activePlayerIndicator
+          : Styling.inactivePlayerIndicator,
+      child: Row(
+        children: <Widget>[
+          SizedBox(
+            width: 25.0,
+            height: 25.0,
+            child: Image(
+              image: AssetImage(assetImageName),
+              fit: BoxFit.fill,
+            ),
+          ),
+          const SizedBox(
+            width: 10.0,
+          ),
+          Text(
+            scoreText,
+            textAlign: TextAlign.center,
+            style: const TextStyle(
+              fontSize: 35.0,
+              color: Color(0xff000000),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/lib/thinking_indicator.dart b/lib/ui/widgets/game/thinking_indicator.dart
similarity index 98%
rename from lib/thinking_indicator.dart
rename to lib/ui/widgets/game/thinking_indicator.dart
index b64739c7614ab782772ee4580468d98128b4c4b1..69aeaf2f8d6781f4710d246a9433f07592c349ee 100644
--- a/lib/thinking_indicator.dart
+++ b/lib/ui/widgets/game/thinking_indicator.dart
@@ -3,7 +3,8 @@
 // found in the LICENSE file.
 
 import 'package:flutter/widgets.dart';
-import 'styling.dart';
+
+import 'package:reversi/config/styling.dart';
 
 /// This is a self-animated progress spinner, only instead of spinning it
 /// moves five little circles in a horizontal arrangement.
@@ -28,7 +29,6 @@ class ThinkingIndicator extends ImplicitlyAnimatedWidget {
 class _ThinkingIndicatorState extends AnimatedWidgetBaseState<ThinkingIndicator> {
   Tween<double>? _opacityTween;
 
-
   @override
   Widget build(BuildContext context) {
     return Center(
diff --git a/lib/ui/widgets/global_app_bar.dart b/lib/ui/widgets/global_app_bar.dart
new file mode 100644
index 0000000000000000000000000000000000000000..ffc7930b886372a785f7de4eb71b35fe40577759
--- /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:reversi/config/menu.dart';
+import 'package:reversi/cubit/game_cubit.dart';
+import 'package:reversi/cubit/nav_cubit.dart';
+import 'package:reversi/models/game/game.dart';
+import 'package:reversi/ui/helpers/app_titles.dart';
+import 'package:reversi/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 fc4dddda5e0f692204218f1817cf4a11e2444902..c4807cc893593f0a6b2dbef258bfc7d941409824 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -1,6 +1,14 @@
 # Generated by pub
 # See https://dart.dev/tools/pub/glossary#lockfile
 packages:
+  args:
+    dependency: transitive
+    description:
+      name: args
+      sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.6.0"
   async:
     dependency: "direct main"
     description:
@@ -9,6 +17,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.12.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:
@@ -17,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:
@@ -25,6 +57,38 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.18.0"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.6"
+  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:
@@ -46,6 +110,14 @@ packages:
     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:
@@ -54,11 +126,56 @@ packages:
       url: "https://pub.dev"
     source: hosted
     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:
@@ -91,6 +208,22 @@ packages:
       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:
@@ -99,6 +232,30 @@ packages:
       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: "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:
@@ -140,7 +297,7 @@ packages:
     source: hosted
     version: "2.1.8"
   provider:
-    dependency: "direct main"
+    dependency: transitive
     description:
       name: provider
       sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
@@ -148,7 +305,7 @@ packages:
     source: hosted
     version: "6.1.2"
   shared_preferences:
-    dependency: "direct main"
+    dependency: transitive
     description:
       name: shared_preferences
       sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051"
@@ -208,6 +365,54 @@ packages:
     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:
@@ -224,6 +429,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.1.0"
+  win32:
+    dependency: transitive
+    description:
+      name: win32
+      sha256: e5c39a90447e7c81cfec14b041cdbd0d0916bd9ebbc7fe02ab69568be703b9bd
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.6.0"
   xdg_directories:
     dependency: transitive
     description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 1ae88f29216a51ae9c34ba8812f24c8a313f22f3..7cf23d6c99793c2d75b97cce715fee294de5c44f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,16 +1,28 @@
-name: simpler_reversi
+name: reversi
 description: A reversi game application.
+
 publish_to: 'none'
-version: 0.1.1+19
+version: 0.1.2+20
 
 environment:
-  sdk: '^3.0.0'
+  sdk: "^3.0.0"
 
 dependencies:
   flutter:
     sdk: flutter
-  provider: ^6.0.5
-  shared_preferences: ^2.2.1
+
+  # 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
   async: ^2.11.0
 
 dev_dependencies:
@@ -19,5 +31,18 @@ dev_dependencies:
 flutter:
   uses-material-design: true
   assets:
-    - assets/icons/
     - assets/skins/
+    - 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/resources/app/build_application_resources.sh b/resources/app/build_application_resources.sh
new file mode 100755
index 0000000000000000000000000000000000000000..1ace90d0e0029bf1704122d2b60bced59d5ed348
--- /dev/null
+++ b/resources/app/build_application_resources.sh
@@ -0,0 +1,127 @@
+#! /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}")")"
+
+SOURCE_ICON="${CURRENT_DIR}/icon.svg"
+SOURCE_FASTLANE="${CURRENT_DIR}/featureGraphic.svg"
+SOURCE_LAUNCH_IMAGE="${CURRENT_DIR}/icon.svg"
+
+OPTIPNG_OPTIONS="-preserve -quiet -o7"
+
+if [ ! -f "${SOURCE_ICON}" ]; then
+  echo "Missing file: ${SOURCE_ICON}"
+fi
+
+if [ ! -f "${SOURCE_FASTLANE}" ]; then
+  echo "Missing file: ${SOURCE_FASTLANE}"
+fi
+
+if [ ! -f "${SOURCE_LAUNCH_IMAGE}" ]; then
+  echo "Missing file: ${SOURCE_LAUNCH_IMAGE}"
+fi
+
+function optimize_svg() {
+  SVG="$1"
+
+  cp ${SVG} ${SVG}.tmp
+  scour \
+    --remove-descriptive-elements \
+    --enable-id-stripping \
+    --enable-viewboxing \
+    --enable-comment-stripping \
+    --nindent=4 \
+    --quiet \
+    -i ${SVG}.tmp \
+    -o ${SVG}
+  rm ${SVG}.tmp
+}
+
+# optimize source svg files
+optimize_svg ${SOURCE_ICON}
+optimize_svg ${SOURCE_FASTLANE}
+optimize_svg ${SOURCE_LAUNCH_IMAGE}
+
+# build icons
+function build_application_icon() {
+  ICON_SIZE="$1"
+  TARGET="$2"
+
+  echo "Building ${TARGET}"
+
+  TARGET_PNG="${TARGET}.png"
+
+  inkscape \
+    --export-width=${ICON_SIZE} \
+    --export-height=${ICON_SIZE} \
+    --export-filename=${TARGET_PNG} \
+    ${SOURCE_ICON}
+
+  optipng ${OPTIPNG_OPTIONS} ${TARGET_PNG}
+}
+
+# build fastlane image
+function build_fastlane_image() {
+  WIDTH="$1"
+  HEIGHT="$2"
+  TARGET="$3"
+
+  echo "Building ${TARGET}"
+
+  TARGET_PNG="${TARGET}.png"
+
+  inkscape \
+    --export-width=${WIDTH} \
+    --export-height=${HEIGHT} \
+    --export-filename=${TARGET_PNG} \
+    ${SOURCE_FASTLANE}
+
+  optipng ${OPTIPNG_OPTIONS} ${TARGET_PNG}
+}
+
+# build launch images (splash screen)
+function build_launch_image() {
+  ICON_SIZE="$1"
+  TARGET="$2"
+
+  echo "Building ${TARGET}"
+
+  TARGET_PNG="${TARGET}.png"
+
+  inkscape \
+    --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 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 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
+
+build_fastlane_image 1024 500 ${BASE_DIR}/fastlane/metadata/android/en-US/images/featureGraphic
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/icons/empty.svg b/resources/ui/images/empty.svg
similarity index 57%
rename from icons/empty.svg
rename to resources/ui/images/empty.svg
index d988c4937ec1d00ce581b412ff4a859a0fd1862d..23ace81fbb82a8409cc0710c0f7bddd6381f7256 100644
--- a/icons/empty.svg
+++ b/resources/ui/images/empty.svg
@@ -1,2 +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"/>
+<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"/>
diff --git a/resources/ui/images/game_fail.svg b/resources/ui/images/game_fail.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2922fd7adc2bd2e813836c728f095376c73d4143
--- /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"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#d11717" stroke="#fff" stroke-width=".238"/><path d="m71.624 59.304c3.5089 3.5089 3.5089 9.0561 0 12.565-1.6976 1.6976-3.9623 2.6034-6.2261 2.6034s-4.5275-0.90569-6.2261-2.6034l-12.452-12.452-12.452 12.452c-1.6976 1.6976-3.9623 2.6034-6.2261 2.6034s-4.5275-0.90569-6.2261-2.6034c-3.5089-3.5089-3.5089-9.0561 0-12.565l12.452-12.452-12.452-12.452c-3.5089-3.5089-3.5089-9.0561 0-12.565s9.0561-3.5089 12.565 0l12.452 12.452 12.452-12.452c3.5089-3.5089 9.0561-3.5089 12.565 0s3.5089 9.0561 0 12.565l-12.452 12.452z" fill="#e7e7e7" stroke-width=".20213"/></svg>
diff --git a/icons/game_win.svg b/resources/ui/images/game_win.svg
similarity index 100%
rename from icons/game_win.svg
rename to resources/ui/images/game_win.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"/>
diff --git a/icons/skins/default/tile_black.svg b/resources/ui/skins/default/tile_black.svg
similarity index 100%
rename from icons/skins/default/tile_black.svg
rename to resources/ui/skins/default/tile_black.svg
diff --git a/icons/skins/default/tile_empty.svg b/resources/ui/skins/default/tile_empty.svg
similarity index 100%
rename from icons/skins/default/tile_empty.svg
rename to resources/ui/skins/default/tile_empty.svg
diff --git a/icons/skins/default/tile_white.svg b/resources/ui/skins/default/tile_white.svg
similarity index 100%
rename from icons/skins/default/tile_white.svg
rename to resources/ui/skins/default/tile_white.svg