diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000000000000000000000000000000000..8d86e4518e450377768d1de80adafba87984a148
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.json]
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.metadata b/.metadata
deleted file mode 100644
index 182cccafd96b59814f686d06695a1a60af003e8d..0000000000000000000000000000000000000000
--- a/.metadata
+++ /dev/null
@@ -1,10 +0,0 @@
-# This file tracks properties of this Flutter project.
-# Used by Flutter tool to assess capabilities and perform upgrades etc.
-#
-# This file should be version controlled and should not be manually edited.
-
-version:
-  revision: 78910062997c3a836feee883712c241a5fd22983
-  channel: stable
-
-project_type: app
diff --git a/LICENSE.txt b/LICENSE.txt
deleted file mode 100644
index f288702d2fa16d3cdf0035b15a9fcbc552cd88e7..0000000000000000000000000000000000000000
--- a/LICENSE.txt
+++ /dev/null
@@ -1,674 +0,0 @@
-                    GNU GENERAL PUBLIC LICENSE
-                       Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-                            Preamble
-
-  The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
-  The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works.  By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users.  We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors.  You can apply it to
-your programs, too.
-
-  When we speak of free software, we are referring to freedom, not
-price.  Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
-  To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights.  Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
-  For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received.  You must make sure that they, too, receive
-or can get the source code.  And you must show them these terms so they
-know their rights.
-
-  Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
-  For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software.  For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
-  Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so.  This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software.  The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable.  Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products.  If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
-  Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary.  To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.
-
-                       TERMS AND CONDITIONS
-
-  0. Definitions.
-
-  "This License" refers to version 3 of the GNU General Public License.
-
-  "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
-  "The Program" refers to any copyrightable work licensed under this
-License.  Each licensee is addressed as "you".  "Licensees" and
-"recipients" may be individuals or organizations.
-
-  To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy.  The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
-  A "covered work" means either the unmodified Program or a work based
-on the Program.
-
-  To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy.  Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
-  To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies.  Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
-  An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License.  If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
-  1. Source Code.
-
-  The "source code" for a work means the preferred form of the work
-for making modifications to it.  "Object code" means any non-source
-form of a work.
-
-  A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
-  The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form.  A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
-  The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities.  However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work.  For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
-  The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
-  The Corresponding Source for a work in source code form is that
-same work.
-
-  2. Basic Permissions.
-
-  All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met.  This License explicitly affirms your unlimited
-permission to run the unmodified Program.  The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work.  This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
-  You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force.  You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright.  Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
-  Conveying under any other circumstances is permitted solely under
-the conditions stated below.  Sublicensing is not allowed; section 10
-makes it unnecessary.
-
-  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
-  No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
-  When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
-  4. Conveying Verbatim Copies.
-
-  You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
-  You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
-  5. Conveying Modified Source Versions.
-
-  You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
-    a) The work must carry prominent notices stating that you modified
-    it, and giving a relevant date.
-
-    b) The work must carry prominent notices stating that it is
-    released under this License and any conditions added under section
-    7.  This requirement modifies the requirement in section 4 to
-    "keep intact all notices".
-
-    c) You must license the entire work, as a whole, under this
-    License to anyone who comes into possession of a copy.  This
-    License will therefore apply, along with any applicable section 7
-    additional terms, to the whole of the work, and all its parts,
-    regardless of how they are packaged.  This License gives no
-    permission to license the work in any other way, but it does not
-    invalidate such permission if you have separately received it.
-
-    d) If the work has interactive user interfaces, each must display
-    Appropriate Legal Notices; however, if the Program has interactive
-    interfaces that do not display Appropriate Legal Notices, your
-    work need not make them do so.
-
-  A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit.  Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
-  6. Conveying Non-Source Forms.
-
-  You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
-    a) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by the
-    Corresponding Source fixed on a durable physical medium
-    customarily used for software interchange.
-
-    b) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by a
-    written offer, valid for at least three years and valid for as
-    long as you offer spare parts or customer support for that product
-    model, to give anyone who possesses the object code either (1) a
-    copy of the Corresponding Source for all the software in the
-    product that is covered by this License, on a durable physical
-    medium customarily used for software interchange, for a price no
-    more than your reasonable cost of physically performing this
-    conveying of source, or (2) access to copy the
-    Corresponding Source from a network server at no charge.
-
-    c) Convey individual copies of the object code with a copy of the
-    written offer to provide the Corresponding Source.  This
-    alternative is allowed only occasionally and noncommercially, and
-    only if you received the object code with such an offer, in accord
-    with subsection 6b.
-
-    d) Convey the object code by offering access from a designated
-    place (gratis or for a charge), and offer equivalent access to the
-    Corresponding Source in the same way through the same place at no
-    further charge.  You need not require recipients to copy the
-    Corresponding Source along with the object code.  If the place to
-    copy the object code is a network server, the Corresponding Source
-    may be on a different server (operated by you or a third party)
-    that supports equivalent copying facilities, provided you maintain
-    clear directions next to the object code saying where to find the
-    Corresponding Source.  Regardless of what server hosts the
-    Corresponding Source, you remain obligated to ensure that it is
-    available for as long as needed to satisfy these requirements.
-
-    e) Convey the object code using peer-to-peer transmission, provided
-    you inform other peers where the object code and Corresponding
-    Source of the work are being offered to the general public at no
-    charge under subsection 6d.
-
-  A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
-  A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling.  In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage.  For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product.  A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
-  "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source.  The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
-  If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information.  But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
-  The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed.  Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
-  Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
-  7. Additional Terms.
-
-  "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law.  If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
-  When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it.  (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.)  You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
-  Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
-    a) Disclaiming warranty or limiting liability differently from the
-    terms of sections 15 and 16 of this License; or
-
-    b) Requiring preservation of specified reasonable legal notices or
-    author attributions in that material or in the Appropriate Legal
-    Notices displayed by works containing it; or
-
-    c) Prohibiting misrepresentation of the origin of that material, or
-    requiring that modified versions of such material be marked in
-    reasonable ways as different from the original version; or
-
-    d) Limiting the use for publicity purposes of names of licensors or
-    authors of the material; or
-
-    e) Declining to grant rights under trademark law for use of some
-    trade names, trademarks, or service marks; or
-
-    f) Requiring indemnification of licensors and authors of that
-    material by anyone who conveys the material (or modified versions of
-    it) with contractual assumptions of liability to the recipient, for
-    any liability that these contractual assumptions directly impose on
-    those licensors and authors.
-
-  All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10.  If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term.  If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
-  If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
-  Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
-  8. Termination.
-
-  You may not propagate or modify a covered work except as expressly
-provided under this License.  Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
-  However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
-  Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
-  Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License.  If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
-  9. Acceptance Not Required for Having Copies.
-
-  You are not required to accept this License in order to receive or
-run a copy of the Program.  Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance.  However,
-nothing other than this License grants you permission to propagate or
-modify any covered work.  These actions infringe copyright if you do
-not accept this License.  Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
-  10. Automatic Licensing of Downstream Recipients.
-
-  Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License.  You are not responsible
-for enforcing compliance by third parties with this License.
-
-  An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations.  If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
-  You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License.  For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
-  11. Patents.
-
-  A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based.  The
-work thus licensed is called the contributor's "contributor version".
-
-  A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version.  For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
-  Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
-  In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement).  To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
-  If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients.  "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
-  If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
-  A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License.  You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
-  Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
-  12. No Surrender of Others' Freedom.
-
-  If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all.  For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
-  13. Use with the GNU Affero General Public License.
-
-  Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work.  The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
-  14. Revised Versions of this License.
-
-  The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time.  Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-  Each version is given a distinguishing version number.  If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation.  If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
-  If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
-  Later license versions may give you additional or different
-permissions.  However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
-  15. Disclaimer of Warranty.
-
-  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-  16. Limitation of Liability.
-
-  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
-  17. Interpretation of Sections 15 and 16.
-
-  If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
-                     END OF TERMS AND CONDITIONS
-
-            How to Apply These Terms to Your New Programs
-
-  If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
-  To do so, attach the following notices to the program.  It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
-  If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
-    <program>  Copyright (C) <year>  <name of author>
-    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
-    This is free software, and you are welcome to redistribute it
-    under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License.  Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
-  You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-<https://www.gnu.org/licenses/>.
-
-  The GNU General Public License does not permit incorporating your program
-into proprietary programs.  If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library.  If this is what you want to do, use the GNU Lesser General
-Public License instead of this License.  But first, please read
-<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/README.md b/README.md
index a0c89b96fa7cf70ac3dbc38d0b99b5b97cfd98b6..ed4d38e96c23004b6a210a3e61c235df782eb510 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,2 @@
 Hangman
-========
 
-GNU General Public License (GPLv3).
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 148eac42d6cdd4256a3201f31c294ca3b64a2759..6399b0b1a7236cedb384ced2332873e5f4988fb1 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -37,7 +37,7 @@ if (keystorePropertiesFile.exists()) {
 }
 
 android {
-    compileSdkVersion 33
+    compileSdkVersion 34
     namespace "org.benoitharrault.hangman"
 
     defaultConfig {
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index a68012e7b467604dd3a36fe3447b18866219dd26..39852e853e9357f8bfcd654b5bdf7a64e6ee2f62 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,10 +1,5 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="org.benoitharrault.hangman">
-    <!-- io.flutter.app.FlutterApplication is an android.app.Application that
-         calls FlutterMain.startInitialization(this); in its onCreate method.
-         In most cases you can leave this as-is, but you if you want to provide
-         additional functionality it is fine to subclass or reimplement
-         FlutterApplication and put your custom class here. -->
     <uses-permission android:name="android.permission.INTERNET"/>
     <application
         android:label="Hangman"
@@ -16,19 +11,10 @@
             android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
             android:hardwareAccelerated="true"
             android:windowSoftInputMode="adjustResize">
-            <!-- Specifies an Android theme to apply to this Activity as soon as
-                 the Android process has started. This theme is visible to the user
-                 while the Flutter UI initializes. After that, this theme continues
-                 to determine the Window background behind the Flutter UI. -->
             <meta-data
               android:name="io.flutter.embedding.android.NormalTheme"
               android:resource="@style/NormalTheme"
               />
-            <!-- Displays an Android View that continues showing the launch screen
-                 Drawable until Flutter paints its first frame, then this splash
-                 screen fades out. A splash screen is useful to avoid any visual
-                 gap between the end of Android's launch screen and the painting of
-                 Flutter's first frame. -->
             <meta-data
               android:name="io.flutter.embedding.android.SplashScreenDrawable"
               android:resource="@drawable/launch_background"
@@ -38,8 +24,6 @@
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <!-- Don't delete the meta-data below.
-             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
         <meta-data
             android:name="flutterEmbedding"
             android:value="2" />
diff --git a/android/app/src/main/java/com/hangman/MainActivity.java b/android/app/src/main/java/org/benoitharrault/hangman/MainActivity.java
similarity index 100%
rename from android/app/src/main/java/com/hangman/MainActivity.java
rename to android/app/src/main/java/org/benoitharrault/hangman/MainActivity.java
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000000000000000000000000000000000000..428eb361622fc80a398549107f435d3608d8cbe8
--- /dev/null
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="?android:colorBackground" />
+
+    <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item>
+</layer-list>
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c6e7031f33f4c950119a059603cf058d16983dd3
--- /dev/null
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
+    </style>
+</resources>
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
index d1dab0397ac46cebc1617cc6dace23e3a41ee3a8..ff81bae863800e62cfd23ccf572c4ba607128496 100644
--- a/android/app/src/main/res/values/styles.xml
+++ b/android/app/src/main/res/values/styles.xml
@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+    <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
         <item name="android:windowBackground">@drawable/launch_background</item>
     </style>
-    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
-        <item name="android:windowBackground">@android:color/white</item>
+    <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
     </style>
 </resources>
diff --git a/android/gradle.properties b/android/gradle.properties
index f522c400d72b2afc43382fa45d82adf1ac156317..7f924def6fbe9a6322f047c8511363349ba0baf1 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,5 +1,5 @@
 org.gradle.jvmargs=-Xmx1536M
 android.useAndroidX=true
 android.enableJetifier=true
-app.versionName=1.2.16
-app.versionCode=27
+app.versionName=1.2.17
+app.versionCode=28
diff --git a/assets/files/word-list-fr.json b/assets/files/word-list-fr.json
deleted file mode 100644
index 6892b65ca48889cf8c0ffd276780cbbaa76eaf31..0000000000000000000000000000000000000000
--- a/assets/files/word-list-fr.json
+++ /dev/null
@@ -1,188 +0,0 @@
-{
-  "words-list": [
-    {
-      "category": "ANIMAUX",
-      "clue": "Animal",
-      "words": [
-        "ANACONDA",
-        "AUTRUCHE",
-        "BARRACUDA",
-        "BOUQUETIN",
-        "COCCINELLE",
-        "CROCODILE",
-        "DROMADAIRE",
-        "ELEPHANT",
-        "ESCARGOT",
-        "FOURMILIER",
-        "GRENOUILLE",
-        "HIPPOCAMPE",
-        "HIPPOPOTAME",
-        "KANGOUROU",
-        "LIBELLULE",
-        "PERROQUET",
-        "PIPISTRELLE",
-        "RHINOCEROS",
-        "SAUTERELLE",
-        "TARENTULE"
-      ]
-    },
-    {
-      "category": "FRUITS",
-      "clue": "Fruit",
-      "words": [
-        "AUBERGINE",
-        "BETTERAVE",
-        "CITROUILLE",
-        "CONCOMBRE",
-        "FRAMBOISE",
-        "GROSEILLE",
-        "MANDARINE",
-        "MIRABELLE",
-        "MYRTILLE",
-        "PAMPLEMOUSSE"
-      ]
-    },
-    {
-      "category": "METIERS",
-      "clue": "Métier",
-      "words": [
-        "AGRICULTEUR",
-        "ARCHEOLOGUE",
-        "ARCHITECTE",
-        "ASTRONAUTE",
-        "BIJOUTIER",
-        "BIOLOGISTE",
-        "CHARCUTIER",
-        "CHARPENTIER",
-        "CUISINIER",
-        "ELECTRICIEN",
-        "HORTICULTEUR",
-        "INFIRMIER",
-        "MECANICIEN",
-        "MENUISIER",
-        "METEOROLOGUE",
-        "PHOTOGRAPHE",
-        "PROFESSEUR",
-        "STANDARDISTE",
-        "VETERINAIRE",
-        "VOLCANOLOGUE"
-      ]
-    },
-    {
-      "category": "GEOGRAPHIE",
-      "clue": "Géographie",
-      "words": [
-        "ALLEMAGNE",
-        "ANTARCTIQUE",
-        "ARGENTINE",
-        "ATLANTIQUE",
-        "AUSTRALIE",
-        "EMBOUCHURE",
-        "HEMISPHERE",
-        "HYDROGRAPHIE",
-        "KILIMANDJARO",
-        "LUXEMBOURG",
-        "MADAGASCAR",
-        "MEDITERRANEE",
-        "MISSISSIPPI",
-        "NORMANDIE",
-        "PACIFIQUE",
-        "PLANISPHERE",
-        "STRASBOURG",
-        "SUPERFICIE",
-        "VENEZUELA",
-        "WASHINGTON"
-      ]
-    },
-    {
-      "category": "COULEURS",
-      "clue": "Couleur",
-      "words": [
-        "ROUGE",
-        "BLEU",
-        "VERT",
-        "JAUNE",
-        "VIOLET",
-        "ORANGE",
-        "MARRON",
-        "NOIR",
-        "BLANC",
-        "TURQUOISE",
-        "BEIGE",
-        "ROSE"
-      ]
-    },
-    {
-      "category": "FLEURS",
-      "clue": "Fleur",
-      "words": [
-        "ROSE",
-        "PIVOINE",
-        "TULIPE",
-        "JONQUILLE",
-        "CACTUS"
-      ]
-    },
-    {
-      "category": "SPORTS",
-      "clue": "Sport ou jeu",
-      "words": [
-        "GYMNASTIQUE",
-        "FOOTBALL",
-        "HANDBALL",
-        "COURSE",
-        "CYCLISME",
-        "RANDONNEE"
-      ]
-    },
-    {
-      "category": "ALIMENTS",
-      "clue": "Aliment ou plat",
-      "words": [
-        "FROMAGE",
-        "PIZZA",
-        "SAUCISSON",
-        "JAMBON",
-        "SALAMI",
-        "PAELLA",
-        "PATES",
-        "SALADE",
-        "SOUPE",
-        "CHOCOLAT",
-        "OEUF",
-        "CREME",
-        "LAIT",
-        "CORNICHON",
-        "FLAN",
-        "TARTE",
-        "PUREE",
-        "SAUMON",
-        "SANDWICH"
-      ]
-    },
-    {
-      "category": "VEHICULE",
-      "clue": "Véhicule ou moyen de transport",
-      "words": [
-        "VOITURE",
-        "MOTO",
-        "VELO",
-        "TRAIN",
-        "BATEAU",
-        "AVION",
-        "HELICOPTERE",
-        "AUTOBUS",
-        "CAR",
-        "TRAINEAU",
-        "FUSEE",
-        "VOILIER",
-        "PAQUEBOT",
-        "METRO",
-        "SOUS-MARIN",
-        "CAMION",
-        "TRACTEUR",
-        "KAYAK"
-      ]
-    }
-  ]
-}
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/images/gameover.png b/assets/images/gameover.png
deleted file mode 100644
index 97e98b39d94c4934e1a731d6b053b56bf7de0ed8..0000000000000000000000000000000000000000
Binary files a/assets/images/gameover.png and /dev/null differ
diff --git a/assets/images/img1.png b/assets/images/img1.png
deleted file mode 100644
index 1cdc24f0f8c88ecf576101517462b9b8c9ea4767..0000000000000000000000000000000000000000
Binary files a/assets/images/img1.png and /dev/null differ
diff --git a/assets/images/img2.png b/assets/images/img2.png
deleted file mode 100644
index 7ee894b8b2e5847c374f398ef376ce7481838129..0000000000000000000000000000000000000000
Binary files a/assets/images/img2.png and /dev/null differ
diff --git a/assets/images/img3.png b/assets/images/img3.png
deleted file mode 100644
index cf02d9bd0a49e9c5f7ab65bb763e14cb02da05e8..0000000000000000000000000000000000000000
Binary files a/assets/images/img3.png and /dev/null differ
diff --git a/assets/images/img4.png b/assets/images/img4.png
deleted file mode 100644
index 1017ccb260a995e1099335f8e3d737684200fa91..0000000000000000000000000000000000000000
Binary files a/assets/images/img4.png and /dev/null differ
diff --git a/assets/images/img5.png b/assets/images/img5.png
deleted file mode 100644
index bd09508eb49a7ac5e099826804f458161284f43d..0000000000000000000000000000000000000000
Binary files a/assets/images/img5.png and /dev/null differ
diff --git a/assets/images/img6.png b/assets/images/img6.png
deleted file mode 100644
index 4f9d5b119f47e5db3ddcfc4f3fa97bd7e6d3d9c6..0000000000000000000000000000000000000000
Binary files a/assets/images/img6.png and /dev/null differ
diff --git a/assets/images/img7.png b/assets/images/img7.png
deleted file mode 100644
index ceadc43ae13aed5d45c7e65d8dff823753acb207..0000000000000000000000000000000000000000
Binary files a/assets/images/img7.png and /dev/null differ
diff --git a/assets/images/img8.png b/assets/images/img8.png
deleted file mode 100644
index 9b7b8db3a3cebba376ba7e7f1b40a7622eaee009..0000000000000000000000000000000000000000
Binary files a/assets/images/img8.png and /dev/null differ
diff --git a/assets/images/img9.png b/assets/images/img9.png
deleted file mode 100644
index 9d6ec80ac63304397349443881eb17fe319fab91..0000000000000000000000000000000000000000
Binary files a/assets/images/img9.png and /dev/null differ
diff --git a/assets/skins/default_img1.png b/assets/skins/default_img1.png
new file mode 100644
index 0000000000000000000000000000000000000000..9271f8beb8f9532818341ceb75d8cbb2ed9aa481
Binary files /dev/null and b/assets/skins/default_img1.png differ
diff --git a/assets/skins/default_img2.png b/assets/skins/default_img2.png
new file mode 100644
index 0000000000000000000000000000000000000000..82cb8dcb2701c6bcb473975d5cb1a6dcc0d2aaf7
Binary files /dev/null and b/assets/skins/default_img2.png differ
diff --git a/assets/skins/default_img3.png b/assets/skins/default_img3.png
new file mode 100644
index 0000000000000000000000000000000000000000..9aabea90a3e180829f609ce0da4dfb40b4762896
Binary files /dev/null and b/assets/skins/default_img3.png differ
diff --git a/assets/skins/default_img4.png b/assets/skins/default_img4.png
new file mode 100644
index 0000000000000000000000000000000000000000..5de0b9da5b21f9ba50097b865a4b051431d56c67
Binary files /dev/null and b/assets/skins/default_img4.png differ
diff --git a/assets/skins/default_img5.png b/assets/skins/default_img5.png
new file mode 100644
index 0000000000000000000000000000000000000000..9aa5c5cefe1d560ed2991dc14d92985c86c4b14d
Binary files /dev/null and b/assets/skins/default_img5.png differ
diff --git a/assets/skins/default_img6.png b/assets/skins/default_img6.png
new file mode 100644
index 0000000000000000000000000000000000000000..39ba063999584c5cbeb3b42dc4afc3fbccb84256
Binary files /dev/null and b/assets/skins/default_img6.png differ
diff --git a/assets/skins/default_img7.png b/assets/skins/default_img7.png
new file mode 100644
index 0000000000000000000000000000000000000000..3cfef2247142d6683dc2512c6fedc7e3d57dd4d0
Binary files /dev/null and b/assets/skins/default_img7.png differ
diff --git a/assets/skins/default_img8.png b/assets/skins/default_img8.png
new file mode 100644
index 0000000000000000000000000000000000000000..de0079db4875944a56288f5b0d631389a0116afe
Binary files /dev/null and b/assets/skins/default_img8.png differ
diff --git a/assets/skins/default_img9.png b/assets/skins/default_img9.png
new file mode 100644
index 0000000000000000000000000000000000000000..c96a2f9afef971f07dde3b403edc6a3d6e09185f
Binary files /dev/null and b/assets/skins/default_img9.png differ
diff --git a/assets/translations/en.json b/assets/translations/en.json
new file mode 100644
index 0000000000000000000000000000000000000000..2517d964c4ce39900f58d0e80073c4f17cdb5d2f
--- /dev/null
+++ b/assets/translations/en.json
@@ -0,0 +1,12 @@
+{
+  "app_name": "Hangman",
+
+  "settings_title": "Settings",
+  "settings_label_theme": "Theme mode",
+
+  "about_title": "Informations",
+  "about_content": "Hangman",
+  "about_version": "Version: {version}",
+
+  "": ""
+}
diff --git a/assets/translations/fr.json b/assets/translations/fr.json
new file mode 100644
index 0000000000000000000000000000000000000000..c10b7472dbf48d085a967f92dacef11f26119ab1
--- /dev/null
+++ b/assets/translations/fr.json
@@ -0,0 +1,12 @@
+{
+  "app_name": "Le Pendu",
+
+  "settings_title": "Réglages",
+  "settings_label_theme": "Thème de couleurs",
+
+  "about_title": "Informations",
+  "about_content": "Le Pendu.",
+  "about_version": "Version : {version}",
+
+  "": ""
+}
diff --git a/assets/ui/button_back.png b/assets/ui/button_back.png
new file mode 100644
index 0000000000000000000000000000000000000000..cc48ffb1dbb653d9a996f139dfbe02969724bfa5
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..5e4f217689b11e444b7163557d7e5d68f3bbfe7d
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..b2ea0a02d05e42377eb551a4b51428b511a32f5d
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..6845e2f5c21598ab61f1684d2075aeec0334bf23
Binary files /dev/null and b/assets/ui/button_start.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/ui/game_win.png b/assets/ui/game_win.png
new file mode 100644
index 0000000000000000000000000000000000000000..876334279c1711b349a62131a33607eecf924eb6
Binary files /dev/null and b/assets/ui/game_win.png differ
diff --git a/assets/ui/placeholder.png b/assets/ui/placeholder.png
new file mode 100644
index 0000000000000000000000000000000000000000..814df31be6ddc4275ebe4490c79365578dbef1f0
Binary files /dev/null and b/assets/ui/placeholder.png differ
diff --git a/fdroid_metadata.yml b/fdroid_metadata.yml
index 5d0a98c47ef8d0ab8c94de5a88a1aee1d29acf68..4152397802a427fa7b6a5b806f9c76549d962a47 100644
--- a/fdroid_metadata.yml
+++ b/fdroid_metadata.yml
@@ -5,9 +5,9 @@ Name:         Hangman Game
 AutoName:     Hangman
 License:      GPL-3.0-only
 WebSite:      'https://git.harrault.fr/android/hangman'
-SourceCode:   https://git.harrault.fr/android/hangman
-IssueTracker: https://git.harrault.fr/android/hangman/issues
-Changelog:    https://git.harrault.fr/android/hangman/-/tags
+SourceCode:   'https://git.harrault.fr/android/hangman'
+IssueTracker: 'https://git.harrault.fr/android/hangman/issues'
+Changelog:    'https://git.harrault.fr/android/hangman/-/tags'
 Summary:      ''
 
 Description:  |-
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..0000000000000000000000000000000000000000
Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 442d9132ea32808ad980df4bd233b359f76341a7..0000000000000000000000000000000000000000
--- a/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,5 +0,0 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
deleted file mode 100755
index 4f906e0c811fc9e230eb44819f509cd0627f2600..0000000000000000000000000000000000000000
--- a/gradlew
+++ /dev/null
@@ -1,185 +0,0 @@
-#!/usr/bin/env sh
-
-#
-# Copyright 2015 the original author or authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-##############################################################################
-##
-##  Gradle start up script for UN*X
-##
-##############################################################################
-
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
-    ls=`ls -ld "$PRG"`
-    link=`expr "$ls" : '.*-> \(.*\)$'`
-    if expr "$link" : '/.*' > /dev/null; then
-        PRG="$link"
-    else
-        PRG=`dirname "$PRG"`"/$link"
-    fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
-
-warn () {
-    echo "$*"
-}
-
-die () {
-    echo
-    echo "$*"
-    echo
-    exit 1
-}
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-nonstop=false
-case "`uname`" in
-  CYGWIN* )
-    cygwin=true
-    ;;
-  Darwin* )
-    darwin=true
-    ;;
-  MINGW* )
-    msys=true
-    ;;
-  NONSTOP* )
-    nonstop=true
-    ;;
-esac
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
-    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
-        # IBM's JDK on AIX uses strange locations for the executables
-        JAVACMD="$JAVA_HOME/jre/sh/java"
-    else
-        JAVACMD="$JAVA_HOME/bin/java"
-    fi
-    if [ ! -x "$JAVACMD" ] ; then
-        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-    fi
-else
-    JAVACMD="java"
-    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-fi
-
-# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
-    MAX_FD_LIMIT=`ulimit -H -n`
-    if [ $? -eq 0 ] ; then
-        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
-            MAX_FD="$MAX_FD_LIMIT"
-        fi
-        ulimit -n $MAX_FD
-        if [ $? -ne 0 ] ; then
-            warn "Could not set maximum file descriptor limit: $MAX_FD"
-        fi
-    else
-        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
-    fi
-fi
-
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
-    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
-    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
-    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
-    JAVACMD=`cygpath --unix "$JAVACMD"`
-
-    # We build the pattern for arguments to be converted via cygpath
-    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
-    SEP=""
-    for dir in $ROOTDIRSRAW ; do
-        ROOTDIRS="$ROOTDIRS$SEP$dir"
-        SEP="|"
-    done
-    OURCYGPATTERN="(^($ROOTDIRS))"
-    # Add a user-defined pattern to the cygpath arguments
-    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
-        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
-    fi
-    # Now convert the arguments - kludge to limit ourselves to /bin/sh
-    i=0
-    for arg in "$@" ; do
-        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
-        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
-
-        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
-            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
-        else
-            eval `echo args$i`="\"$arg\""
-        fi
-        i=`expr $i + 1`
-    done
-    case $i in
-        0) set -- ;;
-        1) set -- "$args0" ;;
-        2) set -- "$args0" "$args1" ;;
-        3) set -- "$args0" "$args1" "$args2" ;;
-        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
-        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
-        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
-        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
-        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
-        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
-    esac
-fi
-
-# Escape application args
-save () {
-    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
-    echo " "
-}
-APP_ARGS=`save "$@"`
-
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-
-exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
deleted file mode 100644
index ac1b06f93825db68fb0c0b5150917f340eaa5d02..0000000000000000000000000000000000000000
--- a/gradlew.bat
+++ /dev/null
@@ -1,89 +0,0 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem      https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem  Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
diff --git a/icons/build_game_icons.sh b/icons/build_game_icons.sh
deleted file mode 100755
index 218080d1eb12952690ba1e182a468f6e3cf03d65..0000000000000000000000000000000000000000
--- a/icons/build_game_icons.sh
+++ /dev/null
@@ -1,98 +0,0 @@
-#! /bin/bash
-
-# Check dependencies
-command -v inkscape >/dev/null 2>&1 || { echo >&2 "I require inkscape but it's not installed. Aborting."; exit 1; }
-command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not installed. Aborting."; exit 1; }
-command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; }
-
-CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
-BASE_DIR="$(dirname "${CURRENT_DIR}")"
-ASSETS_DIR="${BASE_DIR}/assets"
-
-OPTIPNG_OPTIONS="-preserve -quiet -o7"
-ICON_SIZE=192
-
-#######################################################
-
-# Game images
-AVAILABLE_GAME_IMAGES="
-"
-
-# Settings images
-AVAILABLES_GAME_SETTINGS="
-"
-
-#######################################################
-
-# optimize svg
-function optimize_svg() {
-  SOURCE="$1"
-
-  cp ${SOURCE} ${SOURCE}.tmp
-  scour \
-      --remove-descriptive-elements \
-      --enable-id-stripping \
-      --enable-viewboxing \
-      --enable-comment-stripping \
-      --nindent=4 \
-      --quiet \
-      -i ${SOURCE}.tmp \
-      -o ${SOURCE}
-  rm ${SOURCE}.tmp
-}
-
-# build icons
-function build_icon() {
-  SOURCE="$1"
-  TARGET="$2"
-
-  echo "Building ${TARGET}"
-
-  if [ ! -f "${SOURCE}" ]; then
-    echo "Missing file: ${SOURCE}"
-    exit 1
-  fi
-
-  optimize_svg "${SOURCE}"
-
-  inkscape \
-      --export-width=${ICON_SIZE} \
-      --export-height=${ICON_SIZE} \
-      --export-filename=${TARGET} \
-      ${SOURCE}
-
-  optipng ${OPTIPNG_OPTIONS} ${TARGET}
-}
-
-function build_settings_icons() {
-  INPUT_STRING="$1"
-
-  SETTING_NAME="$(echo "${INPUT_STRING}" | cut -d":" -f1)"
-  SETTING_VALUES="$(echo "${INPUT_STRING}" | cut -d":" -f2 | tr "," " ")"
-
-  for SETTING_VALUE in ${SETTING_VALUES}
-  do
-    SETTING_CODE="${SETTING_NAME}_${SETTING_VALUE}"
-    build_icon ${CURRENT_DIR}/${SETTING_CODE}.svg ${ASSETS_DIR}/icons/${SETTING_CODE}.png
-  done
-}
-
-#######################################################
-
-# Create output folders
-mkdir -p ${ASSETS_DIR}/icons
-
-# Delete existing generated images
-find ${ASSETS_DIR}/icons -type f -name "*.png" -delete
-
-# build game images
-for GAME_IMAGE in ${AVAILABLE_GAME_IMAGES}
-do
-  build_icon ${CURRENT_DIR}/${GAME_IMAGE}.svg ${ASSETS_DIR}/icons/${GAME_IMAGE}.png
-done
-
-# build settings images
-for GAME_SETTING in ${AVAILABLES_GAME_SETTINGS}
-do
-  build_settings_icons "${GAME_SETTING}"
-done
diff --git a/lib/config/default_game_settings.dart b/lib/config/default_game_settings.dart
new file mode 100644
index 0000000000000000000000000000000000000000..f1f3a0c50240191264148f9ecf212227b1e99caf
--- /dev/null
+++ b/lib/config/default_game_settings.dart
@@ -0,0 +1,49 @@
+import 'package:hangman/utils/tools.dart';
+
+class DefaultGameSettings {
+  // available game parameters codes
+  static const String parameterCodeGameMode = 'gameMode';
+  static const String parameterCodeGameLevel = 'gameLevel';
+  static const List<String> availableParameters = [
+    parameterCodeGameMode,
+    parameterCodeGameLevel,
+  ];
+
+  // game mode: available values
+  static const String gameModeValueOnline = 'online';
+  static const String gameModeValueOffline = 'offline';
+  static const List<String> allowedGameModeValues = [
+    gameModeValueOnline,
+    gameModeValueOffline,
+  ];
+  // game mode: default value
+  static const String defaultGameModeValue = gameModeValueOffline;
+
+  // game level: available values
+  static const String gameLevelValueEasy = 'easy';
+  static const String gameLevelValueHard = 'hard';
+  static const List<String> allowedGameLevelValues = [
+    gameLevelValueEasy,
+    gameLevelValueHard,
+  ];
+  // game level: default value
+  static const String defaultGameLevelValue = gameLevelValueEasy;
+
+  // available values from parameter code
+  static List<String> getAvailableValues(String parameterCode) {
+    switch (parameterCode) {
+      case parameterCodeGameMode:
+        return DefaultGameSettings.allowedGameModeValues;
+      case parameterCodeGameLevel:
+        return DefaultGameSettings.allowedGameLevelValues;
+    }
+
+    printlog('Did not find any available value for game parameter "$parameterCode".');
+    return [];
+  }
+
+  // parameters displayed with assets (instead of painter)
+  static List<String> displayedWithAssets = [
+    //
+  ];
+}
diff --git a/lib/config/default_global_settings.dart b/lib/config/default_global_settings.dart
new file mode 100644
index 0000000000000000000000000000000000000000..678b88053acaf959db2266ce3fee9c026ef8d898
--- /dev/null
+++ b/lib/config/default_global_settings.dart
@@ -0,0 +1,33 @@
+import 'package:hangman/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 [];
+  }
+
+  // parameters displayed with assets (instead of painter)
+  static List<String> displayedWithAssets = [
+    //
+  ];
+}
diff --git a/lib/config/game_colors.dart b/lib/config/game_colors.dart
new file mode 100644
index 0000000000000000000000000000000000000000..b42ea3f797cd70440c37e6f462631593323c8f51
--- /dev/null
+++ b/lib/config/game_colors.dart
@@ -0,0 +1,6 @@
+import 'dart:ui';
+
+class GameColors {
+  static const Color boardColor = Color(0xff004D00);
+  static const Color keyColor = Color(0xFFD59500);
+}
diff --git a/lib/config/menu.dart b/lib/config/menu.dart
new file mode 100644
index 0000000000000000000000000000000000000000..d59a4278e25e81b369d2eb12cd4cd055ace5fce0
--- /dev/null
+++ b/lib/config/menu.dart
@@ -0,0 +1,52 @@
+import 'package:flutter/material.dart';
+import 'package:unicons/unicons.dart';
+
+import 'package:hangman/ui/screens/page_about.dart';
+import 'package:hangman/ui/screens/page_game.dart';
+import 'package:hangman/ui/screens/page_settings.dart';
+
+class MenuItem {
+  final Icon icon;
+  final Widget page;
+
+  const MenuItem({
+    required this.icon,
+    required this.page,
+  });
+}
+
+class Menu {
+  static const indexGame = 0;
+  static const menuItemGame = MenuItem(
+    icon: Icon(UniconsLine.home),
+    page: PageGame(),
+  );
+
+  static const indexSettings = 1;
+  static const menuItemSettings = MenuItem(
+    icon: Icon(UniconsLine.setting),
+    page: PageSettings(),
+  );
+
+  static const indexAbout = 2;
+  static const menuItemAbout = MenuItem(
+    icon: Icon(UniconsLine.info_circle),
+    page: PageAbout(),
+  );
+
+  static Map<int, MenuItem> items = {
+    indexGame: menuItemGame,
+    indexSettings: menuItemSettings,
+    indexAbout: menuItemAbout,
+  };
+
+  static bool isIndexAllowed(int pageIndex) {
+    return items.keys.contains(pageIndex);
+  }
+
+  static Widget getPageWidget(int pageIndex) {
+    return items[pageIndex]?.page ?? menuItemGame.page;
+  }
+
+  static int itemsCount = Menu.items.length;
+}
diff --git a/lib/config/theme.dart b/lib/config/theme.dart
new file mode 100644
index 0000000000000000000000000000000000000000..74f532fd5abf693979118609564d29167e902009
--- /dev/null
+++ b/lib/config/theme.dart
@@ -0,0 +1,190 @@
+import 'package:flutter/material.dart';
+
+/// Colors from Tailwind CSS (v3.0) - June 2022
+///
+/// https://tailwindcss.com/docs/customizing-colors
+
+const int _primaryColor = 0xFF6366F1;
+const MaterialColor primarySwatch = MaterialColor(_primaryColor, <int, Color>{
+  50: Color(0xFFEEF2FF), // indigo-50
+  100: Color(0xFFE0E7FF), // indigo-100
+  200: Color(0xFFC7D2FE), // indigo-200
+  300: Color(0xFFA5B4FC), // indigo-300
+  400: Color(0xFF818CF8), // indigo-400
+  500: Color(_primaryColor), // indigo-500
+  600: Color(0xFF4F46E5), // indigo-600
+  700: Color(0xFF4338CA), // indigo-700
+  800: Color(0xFF3730A3), // indigo-800
+  900: Color(0xFF312E81), // indigo-900
+});
+
+const int _textColor = 0xFF64748B;
+const MaterialColor textSwatch = MaterialColor(_textColor, <int, Color>{
+  50: Color(0xFFF8FAFC), // slate-50
+  100: Color(0xFFF1F5F9), // slate-100
+  200: Color(0xFFE2E8F0), // slate-200
+  300: Color(0xFFCBD5E1), // slate-300
+  400: Color(0xFF94A3B8), // slate-400
+  500: Color(_textColor), // slate-500
+  600: Color(0xFF475569), // slate-600
+  700: Color(0xFF334155), // slate-700
+  800: Color(0xFF1E293B), // slate-800
+  900: Color(0xFF0F172A), // slate-900
+});
+
+const Color errorColor = Color(0xFFDC2626); // red-600
+
+final ColorScheme lightColorScheme = ColorScheme.light(
+  primary: primarySwatch.shade500,
+  secondary: primarySwatch.shade500,
+  onSecondary: Colors.white,
+  error: errorColor,
+  onSurface: textSwatch.shade500,
+  surface: textSwatch.shade50,
+  surfaceContainerHighest: Colors.white,
+  shadow: textSwatch.shade900.withOpacity(.1),
+);
+
+final ColorScheme darkColorScheme = ColorScheme.dark(
+  primary: primarySwatch.shade500,
+  secondary: primarySwatch.shade500,
+  onSecondary: Colors.white,
+  error: errorColor,
+  onSurface: textSwatch.shade300,
+  surface: const Color(0xFF262630),
+  surfaceContainerHighest: const Color(0xFF282832),
+  shadow: textSwatch.shade900.withOpacity(.2),
+);
+
+final ThemeData lightTheme = ThemeData(
+  colorScheme: lightColorScheme,
+  fontFamily: 'Nunito',
+  textTheme: TextTheme(
+    displayLarge: TextStyle(
+      color: textSwatch.shade700,
+      fontFamily: 'Nunito',
+    ),
+    displayMedium: TextStyle(
+      color: textSwatch.shade600,
+      fontFamily: 'Nunito',
+    ),
+    displaySmall: TextStyle(
+      color: textSwatch.shade500,
+      fontFamily: 'Nunito',
+    ),
+    headlineLarge: TextStyle(
+      color: textSwatch.shade700,
+      fontFamily: 'Nunito',
+    ),
+    headlineMedium: TextStyle(
+      color: textSwatch.shade600,
+      fontFamily: 'Nunito',
+    ),
+    headlineSmall: TextStyle(
+      color: textSwatch.shade500,
+      fontFamily: 'Nunito',
+    ),
+    titleLarge: TextStyle(
+      color: textSwatch.shade700,
+      fontFamily: 'Nunito',
+    ),
+    titleMedium: TextStyle(
+      color: textSwatch.shade600,
+      fontFamily: 'Nunito',
+    ),
+    titleSmall: TextStyle(
+      color: textSwatch.shade500,
+      fontFamily: 'Nunito',
+    ),
+    bodyLarge: TextStyle(
+      color: textSwatch.shade700,
+      fontFamily: 'Nunito',
+    ),
+    bodyMedium: TextStyle(
+      color: textSwatch.shade600,
+      fontFamily: 'Nunito',
+    ),
+    bodySmall: TextStyle(
+      color: textSwatch.shade500,
+      fontFamily: 'Nunito',
+    ),
+    labelLarge: TextStyle(
+      color: textSwatch.shade700,
+      fontFamily: 'Nunito',
+    ),
+    labelMedium: TextStyle(
+      color: textSwatch.shade600,
+      fontFamily: 'Nunito',
+    ),
+    labelSmall: TextStyle(
+      color: textSwatch.shade500,
+      fontFamily: 'Nunito',
+    ),
+  ),
+);
+
+final ThemeData darkTheme = lightTheme.copyWith(
+  colorScheme: darkColorScheme,
+  textTheme: TextTheme(
+    displayLarge: TextStyle(
+      color: textSwatch.shade200,
+      fontFamily: 'Nunito',
+    ),
+    displayMedium: TextStyle(
+      color: textSwatch.shade300,
+      fontFamily: 'Nunito',
+    ),
+    displaySmall: TextStyle(
+      color: textSwatch.shade400,
+      fontFamily: 'Nunito',
+    ),
+    headlineLarge: TextStyle(
+      color: textSwatch.shade200,
+      fontFamily: 'Nunito',
+    ),
+    headlineMedium: TextStyle(
+      color: textSwatch.shade300,
+      fontFamily: 'Nunito',
+    ),
+    headlineSmall: TextStyle(
+      color: textSwatch.shade400,
+      fontFamily: 'Nunito',
+    ),
+    titleLarge: TextStyle(
+      color: textSwatch.shade200,
+      fontFamily: 'Nunito',
+    ),
+    titleMedium: TextStyle(
+      color: textSwatch.shade300,
+      fontFamily: 'Nunito',
+    ),
+    titleSmall: TextStyle(
+      color: textSwatch.shade400,
+      fontFamily: 'Nunito',
+    ),
+    bodyLarge: TextStyle(
+      color: textSwatch.shade200,
+      fontFamily: 'Nunito',
+    ),
+    bodyMedium: TextStyle(
+      color: textSwatch.shade300,
+      fontFamily: 'Nunito',
+    ),
+    bodySmall: TextStyle(
+      color: textSwatch.shade400,
+      fontFamily: 'Nunito',
+    ),
+    labelLarge: TextStyle(
+      color: textSwatch.shade200,
+      fontFamily: 'Nunito',
+    ),
+    labelMedium: TextStyle(
+      color: textSwatch.shade300,
+      fontFamily: 'Nunito',
+    ),
+    labelSmall: TextStyle(
+      color: textSwatch.shade400,
+      fontFamily: 'Nunito',
+    ),
+  ),
+);
diff --git a/lib/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart
new file mode 100644
index 0000000000000000000000000000000000000000..3aa1c51b6742b9d23085faa15a06d45ad17d8538
--- /dev/null
+++ b/lib/cubit/game_cubit.dart
@@ -0,0 +1,148 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hangman/data/fetch_data_helper.dart';
+import 'package:hangman/models/game/picked_word.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:hangman/models/game/game.dart';
+import 'package:hangman/models/settings/settings_game.dart';
+import 'package:hangman/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,
+      gettingSecretWord: state.currentGame.gettingSecretWord,
+      // Base data
+      secretWord: state.currentGame.secretWord,
+      clue: state.currentGame.clue,
+      // Game data
+      hiddenWord: state.currentGame.hiddenWord,
+      usedLetters: state.currentGame.usedLetters,
+      errorsCount: state.currentGame.errorsCount,
+      isClueDisplayed: state.currentGame.isClueDisplayed,
+    );
+    // game.dump();
+
+    updateState(game);
+  }
+
+  void startNewGame({
+    required GameSettings gameSettings,
+    required GlobalSettings globalSettings,
+  }) {
+    final Game newGame = Game.createNew(
+      // Settings
+      gameSettings: gameSettings,
+      globalSettings: globalSettings,
+    );
+
+    updateState(newGame);
+    refresh();
+
+    FetchDataHelper().pickRandomWord(gameSettings).then((PickedWord pickedWord) {
+      state.currentGame.gettingSecretWord = false;
+      state.currentGame.secretWord = pickedWord.word;
+      state.currentGame.clue = pickedWord.clue;
+      state.currentGame.hiddenWord = List<String>.generate(pickedWord.word.length, (i) => '_');
+      state.currentGame.dump();
+
+      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();
+  }
+
+  void submitLetter(String key) {
+    if (!state.currentGame.usedLetters.contains(key)) {
+      updateUsedLetters(key);
+
+      if (state.currentGame.secretWord.contains(key)) {
+        for (int index = 0; index < state.currentGame.secretWord.length; index++) {
+          if (key == state.currentGame.secretWord[index]) {
+            updateHiddenWord(index, key);
+          }
+        }
+      } else {
+        incrementErrorsCount();
+      }
+    }
+
+    if ((state.currentGame.hiddenWordAsString == state.currentGame.secretWord) ||
+        (state.currentGame.errorsCount == 8)) {
+      state.currentGame.isFinished = true;
+    }
+
+    refresh();
+  }
+
+  void updateUsedLetters(String key) {
+    state.currentGame.usedLetters.add(key);
+    refresh();
+  }
+
+  void updateHiddenWord(int index, String letter) {
+    state.currentGame.hiddenWord[index] = letter;
+    refresh();
+  }
+
+  void incrementErrorsCount() {
+    state.currentGame.errorsCount++;
+    refresh();
+  }
+
+  void toggleClueDisplay() {
+    state.currentGame.isClueDisplayed = !state.currentGame.isClueDisplayed;
+    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..85303abe21a8dae2319d3e3c75ed7afb1574d143
--- /dev/null
+++ b/lib/cubit/nav_cubit.dart
@@ -0,0 +1,37 @@
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:hangman/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..09144f1439b220798c221dbaf6af946a06e77a29
--- /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:hangman/config/default_game_settings.dart';
+import 'package:hangman/models/settings/settings_game.dart';
+
+part 'settings_game_state.dart';
+
+class GameSettingsCubit extends HydratedCubit<GameSettingsState> {
+  GameSettingsCubit() : super(GameSettingsState(settings: GameSettings.createDefault()));
+
+  void setValues({
+    String? gameMode,
+    String? gameLevel,
+  }) {
+    emit(
+      GameSettingsState(
+        settings: GameSettings(
+          gameMode: gameMode ?? state.settings.gameMode,
+          gameLevel: gameLevel ?? state.settings.gameLevel,
+        ),
+      ),
+    );
+  }
+
+  String getParameterValue(String code) {
+    switch (code) {
+      case DefaultGameSettings.parameterCodeGameMode:
+        return GameSettings.getGameModeValueFromUnsafe(state.settings.gameMode);
+      case DefaultGameSettings.parameterCodeGameLevel:
+        return GameSettings.getGameLevelValueFromUnsafe(state.settings.gameLevel);
+    }
+
+    return '';
+  }
+
+  void setParameterValue(String code, String value) {
+    final String gameMode = code == DefaultGameSettings.parameterCodeGameMode
+        ? value
+        : getParameterValue(DefaultGameSettings.parameterCodeGameMode);
+    final String gameLevel = code == DefaultGameSettings.parameterCodeGameLevel
+        ? value
+        : getParameterValue(DefaultGameSettings.parameterCodeGameLevel);
+
+    setValues(
+      gameMode: gameMode,
+      gameLevel: gameLevel,
+    );
+  }
+
+  @override
+  GameSettingsState? fromJson(Map<String, dynamic> json) {
+    final String gameMode = json[DefaultGameSettings.parameterCodeGameMode] as String;
+    final String gameLevel = json[DefaultGameSettings.parameterCodeGameLevel] as String;
+
+    return GameSettingsState(
+      settings: GameSettings(
+        gameMode: gameMode,
+        gameLevel: gameLevel,
+      ),
+    );
+  }
+
+  @override
+  Map<String, dynamic>? toJson(GameSettingsState state) {
+    return <String, dynamic>{
+      DefaultGameSettings.parameterCodeGameMode: state.settings.gameMode,
+      DefaultGameSettings.parameterCodeGameLevel: state.settings.gameLevel,
+    };
+  }
+}
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..091dd900589b74458d55241d7ce97cdbb62f211b
--- /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:hangman/config/default_global_settings.dart';
+import 'package:hangman/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/data/dictionary_french_easy.dart b/lib/data/dictionary_french_easy.dart
new file mode 100644
index 0000000000000000000000000000000000000000..f9c646bfc13bdd4ed2ffb91a1cfa378f5f01f172
--- /dev/null
+++ b/lib/data/dictionary_french_easy.dart
@@ -0,0 +1,193 @@
+const List<dynamic> dictionaryFrenchEasy = [
+  {
+    "category": "ANIMAUX",
+    "clue": "Animal",
+    "words": [
+      "ANACONDA",
+      "AUTRUCHE",
+      "BARRACUDA",
+      "BOUQUETIN",
+      "COCCINELLE",
+      "CROCODILE",
+      "DROMADAIRE",
+      "ELEPHANT",
+      "ESCARGOT",
+      "FOURMILIER",
+      "GRENOUILLE",
+      "HIPPOCAMPE",
+      "HIPPOPOTAME",
+      "KANGOUROU",
+      "LIBELLULE",
+      "PERROQUET",
+      "PIPISTRELLE",
+      "RHINOCEROS",
+      "SAUTERELLE",
+      "TARENTULE"
+    ]
+  },
+  {
+    "category": "FRUITS",
+    "clue": "Fruit",
+    "words": [
+      "AUBERGINE",
+      "BETTERAVE",
+      "CITROUILLE",
+      "CONCOMBRE",
+      "FRAMBOISE",
+      "GROSEILLE",
+      "MANDARINE",
+      "MIRABELLE",
+      "MYRTILLE",
+      "PAMPLEMOUSSE"
+    ]
+  },
+  {
+    "category": "METIERS",
+    "clue": "Métier",
+    "words": [
+      "AGRICULTEUR",
+      "ARCHEOLOGUE",
+      "ARCHITECTE",
+      "ASTRONAUTE",
+      "BIJOUTIER",
+      "BIOLOGISTE",
+      "CHARCUTIER",
+      "CHARPENTIER",
+      "CUISINIER",
+      "ELECTRICIEN",
+      "HORTICULTEUR",
+      "INFIRMIER",
+      "MECANICIEN",
+      "MENUISIER",
+      "METEOROLOGUE",
+      "PHOTOGRAPHE",
+      "PROFESSEUR",
+      "STANDARDISTE",
+      "VETERINAIRE",
+      "VOLCANOLOGUE"
+    ]
+  },
+  {
+    "category": "GEOGRAPHIE",
+    "clue": "Géographie",
+    "words": [
+      "ALLEMAGNE",
+      "ANTARCTIQUE",
+      "ARGENTINE",
+      "ATLANTIQUE",
+      "AUSTRALIE",
+      "EMBOUCHURE",
+      "HEMISPHERE",
+      "HYDROGRAPHIE",
+      "KILIMANDJARO",
+      "LUXEMBOURG",
+      "MADAGASCAR",
+      "MEDITERRANEE",
+      "MISSISSIPPI",
+      "NORMANDIE",
+      "PACIFIQUE",
+      "PLANISPHERE",
+      "STRASBOURG",
+      "SUPERFICIE",
+      "VENEZUELA",
+      "WASHINGTON"
+    ]
+  },
+  {
+    "category": "COULEURS",
+    "clue": "Couleur",
+    "words": [
+      "ROUGE",
+      "BLEU",
+      "VERT",
+      "JAUNE",
+      "VIOLET",
+      "ORANGE",
+      "MARRON",
+      "NOIR",
+      "BLANC",
+      "TURQUOISE",
+      "BEIGE",
+      "ROSE"
+    ]
+  },
+  {
+    "category": "FLEURS",
+    "clue": "Fleur",
+    "words": [
+      "ROSE",
+      "PIVOINE",
+      "TULIPE",
+      "JONQUILLE",
+      "CACTUS",
+      "PETUNIA",
+      "COQUELICOT",
+      "JACINTHE"
+    ]
+  },
+  {
+    "category": "SPORTS",
+    "clue": "Sport ou jeu",
+    "words": [
+      "GYMNASTIQUE",
+      "FOOTBALL",
+      "HANDBALL",
+      "COURSE",
+      "CYCLISME",
+      "RANDONNEE",
+      "NATATION",
+      "KARATE",
+      "ESCRIME",
+      "EQUITATION"
+    ]
+  },
+  {
+    "category": "ALIMENTS",
+    "clue": "Aliment ou plat",
+    "words": [
+      "FROMAGE",
+      "PIZZA",
+      "SAUCISSON",
+      "JAMBON",
+      "SALAMI",
+      "PAELLA",
+      "PATES",
+      "SALADE",
+      "SOUPE",
+      "CHOCOLAT",
+      "OEUF",
+      "CREME",
+      "LAIT",
+      "CORNICHON",
+      "FLAN",
+      "TARTE",
+      "PUREE",
+      "SAUMON",
+      "SANDWICH"
+    ]
+  },
+  {
+    "category": "VEHICULE",
+    "clue": "Véhicule ou moyen de transport",
+    "words": [
+      "VOITURE",
+      "MOTO",
+      "VELO",
+      "TRAIN",
+      "BATEAU",
+      "AVION",
+      "HELICOPTERE",
+      "AUTOBUS",
+      "CAR",
+      "TRAINEAU",
+      "FUSEE",
+      "VOILIER",
+      "PAQUEBOT",
+      "METRO",
+      "SOUS-MARIN",
+      "CAMION",
+      "TRACTEUR",
+      "KAYAK"
+    ]
+  }
+];
diff --git a/lib/words/list_french_words.dart b/lib/data/dictionary_french_hard.dart
similarity index 99%
rename from lib/words/list_french_words.dart
rename to lib/data/dictionary_french_hard.dart
index 26f77025aef6bd99628169924d664f0526b983e5..fcd2bf0035517b9bb7320697cbe048aa58090434 100644
--- a/lib/words/list_french_words.dart
+++ b/lib/data/dictionary_french_hard.dart
@@ -1,5 +1,5 @@
 /// The list of french words, ~340,000 words in total
-const List<String> listFrenchWords = [
+const List<String> dictionaryFrench = [
   'a',
   'à',
   'abaissa',
diff --git a/lib/data/fetch_data_helper.dart b/lib/data/fetch_data_helper.dart
new file mode 100644
index 0000000000000000000000000000000000000000..8ef7ca5f22e430b1a9904eacc303fc432fe2509e
--- /dev/null
+++ b/lib/data/fetch_data_helper.dart
@@ -0,0 +1,116 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:math' show Random;
+
+import 'package:diacritic/diacritic.dart';
+import 'package:hangman/data/dictionary_french_easy.dart';
+import 'package:hangman/data/dictionary_french_hard.dart';
+import 'package:hangman/models/game/picked_word.dart';
+import 'package:html/parser.dart';
+import 'package:html/dom.dart';
+import 'package:http/http.dart' as http;
+
+import 'package:hangman/config/default_game_settings.dart';
+import 'package:hangman/models/settings/settings_game.dart';
+
+class FetchDataHelper {
+  FetchDataHelper();
+
+  Future<PickedWord> pickRandomWord(GameSettings gameSettings) async {
+    switch (gameSettings.gameMode) {
+      case DefaultGameSettings.gameModeValueOffline:
+        return Future(() => pickRandomOfflineWord(gameSettings));
+      case DefaultGameSettings.gameModeValueOnline:
+        return await pickRandomOnlineWord(gameSettings);
+    }
+
+    return PickedWord.none;
+  }
+
+  PickedWord pickRandomOfflineWord(GameSettings gameSettings) {
+    switch (gameSettings.gameLevel) {
+      case DefaultGameSettings.gameLevelValueEasy:
+        return wordFromLocalEasyDictionary();
+      case DefaultGameSettings.gameLevelValueHard:
+        return wordFromLocalFullDictionary();
+    }
+
+    return PickedWord.none;
+  }
+
+  Future<PickedWord> pickRandomOnlineWord(GameSettings gameSettings) {
+    const baseUrl = 'https://www.palabrasaleatorias.com/mots-aleatoires.php';
+    switch (gameSettings.gameLevel) {
+      case DefaultGameSettings.gameLevelValueEasy:
+        return wordFromWeb('$baseUrl?fs=1&fs2=0&Submit=Nouveau+mot');
+      case DefaultGameSettings.gameLevelValueHard:
+        return wordFromWeb('$baseUrl?fs=1&fs2=1&Submit=Nouveau+mot');
+    }
+    return Future(() => PickedWord.none);
+  }
+
+  Future<PickedWord> wordFromWeb(String url) async {
+    try {
+      final response = await http.Client().get(Uri.parse(url));
+      if (response.statusCode == 200) {
+        final html = parse(utf8.decode(response.bodyBytes));
+        Element element = html.getElementsByTagName('div').last;
+        String word = clean(element.text);
+        if (word == '') {
+          throw Exception();
+        }
+        return PickedWord(
+          word: word,
+          clue: '',
+        );
+      } else {
+        throw Exception();
+      }
+    } catch (e) {
+      return PickedWord.none;
+    }
+  }
+
+  PickedWord wordFromLocalFullDictionary() {
+    final String rawWord = dictionaryFrench[Random().nextInt(dictionaryFrench.length)];
+
+    return PickedWord(
+      word: clean(rawWord),
+      clue: '',
+    );
+  }
+
+  PickedWord wordFromLocalEasyDictionary() {
+    final Map<String, dynamic> randomCategory =
+        dictionaryFrenchEasy[Random().nextInt(dictionaryFrenchEasy.length)];
+
+    final clue = randomCategory['clue'];
+    final words = randomCategory['words'] as List<String>;
+
+    final word = words[Random().nextInt(words.length)];
+
+    return PickedWord(
+      word: clean(word),
+      clue: clue,
+    );
+  }
+
+  String clean(String wordCandidate) {
+    String word = wordCandidate.trim();
+
+    if (word.contains(' ')) {
+      return '';
+    }
+
+    word = removeDiacritics(word);
+    word = word.toUpperCase();
+    word = word.replaceAll(RegExp('[0-9]'), '');
+    word = word.replaceAll(RegExp('[^A-Z]'), '');
+
+    if (word.length < 3 || word.length > 12) {
+      return '';
+    }
+
+    return word;
+  }
+}
diff --git a/lib/main.dart b/lib/main.dart
index a96452ba967bda7e898b0a3db2cc239d166e977b..dbcc7a7bbe21aa82bc61e254512d60b074cab448 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,38 +1,113 @@
+import 'dart:io';
+
+import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:hive/hive.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+import 'package:path_provider/path_provider.dart';
+
+import 'package:hangman/config/default_global_settings.dart';
+import 'package:hangman/config/theme.dart';
+import 'package:hangman/cubit/game_cubit.dart';
+import 'package:hangman/cubit/nav_cubit.dart';
+import 'package:hangman/cubit/settings_game_cubit.dart';
+import 'package:hangman/cubit/settings_global_cubit.dart';
+import 'package:hangman/cubit/theme_cubit.dart';
+import 'package:hangman/ui/skeleton.dart';
 
-import 'package:hangman/provider/data.dart';
-import 'package:hangman/screens/game.dart';
-import 'package:hangman/screens/home.dart';
-import 'package:hangman/screens/scores.dart';
-import 'package:hangman/utils/constants.dart';
+void main() async {
+  // Initialize packages
+  WidgetsFlutterBinding.ensureInitialized();
+  await EasyLocalization.ensureInitialized();
+  final Directory tmpDir = await getTemporaryDirectory();
+  Hive.init(tmpDir.toString());
+  HydratedBloc.storage = await HydratedStorage.build(
+    storageDirectory: tmpDir,
+  );
 
-void main() => runApp(const Hangman());
+  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(),
+          )));
+}
 
-class Hangman extends StatelessWidget {
-  const Hangman({super.key});
+class MyApp extends StatelessWidget {
+  const MyApp({super.key});
 
   @override
   Widget build(BuildContext context) {
-    return ChangeNotifierProvider(
-      create: (BuildContext context) => Data(),
-      child: Consumer<Data>(
-        builder: (context, data, child) {
+    final List<String> assets = getImagesAssets();
+    for (String asset in assets) {
+      precacheImage(AssetImage(asset), context);
+    }
+
+    return MultiBlocProvider(
+      providers: [
+        BlocProvider<NavCubit>(create: (context) => NavCubit()),
+        BlocProvider<ThemeCubit>(create: (context) => ThemeCubit()),
+        BlocProvider<GameCubit>(create: (context) => GameCubit()),
+        BlocProvider<GlobalSettingsCubit>(create: (context) => GlobalSettingsCubit()),
+        BlocProvider<GameSettingsCubit>(create: (context) => GameSettingsCubit()),
+      ],
+      child: BlocBuilder<ThemeCubit, ThemeModeState>(
+        builder: (BuildContext context, ThemeModeState state) {
           return MaterialApp(
+            title: 'Hangman',
+            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,
-            theme: ThemeData(
-              primaryColor: const Color(darkGreen),
-              visualDensity: VisualDensity.adaptivePlatformDensity,
-            ),
-            home: const Home(),
-            routes: {
-              Home.id: (context) => const Home(),
-              Game.id: (context) => const Game(),
-              Scores.id: (context) => const Scores(),
-            },
           );
         },
       ),
     );
   }
+
+  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');
+    }
+
+    final List<String> skinImages = [];
+    for (int value = 1; value <= 9; value++) {
+      skinImages.add('img$value');
+    }
+
+    for (String skin in DefaultGlobalSettings.allowedSkinValues) {
+      for (String image in skinImages) {
+        assets.add('assets/skins/${skin}_$image.png');
+      }
+    }
+
+    return assets;
+  }
 }
diff --git a/lib/models/game/game.dart b/lib/models/game/game.dart
new file mode 100644
index 0000000000000000000000000000000000000000..bff643d2f87b809edc6dd160a47bb9d4ce8a5b6e
--- /dev/null
+++ b/lib/models/game/game.dart
@@ -0,0 +1,150 @@
+import 'package:hangman/models/settings/settings_game.dart';
+import 'package:hangman/models/settings/settings_global.dart';
+import 'package:hangman/utils/tools.dart';
+
+typedef MovingTile = String;
+typedef Player = String;
+typedef ConflictsCount = List<List<int>>;
+typedef AnimatedBoard = List<List<bool>>;
+typedef AnimatedBoardSequence = List<AnimatedBoard>;
+typedef Word = String;
+
+class Game {
+  Game({
+    // Settings
+    required this.gameSettings,
+    required this.globalSettings,
+
+    // State
+    this.isRunning = false,
+    this.isStarted = false,
+    this.isFinished = false,
+    this.animationInProgress = false,
+    this.gettingSecretWord = false,
+
+    // Base data
+    required this.secretWord,
+    required this.clue,
+
+    // Game data
+    required this.hiddenWord,
+    required this.usedLetters,
+    this.errorsCount = 0,
+    this.isClueDisplayed = false,
+  });
+
+  // Settings
+  final GameSettings gameSettings;
+  final GlobalSettings globalSettings;
+
+  // State
+  bool isRunning;
+  bool isStarted;
+  bool isFinished;
+  bool animationInProgress;
+  bool gettingSecretWord;
+
+  // Base data
+  String secretWord;
+  String clue;
+
+  // Game data
+  List<String> hiddenWord = [];
+  List<String> usedLetters = [];
+  int errorsCount;
+  bool isClueDisplayed;
+
+  factory Game.createNull() {
+    return Game(
+      // Settings
+      gameSettings: GameSettings.createDefault(),
+      globalSettings: GlobalSettings.createDefault(),
+      // Base data
+      secretWord: '',
+      clue: '',
+      // Game data
+      hiddenWord: [],
+      usedLetters: [],
+    );
+  }
+
+  factory Game.createNew({
+    required GameSettings? gameSettings,
+    required GlobalSettings? globalSettings,
+  }) {
+    final GameSettings newGameSettings = gameSettings ?? GameSettings.createDefault();
+    final GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault();
+
+    return Game(
+      // Settings
+      gameSettings: newGameSettings,
+      globalSettings: newGlobalSettings,
+      // State
+      isRunning: true,
+      gettingSecretWord: true,
+      // Base data
+      secretWord: '',
+      clue: '',
+      // Game data
+      hiddenWord: [],
+      usedLetters: [],
+    );
+  }
+
+  String get hiddenWordAsString => hiddenWord.join();
+
+  bool get canBeResumed => isStarted && !isFinished;
+  bool get gameWon => isFinished && (hiddenWordAsString == secretWord);
+
+  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('    gettingSecretWord: $gettingSecretWord');
+    printlog('  Base data');
+    printlog('    secretWord: $secretWord');
+    printlog('    clue: $clue');
+    printlog('  Game data');
+    printlog('    hiddenWord: $hiddenWord');
+    printlog('    usedLetters: $usedLetters');
+    printlog('    errorsCount: $errorsCount');
+    printlog('    isClueDisplayed: $isClueDisplayed');
+    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,
+      'gettingSecretWord': gettingSecretWord,
+      // Base data
+      'secretWord': secretWord,
+      'clue': clue,
+      // Game data
+      'hiddenWord': hiddenWord,
+      'usedLetters': usedLetters,
+      'errorsCount': errorsCount,
+      'isClueDisplayed': isClueDisplayed,
+    };
+  }
+}
diff --git a/lib/models/game/picked_word.dart b/lib/models/game/picked_word.dart
new file mode 100644
index 0000000000000000000000000000000000000000..890e2ae58cba347555e3d2cf9e0e4bd8c6321428
--- /dev/null
+++ b/lib/models/game/picked_word.dart
@@ -0,0 +1,35 @@
+import 'package:hangman/utils/tools.dart';
+
+class PickedWord {
+  const PickedWord({
+    required this.word,
+    required this.clue,
+  });
+
+  final String word;
+  final String clue;
+
+  static const PickedWord none = PickedWord(
+    word: '',
+    clue: '',
+  );
+
+  void dump() {
+    printlog('$PickedWord:');
+    printlog('  word: $word');
+    printlog('  clue: $clue');
+    printlog('');
+  }
+
+  @override
+  String toString() {
+    return '$PickedWord(${toJson()})';
+  }
+
+  Map<String, dynamic>? toJson() {
+    return <String, dynamic>{
+      'word': word,
+      'clue': clue,
+    };
+  }
+}
diff --git a/lib/models/settings/settings_game.dart b/lib/models/settings/settings_game.dart
new file mode 100644
index 0000000000000000000000000000000000000000..4c0f947021504509c75a39b8f5487a45982cce46
--- /dev/null
+++ b/lib/models/settings/settings_game.dart
@@ -0,0 +1,54 @@
+import 'package:hangman/config/default_game_settings.dart';
+import 'package:hangman/utils/tools.dart';
+
+class GameSettings {
+  final String gameMode;
+  final String gameLevel;
+
+  GameSettings({
+    required this.gameMode,
+    required this.gameLevel,
+  });
+
+  static String getGameModeValueFromUnsafe(String gameMode) {
+    if (DefaultGameSettings.allowedGameModeValues.contains(gameMode)) {
+      return gameMode;
+    }
+
+    return DefaultGameSettings.defaultGameModeValue;
+  }
+
+  static String getGameLevelValueFromUnsafe(String gameLevel) {
+    if (DefaultGameSettings.allowedGameLevelValues.contains(gameLevel)) {
+      return gameLevel;
+    }
+
+    return DefaultGameSettings.defaultGameLevelValue;
+  }
+
+  factory GameSettings.createDefault() {
+    return GameSettings(
+      gameMode: DefaultGameSettings.defaultGameModeValue,
+      gameLevel: DefaultGameSettings.defaultGameLevelValue,
+    );
+  }
+
+  void dump() {
+    printlog('$GameSettings:');
+    printlog('  ${DefaultGameSettings.parameterCodeGameMode}: $gameMode');
+    printlog('  ${DefaultGameSettings.parameterCodeGameLevel}: $gameLevel');
+    printlog('');
+  }
+
+  @override
+  String toString() {
+    return '$GameSettings(${toJson()})';
+  }
+
+  Map<String, dynamic>? toJson() {
+    return <String, dynamic>{
+      DefaultGameSettings.parameterCodeGameMode: gameMode,
+      DefaultGameSettings.parameterCodeGameLevel: gameLevel,
+    };
+  }
+}
diff --git a/lib/models/settings/settings_global.dart b/lib/models/settings/settings_global.dart
new file mode 100644
index 0000000000000000000000000000000000000000..d9c3326f4bbb863be20bd606167a4dd0d7f73335
--- /dev/null
+++ b/lib/models/settings/settings_global.dart
@@ -0,0 +1,41 @@
+import 'package:hangman/config/default_global_settings.dart';
+import 'package:hangman/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/provider/data.dart b/lib/provider/data.dart
deleted file mode 100644
index 1bbdf069f604961f1cdf861ee15f980afd372866..0000000000000000000000000000000000000000
--- a/lib/provider/data.dart
+++ /dev/null
@@ -1,163 +0,0 @@
-import 'package:flutter/foundation.dart';
-
-import 'package:hangman/utils/constants.dart';
-import 'package:hangman/utils/shared_prefs.dart';
-
-class Data extends ChangeNotifier {
-  // settings
-  final SharedPrefs _sharedPrefs = SharedPrefs();
-
-  // screen settings
-  bool _gameModeValue = false;
-  String _levelValue = defaultLevel;
-
-  // randomization
-  String _secretWord = '';
-  bool _searching = false;
-  String _clue = '';
-  List<String> _hiddenWord = [];
-  List<String> _usedLetters = [];
-
-  bool get searching => _searching;
-
-  set searching(bool value) {
-    _searching = value;
-    notifyListeners();
-  }
-
-  // scores
-  int _errors = 0;
-  int _victoryCount = 0;
-  int _defeatCount = 0;
-
-  Data() {
-    _getPrefs();
-  }
-
-  void _getPrefs() async {
-    await _sharedPrefs.init();
-
-    _gameModeValue = onlineGameMode.keys.firstWhere(
-        (k) => onlineGameMode[k]?.contains(_sharedPrefs.level) ?? false,
-        orElse: () => false);
-
-    _levelValue = onlineGameMode[_gameModeValue]?.contains(_sharedPrefs.level) ?? false
-        ? _sharedPrefs.level
-        : onlineGameMode[_gameModeValue]?.first ?? '';
-
-    _victoryCount = _sharedPrefs.victoryCount;
-    _defeatCount = _sharedPrefs.defeatCount;
-    notifyListeners();
-  }
-
-  bool get gameModePref => _sharedPrefs.gameMode;
-  String get levelPref => (_sharedPrefs.level != '')
-      ? _sharedPrefs.level
-      : onlineGameMode[gameModePref]?.first ?? '';
-
-  void resetValues() => _getPrefs();
-
-  set setPrefGameMode(bool prefGameMode) {
-    _sharedPrefs.gameMode = prefGameMode;
-    notifyListeners();
-  }
-
-  set setPrefLevel(String prefLevel) {
-    _sharedPrefs.level = prefLevel;
-    notifyListeners();
-  }
-
-  bool get gameModeValue => _gameModeValue;
-
-  set updateGameMode(bool value) {
-    _gameModeValue = value;
-    setPrefGameMode = gameModeValue;
-    updateLevel = onlineGameMode[value]?.first ?? '';
-    notifyListeners();
-  }
-
-  String get levelValue => _levelValue;
-
-  set updateLevel(String value) {
-    _levelValue = value;
-    setPrefLevel = levelValue;
-    notifyListeners();
-  }
-
-  String get secretWord => _secretWord;
-
-  set updateSecretWord(String value) {
-    _secretWord = value;
-    _hiddenWord = List<String>.generate(value.length, (i) => '_');
-    notifyListeners();
-  }
-
-  String get clue => _clue;
-
-  set updateClue(String value) {
-    _clue = value;
-    notifyListeners();
-  }
-
-  String get hiddenWord => _hiddenWord.join();
-
-  void updateHiddenWord(int index, String letter) {
-    _hiddenWord[index] = letter;
-    notifyListeners();
-  }
-
-  List<String> get usedLetters => _usedLetters;
-
-  void updateUsedLetters(String key) {
-    _usedLetters.add(key);
-    notifyListeners();
-  }
-
-  void resetUsedLetters() {
-    _usedLetters.clear();
-    notifyListeners();
-  }
-
-  int get errors => _errors;
-
-  void addError() {
-    _errors++;
-    notifyListeners();
-  }
-
-  void resetSuccessAndErrors() {
-    _errors = 0;
-    notifyListeners();
-  }
-
-  int get victoryCount => _victoryCount;
-  int get defeatCount => _defeatCount;
-
-  void addVictory() {
-    _victoryCount++;
-    _sharedPrefs.victoryCount = _victoryCount;
-    notifyListeners();
-  }
-
-  void addDefeat() {
-    _defeatCount++;
-    _sharedPrefs.defeatCount = _defeatCount;
-    notifyListeners();
-  }
-
-  void resetScores() {
-    _victoryCount = 0;
-    _defeatCount = 0;
-    notifyListeners();
-  }
-
-  void resetGame() {
-    _errors = 0;
-    _secretWord = '';
-    _clue = '';
-    _hiddenWord = [];
-    _usedLetters = [];
-    _usedLetters.clear();
-    notifyListeners();
-  }
-}
diff --git a/lib/screens/game.dart b/lib/screens/game.dart
deleted file mode 100644
index 65f2f42877086a726a9e663261cda9d88f2a2404..0000000000000000000000000000000000000000
--- a/lib/screens/game.dart
+++ /dev/null
@@ -1,150 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-
-import 'package:hangman/provider/data.dart';
-import 'package:hangman/utils/constants.dart';
-import 'package:hangman/utils/random_pick.dart';
-import 'package:hangman/widgets/letters.dart';
-
-class Game extends StatelessWidget {
-  const Game({super.key});
-
-  static const String id = 'game';
-
-  Future<void> pickWord(BuildContext context, Data myProvider) async {
-    myProvider.searching = true;
-    RandomPick randompick;
-    int attempts = 0;
-    do {
-      randompick = RandomPick(myProvider.levelPref);
-      await randompick.init();
-      if (randompick.word != '') {
-        myProvider.updateSecretWord = randompick.word;
-        myProvider.resetSuccessAndErrors();
-        myProvider.resetUsedLetters();
-        if (myProvider.levelPref == defaultLevel) {
-          myProvider.updateClue = randompick.clue;
-        }
-        myProvider.searching = false;
-        break;
-      }
-      attempts++;
-    } while (attempts < 3);
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    Orientation orientation = MediaQuery.of(context).orientation;
-    Data myProvider = Provider.of<Data>(context);
-
-    return Scaffold(
-      backgroundColor: const Color(board),
-      floatingActionButton: myProvider.levelPref == defaultLevel
-          ? FloatingActionButton(
-              foregroundColor: Colors.white,
-              backgroundColor: Colors.transparent,
-              elevation: 0.0,
-              onPressed: () {
-                showDialog(
-                  context: context,
-                  builder: (context) {
-                    return AlertDialog(
-                      title: const Text('Indice'),
-                      content: Text(myProvider.clue),
-                      actions: <Widget>[
-                        TextButton(
-                          child: const Text('Revenir au jeu'),
-                          onPressed: () => Navigator.of(context).pop(),
-                        )
-                      ],
-                    );
-                  },
-                );
-              },
-              child: const Icon(Icons.help_outline),
-            )
-          : null,
-      floatingActionButtonLocation: orientation == Orientation.portrait
-          ? FloatingActionButtonLocation.endTop
-          : FloatingActionButtonLocation.centerTop,
-      body: orientation == Orientation.portrait
-          ? const Column(
-              mainAxisAlignment: MainAxisAlignment.spaceBetween,
-              children: [
-                Flexible(
-                  child: Padding(
-                    padding: EdgeInsets.only(top: 60.0, left: 30.0, right: 30.0, bottom: 10.0),
-                    child: ImgGallow(),
-                  ),
-                ),
-                Padding(
-                  padding: EdgeInsets.symmetric(horizontal: 30.0),
-                  child: HiddenWord(),
-                ),
-                LetterButtons(),
-              ],
-            )
-          : const Row(
-              children: [
-                Expanded(
-                  flex: 2,
-                  child: Column(
-                    children: [
-                      Expanded(
-                        child: Padding(
-                          padding: EdgeInsets.only(top: 40.0),
-                          child: ImgGallow(),
-                        ),
-                      ),
-                      Padding(
-                        padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
-                        child: HiddenWord(),
-                      ),
-                    ],
-                  ),
-                ),
-                Expanded(child: LetterButtons()),
-              ],
-            ),
-    );
-  }
-}
-
-class ImgGallow extends StatelessWidget {
-  const ImgGallow({super.key});
-  @override
-  Widget build(BuildContext context) {
-    Data myProvider = Provider.of<Data>(context);
-    return Stack(
-      children: [
-        for (int error = 0; error < 9; error++)
-          AnimatedOpacity(
-            opacity: myProvider.errors >= error ? 1.0 : 0.0,
-            duration: Duration(milliseconds: myProvider.errors >= error ? 800 : 0),
-            child: Image.asset('assets/images/img${error + 1}.png'),
-          )
-      ],
-    );
-  }
-}
-
-class HiddenWord extends StatelessWidget {
-  const HiddenWord({super.key});
-  @override
-  Widget build(BuildContext context) {
-    Data myProvider = Provider.of<Data>(context);
-    return FittedBox(
-      child: Text(
-        (myProvider.hiddenWord.isEmpty || myProvider.hiddenWord == '')
-            ? 'UNEXPECTED ERROR'
-            : myProvider.hiddenWord,
-        style: const TextStyle(
-          fontFamily: 'Tiza',
-          color: Colors.white,
-          fontSize: 34.0,
-          letterSpacing: 10.0,
-        ),
-      ),
-    );
-  }
-}
diff --git a/lib/screens/home.dart b/lib/screens/home.dart
deleted file mode 100644
index cde5145e16263914003cb4307cb813a20fd45d48..0000000000000000000000000000000000000000
--- a/lib/screens/home.dart
+++ /dev/null
@@ -1,182 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-
-import 'package:hangman/provider/data.dart';
-import 'package:hangman/screens/game.dart';
-import 'package:hangman/utils/constants.dart';
-import 'package:hangman/widgets/dialog_fetch_error.dart';
-import 'package:hangman/widgets/my_app_bar.dart';
-
-class Home extends StatelessWidget {
-  const Home({super.key});
-
-  static const String id = 'home';
-
-  @override
-  Widget build(BuildContext context) {
-    final Data myProvider = Provider.of<Data>(context);
-
-    void errorWord(context) {
-      showDialog(
-        context: context,
-        builder: (_) => AlertDialog(
-          title: const Text('Erreur inattendue'),
-          content: const Text('Erreur inattendue à la récupération d\'un mot aléatoire.\n'
-              'Installer une nouvelle version de l\'application pourrait corriger cette anomalie.'),
-          actions: <Widget>[
-            TextButton(
-              child: const Text('Fermer'),
-              onPressed: () => Navigator.of(context).pop(),
-            )
-          ],
-        ),
-      );
-    }
-
-    return Scaffold(
-      appBar: MyAppBar(appBar: AppBar()),
-      body: Builder(
-        builder: (context) => Center(
-          child: myProvider.searching == true
-              ? PopScope(
-                  onPopInvoked: (didPop) {},
-                  child: const Center(
-                    child: CircularProgressIndicator(),
-                  ),
-                )
-              : SingleChildScrollView(
-                  padding: const EdgeInsets.all(15.0),
-                  child: SizedBox(
-                    height: MediaQuery.of(context).size.height / 1.25,
-                    child: Column(
-                      mainAxisAlignment: MainAxisAlignment.spaceAround,
-                      children: [
-                        Padding(
-                          padding:
-                              const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0),
-                          child: FittedBox(
-                            fit: BoxFit.fitWidth,
-                            child: Text(
-                              'LE PENDU',
-                              style: TextStyle(
-                                fontFamily: 'Tiza',
-                                fontSize: 28.0,
-                                color: Colors.grey[700],
-                              ),
-                            ),
-                          ),
-                        ),
-                        Column(
-                          children: [
-                            Row(
-                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                              children: [
-                                const Text('Mode en ligne'),
-                                Switch(
-                                  value: myProvider.gameModeValue,
-                                  onChanged: (bool value) => myProvider.updateGameMode = value,
-                                  activeTrackColor: const Color(board),
-                                  activeColor: Colors.greenAccent[400],
-                                ),
-                              ],
-                            ),
-                            Row(
-                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                              children: [
-                                const Text('Niveau'),
-                                DropdownButton<String>(
-                                  value: myProvider.levelValue != ''
-                                      ? myProvider.levelValue
-                                      : onlineGameMode[myProvider.gameModeValue]?.first,
-                                  items: onlineGameMode[myProvider.gameModeValue]
-                                      ?.map<DropdownMenuItem<String>>((String value) {
-                                    return DropdownMenuItem<String>(
-                                      value: value,
-                                      child: Text(value),
-                                    );
-                                  }).toList(),
-                                  onChanged: (String? value) =>
-                                      myProvider.updateLevel = value ?? '',
-                                ),
-                              ],
-                            ),
-                          ],
-                        ),
-                        TextButton.icon(
-                          style: TextButton.styleFrom(
-                            foregroundColor: Colors.white,
-                            backgroundColor: const Color(board),
-                            padding: const EdgeInsets.all(20.0),
-                          ),
-                          onPressed: () async {
-                            myProvider.resetGame();
-                            myProvider.searching = true;
-                            bool control = true;
-                            await const Game().pickWord(context, myProvider);
-                            if (myProvider.secretWord == '' || myProvider.hiddenWord == '') {
-                              control = false;
-                              if (!context.mounted) return;
-                              var response = await Navigator.push(
-                                context,
-                                MaterialPageRoute(
-                                  builder: (context) => const DialogFetchError(),
-                                ),
-                              );
-                              if (response == false) {
-                                myProvider.searching = false;
-                                myProvider.resetGame();
-                              } else {
-                                myProvider.setPrefGameMode = false;
-                                myProvider.setPrefLevel = defaultLevel;
-                                if (!context.mounted) return;
-                                await const Game().pickWord(context, myProvider);
-                                control = true;
-                              }
-                            }
-
-                            if (myProvider.secretWord == 'UNEXPECTED ERROR') {
-                              control = false;
-                              myProvider.resetGame();
-                              if (!context.mounted) return;
-                              errorWord(context);
-                            }
-
-                            if (control) {
-                              if (!context.mounted) return;
-                              Navigator.pushNamed(context, Game.id)
-                                  .then((value) => myProvider.searching = false);
-                            }
-                          },
-                          icon: const Icon(
-                            Icons.check_box,
-                            color: Colors.white,
-                            size: 60.0,
-                          ),
-                          label: Column(
-                            children: [
-                              const Text(
-                                'JOUER',
-                                style: TextStyle(
-                                  fontSize: 22.0,
-                                  letterSpacing: 2.0,
-                                ),
-                              ),
-                              Text(
-                                'Mode de jeu: ${myProvider.levelPref}',
-                                style: const TextStyle(
-                                  fontSize: 10.0,
-                                  fontWeight: FontWeight.w300,
-                                ),
-                              ),
-                            ],
-                          ),
-                        ),
-                      ],
-                    ),
-                  ),
-                ),
-        ),
-      ),
-    );
-  }
-}
diff --git a/lib/screens/scores.dart b/lib/screens/scores.dart
deleted file mode 100644
index 2a26025537843ac522bb08fbc6a176ab2e029737..0000000000000000000000000000000000000000
--- a/lib/screens/scores.dart
+++ /dev/null
@@ -1,124 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-
-import 'package:hangman/provider/data.dart';
-import 'package:hangman/utils/constants.dart';
-
-class Scores extends StatelessWidget {
-  const Scores({super.key});
-
-  static const String id = 'scores';
-
-  @override
-  Widget build(BuildContext context) {
-    final Data myProvider = Provider.of<Data>(context);
-
-    return Scaffold(
-      backgroundColor: const Color(board),
-      appBar: AppBar(
-        title: const Text('Scores'),
-        actions: [
-          IconButton(
-            icon: const Icon(Icons.delete_forever),
-            onPressed: () => myProvider.resetScores(),
-          ),
-        ],
-      ),
-      body: SafeArea(
-        child: Column(
-          mainAxisAlignment: MainAxisAlignment.start,
-          children: [
-            const Flexible(
-              child: FractionallySizedBox(heightFactor: 0.6),
-            ),
-            Padding(
-              padding: const EdgeInsets.symmetric(horizontal: 10.0),
-              child: FittedBox(
-                fit: BoxFit.fitWidth,
-                child: Text(
-                  'SCORES',
-                  style: TextStyle(
-                    fontFamily: 'Tiza',
-                    fontSize: 28.0,
-                    color: Colors.grey[300],
-                  ),
-                ),
-              ),
-            ),
-            const Flexible(child: FractionallySizedBox(heightFactor: 0.2)),
-            Row(
-              mainAxisAlignment: MainAxisAlignment.center,
-              children: [
-                CardBox(
-                  Icon(
-                    Icons.emoji_events,
-                    color: Colors.grey[200],
-                    size: 60.0,
-                  ),
-                  '${myProvider.victoryCount}',
-                ),
-                CardBox(
-                  ImageIcon(
-                    const AssetImage('assets/images/gameover.png'),
-                    color: Colors.grey[200],
-                    size: 60.0,
-                  ),
-                  '${myProvider.defeatCount}',
-                ),
-              ],
-            ),
-          ],
-        ),
-      ),
-    );
-  }
-}
-
-class CardBox extends StatelessWidget {
-  final Widget imagen;
-  final String text;
-
-  const CardBox(this.imagen, this.text, {super.key});
-
-  @override
-  Widget build(BuildContext context) {
-    return Flexible(
-      child: Card(
-        elevation: 10.0,
-        margin: const EdgeInsets.symmetric(horizontal: 10.0),
-        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
-        color: const Color(darkGreen),
-        child: Column(
-          children: [
-            Container(
-              padding: const EdgeInsets.all(10.0),
-              decoration: const BoxDecoration(
-                border: Border(
-                  bottom: BorderSide(
-                    color: Colors.grey,
-                    width: 0.5,
-                  ),
-                ),
-              ),
-              child: Padding(
-                padding: const EdgeInsets.all(10.0),
-                child: imagen,
-              ),
-            ),
-            Container(
-              padding: const EdgeInsets.all(20.0),
-              child: Text(
-                text,
-                style: TextStyle(
-                  fontFamily: 'Tiza',
-                  fontSize: 20.0,
-                  color: Colors.grey[300],
-                ),
-              ),
-            ),
-          ],
-        ),
-      ),
-    );
-  }
-}
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/outlined_text_widget.dart b/lib/ui/helpers/outlined_text_widget.dart
new file mode 100644
index 0000000000000000000000000000000000000000..6d8c7f77e10191464866ebaac4b5ed3a856f0731
--- /dev/null
+++ b/lib/ui/helpers/outlined_text_widget.dart
@@ -0,0 +1,51 @@
+import 'package:flutter/material.dart';
+
+import 'package:hangman/utils/color_extensions.dart';
+
+class OutlinedText extends StatelessWidget {
+  const OutlinedText({
+    super.key,
+    required this.text,
+    required this.fontSize,
+    required this.textColor,
+    this.outlineColor,
+  });
+
+  final String text;
+  final double fontSize;
+  final Color textColor;
+  final Color? outlineColor;
+
+  @override
+  Widget build(BuildContext context) {
+    final double delta = fontSize / 30;
+
+    return Text(
+      text,
+      style: TextStyle(
+        inherit: true,
+        fontSize: fontSize,
+        fontWeight: FontWeight.w600,
+        color: textColor,
+        shadows: [
+          Shadow(
+            offset: Offset(-delta, -delta),
+            color: outlineColor ?? textColor.darken(),
+          ),
+          Shadow(
+            offset: Offset(delta, -delta),
+            color: outlineColor ?? textColor.darken(),
+          ),
+          Shadow(
+            offset: Offset(delta, delta),
+            color: outlineColor ?? textColor.darken(),
+          ),
+          Shadow(
+            offset: Offset(-delta, delta),
+            color: outlineColor ?? textColor.darken(),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/lib/ui/layouts/game_layout.dart b/lib/ui/layouts/game_layout.dart
new file mode 100644
index 0000000000000000000000000000000000000000..0cb2223e8fef4363513c721a81f6e2bce3b6dbe4
--- /dev/null
+++ b/lib/ui/layouts/game_layout.dart
@@ -0,0 +1,70 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:hangman/config/game_colors.dart';
+
+import 'package:hangman/cubit/game_cubit.dart';
+import 'package:hangman/models/game/game.dart';
+import 'package:hangman/ui/widgets/game/game_answer.dart';
+import 'package:hangman/ui/widgets/game/game_clue.dart';
+import 'package:hangman/ui/widgets/game/game_hidden_word.dart';
+import 'package:hangman/ui/widgets/game/game_loading.dart';
+import 'package:hangman/ui/widgets/game/game_something_went_wrong.dart';
+import 'package:hangman/ui/widgets/game/game_virtual_keyboard.dart';
+import 'package:hangman/ui/widgets/game/game_end.dart';
+import 'package:hangman/ui/widgets/game/game_top.dart';
+
+class GameLayout extends StatelessWidget {
+  const GameLayout({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameCubit, GameState>(
+      builder: (BuildContext context, GameState gameState) {
+        final Game currentGame = gameState.currentGame;
+
+        if (currentGame.gettingSecretWord) {
+          return const GameLoadingIndicatorWidget();
+        }
+
+        if (currentGame.secretWord == '') {
+          return const GameSomethingWentWrongWidget();
+        }
+
+        return Container(
+          alignment: AlignmentDirectional.topCenter,
+          padding: const EdgeInsets.all(4),
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              Container(
+                color: GameColors.boardColor,
+                alignment: AlignmentDirectional.topCenter,
+                padding: const EdgeInsets.all(12),
+                child: const Column(
+                  mainAxisAlignment: MainAxisAlignment.start,
+                  crossAxisAlignment: CrossAxisAlignment.center,
+                  children: [
+                    SizedBox(height: 24),
+                    GameTopWidget(),
+                    SizedBox(height: 24),
+                    GameHiddenWordWidget(),
+                    SizedBox(height: 24),
+                  ],
+                ),
+              ),
+              currentGame.clue != '' ? const GameDisplayClueWidget() : const SizedBox.shrink(),
+              currentGame.isFinished && !currentGame.gameWon
+                  ? const GameDisplayAnswerWidget()
+                  : const SizedBox.shrink(),
+              const Expanded(child: SizedBox.shrink()),
+              currentGame.isFinished
+                  ? const GameEndWidget()
+                  : const GameVirtualKeyboardWidget(),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/layouts/parameters_layout.dart b/lib/ui/layouts/parameters_layout.dart
new file mode 100644
index 0000000000000000000000000000000000000000..7022e8339ac09d105ce5dc546e0de0a423fef15c
--- /dev/null
+++ b/lib/ui/layouts/parameters_layout.dart
@@ -0,0 +1,154 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:hangman/config/default_game_settings.dart';
+import 'package:hangman/config/default_global_settings.dart';
+import 'package:hangman/cubit/settings_game_cubit.dart';
+import 'package:hangman/cubit/settings_global_cubit.dart';
+import 'package:hangman/ui/parameters/parameter_painter.dart';
+import 'package:hangman/ui/widgets/actions/button_delete_saved_game.dart';
+import 'package:hangman/ui/widgets/actions/button_game_start_new.dart';
+import 'package:hangman/ui/widgets/actions/button_resume_saved_game.dart';
+import 'package:hangman/ui/parameters/parameter_image.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(SizedBox(height: separatorHeight));
+
+    if (canResume == false) {
+      // Start new game
+      lines.add(const Expanded(
+        child: StartNewGameButton(),
+      ));
+    } else {
+      // Resume game
+      lines.add(const Expanded(
+        child: ResumeSavedGameButton(),
+      ));
+      // Delete saved game
+      lines.add(SizedBox.square(
+        dimension: MediaQuery.of(context).size.width / 4,
+        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 isActive = (value == currentValue);
+
+              final double displayWidth = MediaQuery.of(context).size.width;
+              final double itemWidth = displayWidth / availableValues.length - 26;
+
+              final bool displayedWithAssets =
+                  DefaultGlobalSettings.displayedWithAssets.contains(code) ||
+                      DefaultGameSettings.displayedWithAssets.contains(code);
+
+              return TextButton(
+                child: Container(
+                  child: displayedWithAssets
+                      ? SizedBox.square(
+                          dimension: itemWidth,
+                          child: ParameterImage(
+                            code: code,
+                            value: value,
+                            isSelected: isActive,
+                          ),
+                        )
+                      : CustomPaint(
+                          size: Size(itemWidth, itemWidth),
+                          willChange: false,
+                          painter: ParameterPainter(
+                            code: code,
+                            value: value,
+                            isSelected: isActive,
+                            gameSettings: gameSettingsState.settings,
+                            globalSettings: globalSettingsState.settings,
+                          ),
+                          isComplex: true,
+                        ),
+                ),
+                onPressed: () {
+                  isGlobal
+                      ? globalSettingsCubit.setParameterValue(code, value)
+                      : gameSettingsCubit.setParameterValue(code, value);
+                },
+              );
+            },
+          );
+        },
+      );
+
+      parameterButtons.add(parameterButton);
+    }
+
+    return parameterButtons;
+  }
+}
diff --git a/lib/ui/parameters/parameter_image.dart b/lib/ui/parameters/parameter_image.dart
new file mode 100644
index 0000000000000000000000000000000000000000..fc4b576f85b01158b74548400d11a4d027c57fbe
--- /dev/null
+++ b/lib/ui/parameters/parameter_image.dart
@@ -0,0 +1,38 @@
+import 'package:flutter/material.dart';
+
+class ParameterImage extends StatelessWidget {
+  const ParameterImage({
+    super.key,
+    required this.code,
+    required this.value,
+    required this.isSelected,
+  });
+
+  final String code;
+  final String value;
+  final bool isSelected;
+
+  static const Color buttonBackgroundColor = Colors.white;
+  static const Color buttonBorderColorActive = Colors.blue;
+  static const Color buttonBorderColorInactive = Colors.white;
+  static const double buttonBorderWidth = 8.0;
+  static const double buttonBorderRadius = 8.0;
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      decoration: BoxDecoration(
+        color: buttonBackgroundColor,
+        borderRadius: BorderRadius.circular(buttonBorderRadius),
+        border: Border.all(
+          color: isSelected ? buttonBorderColorActive : buttonBorderColorInactive,
+          width: buttonBorderWidth,
+        ),
+      ),
+      child: Image(
+        image: AssetImage('assets/ui/${code}_$value.png'),
+        fit: BoxFit.fill,
+      ),
+    );
+  }
+}
diff --git a/lib/ui/parameters/parameter_painter.dart b/lib/ui/parameters/parameter_painter.dart
new file mode 100644
index 0000000000000000000000000000000000000000..995ac7a3f1fd63bcd42b034803a9f555f5958bed
--- /dev/null
+++ b/lib/ui/parameters/parameter_painter.dart
@@ -0,0 +1,203 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+
+import 'package:hangman/config/default_game_settings.dart';
+import 'package:hangman/models/settings/settings_game.dart';
+import 'package:hangman/models/settings/settings_global.dart';
+import 'package:hangman/utils/tools.dart';
+
+class ParameterPainter extends CustomPainter {
+  const ParameterPainter({
+    required this.code,
+    required this.value,
+    required this.isSelected,
+    required this.gameSettings,
+    required this.globalSettings,
+  });
+
+  final String code;
+  final String value;
+  final bool isSelected;
+  final GameSettings gameSettings;
+  final GlobalSettings globalSettings;
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    // force square
+    final double canvasSize = min(size.width, size.height);
+
+    const Color borderColorEnabled = Colors.blue;
+    const Color borderColorDisabled = Colors.white;
+
+    // "enabled/disabled" border
+    final paint = Paint();
+    paint.style = PaintingStyle.stroke;
+    paint.color = isSelected ? borderColorEnabled : borderColorDisabled;
+    paint.strokeJoin = StrokeJoin.round;
+    paint.strokeWidth = 10;
+    canvas.drawRect(
+        Rect.fromPoints(const Offset(0, 0), Offset(canvasSize, canvasSize)), paint);
+
+    // content
+    switch (code) {
+      case DefaultGameSettings.parameterCodeGameMode:
+        paintGameModeParameterItem(value, canvas, canvasSize);
+        break;
+      case DefaultGameSettings.parameterCodeGameLevel:
+        paintGameLevelParameterItem(value, canvas, canvasSize);
+        break;
+      default:
+        printlog('Unknown parameter: $code/$value');
+        paintUnknownParameterItem(value, canvas, canvasSize);
+    }
+  }
+
+  @override
+  bool shouldRepaint(CustomPainter oldDelegate) {
+    return false;
+  }
+
+  // "unknown" parameter -> simple block with text
+  void paintUnknownParameterItem(
+    final String value,
+    final Canvas canvas,
+    final double size,
+  ) {
+    final paint = Paint();
+    paint.strokeJoin = StrokeJoin.round;
+    paint.strokeWidth = 3;
+
+    paint.color = Colors.grey;
+    paint.style = PaintingStyle.fill;
+    canvas.drawRect(Rect.fromPoints(const Offset(0, 0), Offset(size, size)), paint);
+
+    final textSpan = TextSpan(
+      text: '?\n$value',
+      style: const TextStyle(
+        color: Colors.black,
+        fontSize: 18,
+        fontWeight: FontWeight.bold,
+      ),
+    );
+    final textPainter = TextPainter(
+      text: textSpan,
+      textDirection: TextDirection.ltr,
+      textAlign: TextAlign.center,
+    );
+    textPainter.layout();
+    textPainter.paint(
+      canvas,
+      Offset(
+        (size - textPainter.width) * 0.5,
+        (size - textPainter.height) * 0.5,
+      ),
+    );
+  }
+
+  void paintGameModeParameterItem(
+    final String value,
+    final Canvas canvas,
+    final double size,
+  ) {
+    const Color backgroundColor = Colors.grey;
+    String text = '';
+
+    switch (value) {
+      case DefaultGameSettings.gameModeValueOnline:
+        text = '🌐';
+        break;
+      case DefaultGameSettings.gameModeValueOffline:
+        text = '📱';
+        break;
+      default:
+        printlog('Wrong value for gameMode parameter value: $value');
+    }
+
+    final paint = Paint();
+    paint.strokeJoin = StrokeJoin.round;
+    paint.strokeWidth = 3 / 100 * size;
+
+    // Colored background
+    paint.color = backgroundColor;
+    paint.style = PaintingStyle.fill;
+    canvas.drawRect(Rect.fromPoints(const Offset(0, 0), Offset(size, size)), paint);
+
+    // centered text value
+    final textSpan = TextSpan(
+      text: text,
+      style: TextStyle(
+        color: Colors.black,
+        fontSize: size / 2,
+        fontWeight: FontWeight.bold,
+      ),
+    );
+    final textPainter = TextPainter(
+      text: textSpan,
+      textDirection: TextDirection.ltr,
+      textAlign: TextAlign.center,
+    );
+    textPainter.layout();
+    textPainter.paint(
+      canvas,
+      Offset(
+        (size - textPainter.width) * 0.5,
+        (size - textPainter.height) * 0.5,
+      ),
+    );
+  }
+
+  void paintGameLevelParameterItem(
+    final String value,
+    final Canvas canvas,
+    final double size,
+  ) {
+    Color backgroundColor = Colors.grey;
+    String text = '';
+
+    switch (value) {
+      case DefaultGameSettings.gameLevelValueEasy:
+        backgroundColor = Colors.green;
+        text = '🧒';
+        break;
+      case DefaultGameSettings.gameLevelValueHard:
+        backgroundColor = Colors.orange;
+        text = '🧑‍🎓';
+        break;
+      default:
+        printlog('Wrong value for gameLevel parameter value: $value');
+    }
+
+    final paint = Paint();
+    paint.strokeJoin = StrokeJoin.round;
+    paint.strokeWidth = 3 / 100 * size;
+
+    // Colored background
+    paint.color = backgroundColor;
+    paint.style = PaintingStyle.fill;
+    canvas.drawRect(Rect.fromPoints(const Offset(0, 0), Offset(size, size)), paint);
+
+    // centered text value
+    final textSpan = TextSpan(
+      text: text,
+      style: TextStyle(
+        color: Colors.black,
+        fontSize: size / 2,
+        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/screens/page_about.dart b/lib/ui/screens/page_about.dart
new file mode 100644
index 0000000000000000000000000000000000000000..f1d171f40eb516cde51a56207eec12e7c0d52d01
--- /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:hangman/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..9d0bee2f20118c5c8551d9ab1223bfa7f8ead34d
--- /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:hangman/cubit/game_cubit.dart';
+import 'package:hangman/models/game/game.dart';
+import 'package:hangman/ui/layouts/parameters_layout.dart';
+import 'package:hangman/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..fe20de3a220e09a4c9d3033367392cab797ec9b1
--- /dev/null
+++ b/lib/ui/screens/page_settings.dart
@@ -0,0 +1,26 @@
+import 'package:flutter/material.dart';
+
+import 'package:hangman/ui/helpers/app_titles.dart';
+import 'package:hangman/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..e79a6be64ff97787f6d52ccf9ae25e11006aa52f
--- /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:hangman/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..c5efb0be8249094bd49fde8e3b3a31ca3346d98e
--- /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:hangman/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..87fd74cf99a82a412190aa7d3284f00f95ecc2e5
--- /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:hangman/config/menu.dart';
+import 'package:hangman/cubit/nav_cubit.dart';
+import 'package:hangman/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..9ea5a65b330bf4ac1b51e63f126ea6464dadc364
--- /dev/null
+++ b/lib/ui/widgets/actions/button_delete_saved_game.dart
@@ -0,0 +1,21 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:hangman/cubit/game_cubit.dart';
+
+class DeleteSavedGameButton extends StatelessWidget {
+  const DeleteSavedGameButton({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return TextButton(
+      child: const Image(
+        image: AssetImage('assets/ui/button_delete_saved_game.png'),
+        fit: BoxFit.fill,
+      ),
+      onPressed: () {
+        BlocProvider.of<GameCubit>(context).deleteSavedGame();
+      },
+    );
+  }
+}
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..6b2978245dbaa01be2b74e47b876683dbf445ca5
--- /dev/null
+++ b/lib/ui/widgets/actions/button_game_quit.dart
@@ -0,0 +1,21 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:hangman/cubit/game_cubit.dart';
+
+class QuitGameButton extends StatelessWidget {
+  const QuitGameButton({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return ElevatedButton(
+      child: const Image(
+        image: AssetImage('assets/ui/button_back.png'),
+        fit: BoxFit.fill,
+      ),
+      onPressed: () {
+        BlocProvider.of<GameCubit>(context).quitGame();
+      },
+    );
+  }
+}
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..1c8af9a5ab3dcbcc29b0fa392f33d57191ace346
--- /dev/null
+++ b/lib/ui/widgets/actions/button_game_start_new.dart
@@ -0,0 +1,34 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:hangman/cubit/game_cubit.dart';
+import 'package:hangman/cubit/settings_game_cubit.dart';
+import 'package:hangman/cubit/settings_global_cubit.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 TextButton(
+              child: const Image(
+                image: AssetImage('assets/ui/button_start.png'),
+                fit: BoxFit.fill,
+              ),
+              onPressed: () {
+                BlocProvider.of<GameCubit>(context).startNewGame(
+                  gameSettings: gameSettingsState.settings,
+                  globalSettings: globalSettingsState.settings,
+                );
+              },
+            );
+          },
+        );
+      },
+    );
+  }
+}
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..2660baa90f8b3777564c116b768cf4eccee10ef8
--- /dev/null
+++ b/lib/ui/widgets/actions/button_resume_saved_game.dart
@@ -0,0 +1,21 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:hangman/cubit/game_cubit.dart';
+
+class ResumeSavedGameButton extends StatelessWidget {
+  const ResumeSavedGameButton({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return TextButton(
+      child: const Image(
+        image: AssetImage('assets/ui/button_resume_game.png'),
+        fit: BoxFit.fill,
+      ),
+      onPressed: () {
+        BlocProvider.of<GameCubit>(context).resumeSavedGame();
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/game_answer.dart b/lib/ui/widgets/game/game_answer.dart
new file mode 100644
index 0000000000000000000000000000000000000000..5d76196a6e65b9482fef9a118b7c7e27e450157a
--- /dev/null
+++ b/lib/ui/widgets/game/game_answer.dart
@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:hangman/cubit/game_cubit.dart';
+import 'package:hangman/models/game/game.dart';
+
+class GameDisplayAnswerWidget extends StatelessWidget {
+  const GameDisplayAnswerWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Center(
+      child: BlocBuilder<GameCubit, GameState>(
+        builder: (BuildContext context, GameState gameState) {
+          final Game currentGame = gameState.currentGame;
+
+          return Padding(
+            padding: const EdgeInsets.all(8),
+            child: FittedBox(
+              child: Text(
+                currentGame.secretWord,
+                style: const TextStyle(
+                  fontFamily: 'Tiza',
+                  fontSize: 36.0,
+                ),
+              ),
+            ),
+          );
+        },
+      ),
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/game_clue.dart b/lib/ui/widgets/game/game_clue.dart
new file mode 100644
index 0000000000000000000000000000000000000000..0fe839f8eb8de116592a9072348fbe74e4caf276
--- /dev/null
+++ b/lib/ui/widgets/game/game_clue.dart
@@ -0,0 +1,36 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:hangman/cubit/game_cubit.dart';
+import 'package:hangman/models/game/game.dart';
+
+class GameDisplayClueWidget extends StatelessWidget {
+  const GameDisplayClueWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Center(
+      child: BlocBuilder<GameCubit, GameState>(
+        builder: (BuildContext context, GameState gameState) {
+          final Game currentGame = gameState.currentGame;
+
+          return TextButton(
+            onPressed: () {
+              BlocProvider.of<GameCubit>(context).toggleClueDisplay();
+            },
+            child: FittedBox(
+              child: Text(
+                currentGame.isClueDisplayed ? currentGame.clue : '?',
+                style: const TextStyle(
+                  fontFamily: 'Tiza',
+                  color: Colors.black,
+                  fontSize: 24.0,
+                ),
+              ),
+            ),
+          );
+        },
+      ),
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/game_end.dart b/lib/ui/widgets/game/game_end.dart
new file mode 100644
index 0000000000000000000000000000000000000000..acf9c8209447473a381152921cafd55f32630107
--- /dev/null
+++ b/lib/ui/widgets/game/game_end.dart
@@ -0,0 +1,52 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:hangman/cubit/game_cubit.dart';
+import 'package:hangman/models/game/game.dart';
+import 'package:hangman/ui/widgets/actions/button_game_quit.dart';
+
+class GameEndWidget extends StatelessWidget {
+  const GameEndWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameCubit, GameState>(
+      builder: (BuildContext context, GameState gameState) {
+        final Game currentGame = gameState.currentGame;
+
+        final Image decorationImage = Image(
+          image: AssetImage(
+              currentGame.gameWon ? 'assets/ui/game_win.png' : 'assets/ui/game_fail.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: [
+                      currentGame.animationInProgress
+                          ? decorationImage
+                          : const QuitGameButton()
+                    ],
+                  ),
+                  Column(
+                    children: [decorationImage],
+                  ),
+                ],
+              ),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/game_hidden_word.dart b/lib/ui/widgets/game/game_hidden_word.dart
new file mode 100644
index 0000000000000000000000000000000000000000..0cd443e5b6bf9813b5c44866d296e114480238c1
--- /dev/null
+++ b/lib/ui/widgets/game/game_hidden_word.dart
@@ -0,0 +1,32 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:hangman/cubit/game_cubit.dart';
+import 'package:hangman/models/game/game.dart';
+
+class GameHiddenWordWidget extends StatelessWidget {
+  const GameHiddenWordWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Center(
+      child: BlocBuilder<GameCubit, GameState>(
+        builder: (BuildContext context, GameState gameState) {
+          final Game currentGame = gameState.currentGame;
+
+          return FittedBox(
+            child: Text(
+              currentGame.hiddenWordAsString,
+              style: const TextStyle(
+                fontFamily: 'Tiza',
+                color: Colors.white,
+                fontSize: 34.0,
+                letterSpacing: 10.0,
+              ),
+            ),
+          );
+        },
+      ),
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/game_loading.dart b/lib/ui/widgets/game/game_loading.dart
new file mode 100644
index 0000000000000000000000000000000000000000..c1f01b4f362df6aaf820d382c377e978a0d0221f
--- /dev/null
+++ b/lib/ui/widgets/game/game_loading.dart
@@ -0,0 +1,12 @@
+import 'package:flutter/material.dart';
+
+class GameLoadingIndicatorWidget extends StatelessWidget {
+  const GameLoadingIndicatorWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return const Center(
+      child: CircularProgressIndicator(),
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/game_something_went_wrong.dart b/lib/ui/widgets/game/game_something_went_wrong.dart
new file mode 100644
index 0000000000000000000000000000000000000000..407173152096569dc611412e3ad75c995295cae8
--- /dev/null
+++ b/lib/ui/widgets/game/game_something_went_wrong.dart
@@ -0,0 +1,12 @@
+import 'package:flutter/material.dart';
+
+class GameSomethingWentWrongWidget extends StatelessWidget {
+  const GameSomethingWentWrongWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return const Center(
+      child: Text("Something went wrong..."),
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/game_top.dart b/lib/ui/widgets/game/game_top.dart
new file mode 100644
index 0000000000000000000000000000000000000000..9e5bb63e1617ea148664247d1ffbb480b19fb7ef
--- /dev/null
+++ b/lib/ui/widgets/game/game_top.dart
@@ -0,0 +1,28 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:hangman/cubit/game_cubit.dart';
+import 'package:hangman/models/game/game.dart';
+
+class GameTopWidget extends StatelessWidget {
+  const GameTopWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameCubit, GameState>(
+      builder: (BuildContext context, GameState gameState) {
+        final Game currentGame = gameState.currentGame;
+        return Stack(
+          children: [
+            for (int error = 0; error < 9; error++)
+              AnimatedOpacity(
+                opacity: currentGame.errorsCount >= error ? 1.0 : 0.0,
+                duration: Duration(milliseconds: currentGame.errorsCount >= error ? 500 : 0),
+                child: Image.asset('assets/skins/default_img${error + 1}.png'),
+              )
+          ],
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/game_virtual_keyboard.dart b/lib/ui/widgets/game/game_virtual_keyboard.dart
new file mode 100644
index 0000000000000000000000000000000000000000..02c028832b9ef86ed0e468a309ce4dd665b1c29d
--- /dev/null
+++ b/lib/ui/widgets/game/game_virtual_keyboard.dart
@@ -0,0 +1,52 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:hangman/cubit/game_cubit.dart';
+import 'package:hangman/models/game/game.dart';
+import 'package:hangman/ui/widgets/game/game_virtual_keyboard_key.dart';
+
+class GameVirtualKeyboardWidget extends StatelessWidget {
+  const GameVirtualKeyboardWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameCubit, GameState>(
+      builder: (BuildContext context, GameState gameState) {
+        final Game currentGame = gameState.currentGame;
+
+        final List<List<String>> keys = [
+          ['A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
+          ['Q', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M'],
+          [' ', ' ', 'W', 'X', 'C', 'V', 'B', 'N', ' ', ' '],
+        ];
+
+        List<TableRow> tableRows = [];
+        for (List<String> row in keys) {
+          List<TableCell> tableCells = [];
+          for (String key in row) {
+            tableCells.add(TableCell(
+              child: GameVirtualKeyboardKeyWidget(
+                caption: key,
+                enabled: !currentGame.usedLetters.contains(key),
+              ),
+            ));
+          }
+          tableRows.add(TableRow(children: tableCells));
+        }
+
+        return SizedBox(
+          height: 150,
+          width: double.maxFinite,
+          child: Container(
+            margin: const EdgeInsets.symmetric(horizontal: 2),
+            padding: const EdgeInsets.all(2),
+            child: Table(
+              defaultVerticalAlignment: TableCellVerticalAlignment.middle,
+              children: tableRows,
+            ),
+          ),
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/game_virtual_keyboard_key.dart b/lib/ui/widgets/game/game_virtual_keyboard_key.dart
new file mode 100644
index 0000000000000000000000000000000000000000..1f6b3f0945f951d47126061bf8b03dcdd3d75ae8
--- /dev/null
+++ b/lib/ui/widgets/game/game_virtual_keyboard_key.dart
@@ -0,0 +1,52 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:hangman/config/game_colors.dart';
+
+import 'package:hangman/cubit/game_cubit.dart';
+
+class GameVirtualKeyboardKeyWidget extends StatelessWidget {
+  const GameVirtualKeyboardKeyWidget({
+    super.key,
+    required this.caption,
+    required this.enabled,
+  });
+
+  final String caption;
+  final bool enabled;
+
+  @override
+  Widget build(BuildContext context) {
+    final Color keyColor = enabled ? Colors.black : GameColors.keyColor;
+
+    if (caption == ' ') {
+      return const SizedBox();
+    }
+
+    return Padding(
+      padding: const EdgeInsets.all(2),
+      child: AspectRatio(
+        aspectRatio: 1,
+        child: TextButton(
+          style: TextButton.styleFrom(
+            padding: const EdgeInsets.all(0),
+            backgroundColor: GameColors.keyColor,
+          ),
+          child: Text(
+            caption,
+            style: TextStyle(
+              color: keyColor,
+              fontSize: 24.0,
+              fontWeight: FontWeight.w800,
+            ),
+            textAlign: TextAlign.center,
+          ),
+          onPressed: () {
+            if (caption != ' ') {
+              BlocProvider.of<GameCubit>(context).submitLetter(caption);
+            }
+          },
+        ),
+      ),
+    );
+  }
+}
diff --git a/lib/ui/widgets/global_app_bar.dart b/lib/ui/widgets/global_app_bar.dart
new file mode 100644
index 0000000000000000000000000000000000000000..8e56835db33475c88241a4711556b9a51c3145c3
--- /dev/null
+++ b/lib/ui/widgets/global_app_bar.dart
@@ -0,0 +1,84 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:hangman/config/menu.dart';
+import 'package:hangman/cubit/game_cubit.dart';
+import 'package:hangman/cubit/nav_cubit.dart';
+import 'package:hangman/models/game/game.dart';
+import 'package:hangman/ui/helpers/app_titles.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(TextButton(
+                child: const Image(
+                  image: AssetImage('assets/ui/button_back.png'),
+                  fit: BoxFit.fill,
+                ),
+                onPressed: () {},
+                onLongPress: () {
+                  final GameCubit gameCubit = BlocProvider.of<GameCubit>(context);
+                  gameCubit.quitGame();
+                },
+              ));
+            } 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/lib/utils/constants.dart b/lib/utils/constants.dart
deleted file mode 100644
index ef2ab77fd46920fe1e4314140213158617e6379a..0000000000000000000000000000000000000000
--- a/lib/utils/constants.dart
+++ /dev/null
@@ -1,37 +0,0 @@
-import 'package:flutter/material.dart';
-
-const String letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
-
-const Map<bool, List<String>> onlineGameMode = {
-  false: ['Thèmes', 'Expert'],
-  true: ['Junior', 'Avancé'],
-};
-
-const String defaultLevel = 'Thèmes';
-
-// Colors
-const int darkGreen = 0xff013220;
-const int board = 0xff004D00;
-const int accent = 0xffD81B60;
-
-// GameOver
-enum GameOver { victory, defeat }
-
-extension GameOverExtension on GameOver {
-  static const titles = {
-    GameOver.victory: 'Victoire !',
-    GameOver.defeat: 'Pendu !',
-  };
-
-  String get title => titles[this] ?? '?';
-
-  static const icons = {
-    GameOver.victory: Icons.military_tech,
-    GameOver.defeat: Icons.gavel,
-  };
-
-  IconData get icon => icons[this] ?? Icons.error;
-}
-
-const victory = GameOver.victory;
-const defeat = GameOver.defeat;
diff --git a/lib/utils/random_pick.dart b/lib/utils/random_pick.dart
deleted file mode 100644
index 668d5be94e8b4816f634c0ba91be6836736496ef..0000000000000000000000000000000000000000
--- a/lib/utils/random_pick.dart
+++ /dev/null
@@ -1,118 +0,0 @@
-import 'dart:async';
-import 'dart:convert';
-import 'dart:math' show Random;
-
-import 'package:diacritic/diacritic.dart';
-import 'package:flutter/services.dart';
-import 'package:html/parser.dart';
-import 'package:html/dom.dart';
-import 'package:http/http.dart' as http;
-
-import 'package:hangman/words/list_french_words.dart';
-
-class RandomPick {
-  final String level;
-  RandomPick(this.level);
-
-  String _url = '';
-  String _word = '';
-  String _clue = '';
-  final random = Random();
-
-  init() async {
-    switch (level) {
-      case 'Thèmes':
-        await wordFromLocal();
-        break;
-      case 'Avancé':
-        _url =
-            'https://www.palabrasaleatorias.com/mots-aleatoires.php?fs=1&fs2=0&Submit=Nouveau+mot';
-        await wordFromWeb(_url);
-        break;
-      case 'Junior':
-        _url =
-            'https://www.palabrasaleatorias.com/mots-aleatoires.php?fs=1&fs2=1&Submit=Nouveau+mot';
-        await wordFromWeb(_url);
-        break;
-      case 'Expert':
-        await wordFromPackage();
-        break;
-      default:
-        await wordFromLocal();
-    }
-  }
-
-  Future<void> wordFromWeb(String url) async {
-    try {
-      var response = await http.Client().get(Uri.parse(url));
-      if (response.statusCode == 200) {
-        var html = parse(utf8.decode(response.bodyBytes));
-        Element element = html.getElementsByTagName('div').last;
-        String word = _validate(element.text);
-        if (word == '') {
-          throw Exception();
-        }
-        _word = word;
-      } else {
-        throw Exception();
-      }
-    } catch (e) {
-      _word = '';
-    }
-  }
-
-  String _validate(String word) {
-    String wordToValidate = word.trim();
-    if (wordToValidate.contains(' ')) {
-      return '';
-    }
-    wordToValidate = removeDiacritics(wordToValidate);
-    wordToValidate = wordToValidate.toUpperCase();
-    wordToValidate = wordToValidate.replaceAll(RegExp('[0-9]'), '');
-    wordToValidate = wordToValidate.replaceAll(RegExp('[^A-Z]'), '');
-    if (wordToValidate.length < 3 || wordToValidate.length > 12) {
-      return '';
-    }
-    return wordToValidate;
-  }
-
-  Future _waitList() => Future(() {
-        final completer = Completer();
-        int indexRandom = random.nextInt(listFrenchWords.length);
-        completer.complete(listFrenchWords.sublist(indexRandom, indexRandom + 1).join('\n'));
-        return completer.future;
-      });
-
-  wordFromPackage() async {
-    var wordList = await _waitList();
-    var word = _validate(wordList);
-    _word = word;
-  }
-
-  Future<void> wordFromLocal() async {
-    String jsonString;
-    try {
-      jsonString = await rootBundle.loadString('assets/files/word-list-fr.json');
-      final jsonResponse = await json.decode(jsonString);
-      var wordList = jsonResponse[jsonResponse.keys.toList().join()];
-      int randomCategoryIndex = random.nextInt(wordList.length);
-      _clue = wordList[randomCategoryIndex]['clue'];
-      List<String> words = [];
-      for (var word in wordList[randomCategoryIndex]['words']) {
-        words.add(word);
-      }
-      String word = words[random.nextInt(words.length)];
-      if (word != '') {
-        _word = word;
-      } else {
-        _word = 'UNEXPECTED ERROR';
-      }
-    } catch (e) {
-      _word = 'UNEXPECTED ERROR';
-    }
-  }
-
-  String get word => _word;
-
-  String get clue => _clue;
-}
diff --git a/lib/utils/shared_prefs.dart b/lib/utils/shared_prefs.dart
deleted file mode 100644
index fff13bf5d21b6797fa5c8673d74bf837ba0c583e..0000000000000000000000000000000000000000
--- a/lib/utils/shared_prefs.dart
+++ /dev/null
@@ -1,29 +0,0 @@
-import 'package:shared_preferences/shared_preferences.dart';
-
-class SharedPrefs {
-  static SharedPreferences? _sharedPrefs;
-  static const String _prefsGameMode = 'gameMode';
-  static const String _prefsLevel = 'level';
-  static const String _prefsVictoryCount = 'victoryCount';
-  static const String _prefsDefeatCount = 'defeatCount';
-
-  init() async {
-    _sharedPrefs ??= await SharedPreferences.getInstance();
-  }
-
-  bool get gameMode => _sharedPrefs?.getBool(_prefsGameMode) ?? false;
-
-  set gameMode(bool value) => _sharedPrefs?.setBool(_prefsGameMode, value);
-
-  String get level => _sharedPrefs?.getString(_prefsLevel) ?? '';
-
-  set level(String value) => _sharedPrefs?.setString(_prefsLevel, value);
-
-  int get victoryCount => _sharedPrefs?.getInt(_prefsVictoryCount) ?? 0;
-
-  set victoryCount(int value) => _sharedPrefs?.setInt(_prefsVictoryCount, value);
-
-  int get defeatCount => _sharedPrefs?.getInt(_prefsDefeatCount) ?? 0;
-
-  set defeatCount(int value) => _sharedPrefs?.setInt(_prefsDefeatCount, value);
-}
diff --git a/lib/widgets/dialog_fetch_error.dart b/lib/widgets/dialog_fetch_error.dart
deleted file mode 100644
index 45f235d2bc4903b3471457d9ae8e575d2f3348bb..0000000000000000000000000000000000000000
--- a/lib/widgets/dialog_fetch_error.dart
+++ /dev/null
@@ -1,32 +0,0 @@
-import 'package:hangman/utils/constants.dart';
-import 'package:flutter/material.dart';
-
-class DialogFetchError extends StatelessWidget {
-  const DialogFetchError({super.key});
-
-  @override
-  Widget build(BuildContext context) {
-    return Scaffold(
-      backgroundColor: const Color(board),
-      body: PopScope(
-        onPopInvoked: (didPop) {},
-        child: AlertDialog(
-          title: const Text('Connexion impossible'),
-          content: const Text('Impossible de récupérer un mot aléatoire. '
-              'La connexion internet est peut-être inaccessible.\n'
-              'Changer vers un mode de jeu hors-ligne ?'),
-          actions: [
-            TextButton(
-              child: const Text('REVENIR'),
-              onPressed: () => Navigator.pop(context, false),
-            ),
-            TextButton(
-              child: const Text('ACCEPTER'),
-              onPressed: () => Navigator.pop(context, true),
-            ),
-          ],
-        ),
-      ),
-    );
-  }
-}
diff --git a/lib/widgets/dialog_gameover.dart b/lib/widgets/dialog_gameover.dart
deleted file mode 100644
index 96f336faa7ef4c731c4efc4e4f16ac171e51549e..0000000000000000000000000000000000000000
--- a/lib/widgets/dialog_gameover.dart
+++ /dev/null
@@ -1,63 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-
-import 'package:hangman/provider/data.dart';
-import 'package:hangman/screens/game.dart';
-import 'package:hangman/screens/home.dart';
-import 'package:hangman/utils/constants.dart';
-
-class DialogGameOver extends StatelessWidget {
-  final GameOver gameOver;
-  const DialogGameOver(this.gameOver, {super.key});
-
-  @override
-  Widget build(BuildContext context) {
-    final Data myProvider = Provider.of<Data>(context);
-
-    return Scaffold(
-      backgroundColor: Colors.transparent,
-      body: myProvider.searching == true
-          ? const Center(child: CircularProgressIndicator())
-          : PopScope(
-              onPopInvoked: (didPop) {},
-              child: AlertDialog(
-                title: Row(
-                  children: [
-                    Icon(gameOver.icon),
-                    const SizedBox(width: 10.0),
-                    Text(gameOver.title),
-                  ],
-                ),
-                content: SingleChildScrollView(
-                  child: Column(
-                    crossAxisAlignment: CrossAxisAlignment.start,
-                    mainAxisSize: MainAxisSize.min,
-                    children: [
-                      if (gameOver == GameOver.defeat)
-                        Text('Le mot secret était ${myProvider.secretWord}'),
-                      const Text('Rejouer ?'),
-                    ],
-                  ),
-                ),
-                actions: [
-                  TextButton(
-                    child: const Text('QUITTER'),
-                    onPressed: () {
-                      Navigator.pushNamed(context, Home.id);
-                    },
-                  ),
-                  TextButton(
-                    child: const Text('REJOUER'),
-                    onPressed: () async {
-                      myProvider.resetSuccessAndErrors();
-                      await const Game().pickWord(context, myProvider);
-                      if (!context.mounted) return;
-                      Navigator.of(context).pop();
-                    },
-                  ),
-                ],
-              ),
-            ),
-    );
-  }
-}
diff --git a/lib/widgets/letters.dart b/lib/widgets/letters.dart
deleted file mode 100644
index eb567e5a6d65d636b38c39d65fabe8d12068e7e5..0000000000000000000000000000000000000000
--- a/lib/widgets/letters.dart
+++ /dev/null
@@ -1,90 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-
-import 'package:hangman/provider/data.dart';
-import 'package:hangman/utils/constants.dart';
-import 'package:hangman/widgets/dialog_gameover.dart';
-
-class LetterButtons extends StatelessWidget {
-  const LetterButtons({super.key});
-
-  @override
-  Widget build(BuildContext context) {
-    final Orientation orientation = MediaQuery.of(context).orientation;
-    var size = MediaQuery.of(context).size;
-    final double paddingTop = MediaQuery.of(context).padding.top;
-    final double itemHeight = (size.height - paddingTop) / 2;
-    final double itemWidth = size.width / 2;
-
-    final Data myProvider = Provider.of<Data>(context);
-
-    final List<String> lettersList = letters.split('');
-    final List<Widget> keys = [];
-
-    for (var key in lettersList) {
-      keys.add(
-        Padding(
-          padding: const EdgeInsets.all(2.0),
-          child: TextButton(
-            style: TextButton.styleFrom(
-              backgroundColor: Colors.grey,
-              foregroundColor: Colors.white,
-              shadowColor: const Color(accent),
-            ),
-            onPressed: myProvider.usedLetters.contains(key)
-                ? null
-                : () async {
-                    myProvider.updateUsedLetters(key);
-                    if (myProvider.secretWord.contains(key)) {
-                      for (int index = 0; index < myProvider.secretWord.length; index++) {
-                        if (key == myProvider.secretWord[index]) {
-                          myProvider.updateHiddenWord(index, key);
-                        }
-                      }
-
-                      if (myProvider.hiddenWord == myProvider.secretWord) {
-                        myProvider.addVictory();
-                        showDialog(
-                          context: context,
-                          builder: (context) => const DialogGameOver(victory),
-                        );
-                      }
-                    } else {
-                      myProvider.addError();
-                      if (myProvider.errors == 8) {
-                        await Future.delayed(const Duration(milliseconds: 900)); //????
-                        myProvider.addDefeat();
-                        if (!context.mounted) return;
-                        showDialog(
-                          context: context,
-                          builder: (context) => const DialogGameOver(defeat),
-                        );
-                      }
-                    }
-                  },
-            child: Text(
-              key,
-              textAlign: TextAlign.center,
-            ),
-          ),
-        ),
-      );
-    }
-
-    return Container(
-      alignment:
-          orientation == Orientation.portrait ? Alignment.bottomCenter : Alignment.center,
-      color: const Color(darkGreen),
-      padding: const EdgeInsets.all(10.0),
-      margin: EdgeInsets.only(top: orientation == Orientation.portrait ? 0.0 : paddingTop),
-      child: GridView.count(
-        padding: EdgeInsets.zero,
-        shrinkWrap: true,
-        crossAxisCount: orientation == Orientation.portrait ? 9 : 3,
-        childAspectRatio:
-            orientation == Orientation.portrait ? 1 / 1 : (itemWidth / itemHeight),
-        children: keys,
-      ),
-    );
-  }
-}
diff --git a/lib/widgets/my_app_bar.dart b/lib/widgets/my_app_bar.dart
deleted file mode 100644
index d25c46f6a6f566551680441f58c8b801cd524c0d..0000000000000000000000000000000000000000
--- a/lib/widgets/my_app_bar.dart
+++ /dev/null
@@ -1,43 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart' show SystemNavigator;
-
-import 'package:hangman/screens/scores.dart';
-
-class MyAppBar extends StatelessWidget implements PreferredSizeWidget {
-  const MyAppBar({super.key, this.appBar});
-
-  final AppBar? appBar;
-
-  @override
-  Size get preferredSize => Size.fromHeight(appBar?.preferredSize.height ?? 0.0);
-
-  @override
-  Widget build(BuildContext context) {
-    return AppBar(
-      title: const Text('Hangman'),
-      automaticallyImplyLeading: false,
-      actions: [
-        PopupMenuButton<String>(
-          onSelected: (String value) {
-            switch (value) {
-              case 'Quitter':
-                SystemNavigator.pop();
-                break;
-              case 'Scores':
-                Navigator.pushNamed(context, Scores.id);
-                break;
-            }
-          },
-          itemBuilder: (BuildContext context) {
-            return {'Scores', 'Quitter'}.map((String choice) {
-              return PopupMenuItem<String>(
-                value: choice,
-                child: Text(choice),
-              );
-            }).toList();
-          },
-        ),
-      ],
-    );
-  }
-}
diff --git a/pubspec.lock b/pubspec.lock
index 2599bd07dcef5abeda4ed10e2309ee446443f091..bbf060c1d2f00d1a8f8bdb1b07ec681da40180ec 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: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.5.0"
   async:
     dependency: transitive
     description:
@@ -9,6 +17,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.11.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 +33,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 +49,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.18.0"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.5"
   csslib:
     dependency: transitive
     description:
@@ -41,14 +73,38 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.1.5"
+  easy_localization:
+    dependency: "direct main"
+    description:
+      name: easy_localization
+      sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.7"
+  easy_logger:
+    dependency: transitive
+    description:
+      name: easy_logger
+      sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.0.2"
+  equatable:
+    dependency: "direct main"
+    description:
+      name: equatable
+      sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.5"
   ffi:
     dependency: transitive
     description:
       name: ffi
-      sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
+      sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.2"
+    version: "2.1.3"
   file:
     dependency: transitive
     description:
@@ -62,19 +118,40 @@ 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:
       name: flutter_lints
-      sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
+      sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.1"
+    version: "4.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"
   html:
     dependency: "direct main"
     description:
@@ -87,10 +164,10 @@ packages:
     dependency: "direct main"
     description:
       name: http
-      sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
+      sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
       url: "https://pub.dev"
     source: hosted
-    version: "1.2.1"
+    version: "1.2.2"
   http_parser:
     dependency: transitive
     description:
@@ -99,30 +176,46 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.0.2"
+  hydrated_bloc:
+    dependency: "direct main"
+    description:
+      name: hydrated_bloc
+      sha256: af35b357739fe41728df10bec03aad422cdc725a1e702e03af9d2a41ea05160c
+      url: "https://pub.dev"
+    source: hosted
+    version: "9.1.5"
+  intl:
+    dependency: transitive
+    description:
+      name: intl
+      sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.19.0"
   lints:
     dependency: transitive
     description:
       name: lints
-      sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
+      sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.0"
+    version: "4.0.0"
   material_color_utilities:
     dependency: transitive
     description:
       name: material_color_utilities
-      sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
+      sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
       url: "https://pub.dev"
     source: hosted
-    version: "0.8.0"
+    version: "0.11.1"
   meta:
     dependency: transitive
     description:
       name: meta
-      sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
+      sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
       url: "https://pub.dev"
     source: hosted
-    version: "1.11.0"
+    version: "1.15.0"
   nested:
     dependency: transitive
     description:
@@ -131,6 +224,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.0.0"
+  package_info_plus:
+    dependency: "direct main"
+    description:
+      name: package_info_plus
+      sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918
+      url: "https://pub.dev"
+    source: hosted
+    version: "8.0.2"
+  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:
@@ -139,6 +248,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: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.10"
+  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:
@@ -159,18 +292,18 @@ packages:
     dependency: transitive
     description:
       name: path_provider_windows
-      sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
+      sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.1"
+    version: "2.3.0"
   platform:
     dependency: transitive
     description:
       name: platform
-      sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
+      sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
       url: "https://pub.dev"
     source: hosted
-    version: "3.1.4"
+    version: "3.1.5"
   plugin_platform_interface:
     dependency: transitive
     description:
@@ -180,7 +313,7 @@ packages:
     source: hosted
     version: "2.1.8"
   provider:
-    dependency: "direct main"
+    dependency: transitive
     description:
       name: provider
       sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
@@ -188,61 +321,61 @@ packages:
     source: hosted
     version: "6.1.2"
   shared_preferences:
-    dependency: "direct main"
+    dependency: transitive
     description:
       name: shared_preferences
-      sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
+      sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051"
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.2"
+    version: "2.3.2"
   shared_preferences_android:
     dependency: transitive
     description:
       name: shared_preferences_android
-      sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
+      sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.1"
+    version: "2.3.1"
   shared_preferences_foundation:
     dependency: transitive
     description:
       name: shared_preferences_foundation
-      sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
+      sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.5"
+    version: "2.5.2"
   shared_preferences_linux:
     dependency: transitive
     description:
       name: shared_preferences_linux
-      sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
+      sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.2"
+    version: "2.4.1"
   shared_preferences_platform_interface:
     dependency: transitive
     description:
       name: shared_preferences_platform_interface
-      sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
+      sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.2"
+    version: "2.4.1"
   shared_preferences_web:
     dependency: transitive
     description:
       name: shared_preferences_web
-      sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
+      sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.0"
+    version: "2.4.2"
   shared_preferences_windows:
     dependency: transitive
     description:
       name: shared_preferences_windows
-      sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
+      sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.2"
+    version: "2.4.1"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -260,10 +393,18 @@ packages:
     dependency: transitive
     description:
       name: string_scanner
-      sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
+      sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
+  synchronized:
+    dependency: transitive
+    description:
+      name: synchronized
+      sha256: a824e842b8a054f91a728b783c177c1e4731f6b124f9192468457a8913371255
       url: "https://pub.dev"
     source: hosted
-    version: "1.2.0"
+    version: "3.2.0"
   term_glyph:
     dependency: transitive
     description:
@@ -280,6 +421,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.3.2"
+  unicons:
+    dependency: "direct main"
+    description:
+      name: unicons
+      sha256: "1cca7462df18ff191b7e41b52f747d08854916531d1d7ab7cec0552095995206"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
   vector_math:
     dependency: transitive
     description:
@@ -292,18 +441,18 @@ packages:
     dependency: transitive
     description:
       name: web
-      sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad"
+      sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062
       url: "https://pub.dev"
     source: hosted
-    version: "0.5.0"
+    version: "1.0.0"
   win32:
     dependency: transitive
     description:
       name: win32
-      sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
+      sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a"
       url: "https://pub.dev"
     source: hosted
-    version: "5.2.0"
+    version: "5.5.4"
   xdg_directories:
     dependency: transitive
     description:
@@ -313,5 +462,5 @@ packages:
     source: hosted
     version: "1.0.4"
 sdks:
-  dart: ">=3.3.0 <4.0.0"
-  flutter: ">=3.19.0"
+  dart: ">=3.5.0 <4.0.0"
+  flutter: ">=3.22.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 30bab3294dae8c01f991a1c8c76d4d08a0fd9d9d..344a91808baf139742e0939f363f0a02e9b52a28 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,29 +1,54 @@
 name: hangman
 description: Hangman game, have fun with words and letters!
-publish_to: 'none'
-version: 1.2.16+27
+
+publish_to: "none"
+
+version: 1.2.17+28
 
 environment:
-  sdk: '^3.0.0'
+  sdk: "^3.0.0"
 
 dependencies:
   flutter:
     sdk: flutter
-  provider: ^6.0.5
-  shared_preferences: ^2.2.1
+
+  # base
+  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: ^2.1.1
+
+  # specific
+  diacritic: ^0.1.4
   html: ^0.15.4
   http: ^1.1.0
-  diacritic: ^0.1.4
 
 dev_dependencies:
-  flutter_lints: ^3.0.1
+  flutter_lints: ^4.0.0
 
 flutter:
   uses-material-design: true
   assets:
-    - assets/images/
-    - assets/files/
+    - assets/skins/
+    - assets/translations/
+    - assets/ui/
+
   fonts:
     - family: Tiza
       fonts:
         - asset: assets/fonts/tiza.ttf
+    - family: Nunito
+      fonts:
+        - asset: assets/fonts/Nunito-Bold.ttf
+          weight: 700
+        - asset: assets/fonts/Nunito-Medium.ttf
+          weight: 500
+        - asset: assets/fonts/Nunito-Regular.ttf
+          weight: 400
+        - asset: assets/fonts/Nunito-Light.ttf
+          weight: 300
+
diff --git a/icons/build_application_icons.sh b/resources/app/build_application_resources.sh
similarity index 98%
rename from icons/build_application_icons.sh
rename to resources/app/build_application_resources.sh
index 27dbe2647fe4e6d562fbd99451716d1b7d448570..6d67b8f4f9eca701d1aed7331ef41dfb0bd44f20 100755
--- a/icons/build_application_icons.sh
+++ b/resources/app/build_application_resources.sh
@@ -6,7 +6,7 @@ command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not ins
 command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; }
 
 CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
-BASE_DIR="$(dirname "${CURRENT_DIR}")"
+BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")"
 
 SOURCE_ICON="${CURRENT_DIR}/icon.svg"
 SOURCE_FASTLANE="${CURRENT_DIR}/featureGraphic.svg"
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..45d0d745ed6c0c15fdd3c876a415c0c3ba428248
--- /dev/null
+++ b/resources/build_resources.sh
@@ -0,0 +1,7 @@
+#! /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
+${CURRENT_DIR}/data/build_words_lists.sh
diff --git a/resources/data/build_words_lists.sh b/resources/data/build_words_lists.sh
new file mode 100755
index 0000000000000000000000000000000000000000..927331a3a23b0918a032f80375e4886b6d0733dd
--- /dev/null
+++ b/resources/data/build_words_lists.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+command -v jq >/dev/null 2>&1 || { echo >&2 "I require jq (json parser) 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}")")"
+
+# JSON date file for frean "easy" words
+DICTIONARY_JSON_FILE="${CURRENT_DIR}/dictionary-fr-easy.json"
+if [ ! -f "${DICTIONARY_JSON_FILE}" ]; then
+  echo "File ${DICTIONARY_JSON_FILE} not found. Abort."
+  exit 1
+fi
+# DART source code file to inject data into
+GAME_DATA_DART_FILE="${BASE_DIR}/lib/data/dictionary_french_easy.dart"
+if [ ! -f "${GAME_DATA_DART_FILE}" ]; then
+  echo "File ${GAME_DATA_DART_FILE} not found. Abort."
+  exit 1
+fi
+
+echo "Injecting json file ${DICTIONARY_JSON_FILE} in ${GAME_DATA_DART_FILE}..."
+echo "const List<dynamic> dictionaryFrenchEasy = $(cat "${DICTIONARY_JSON_FILE}");" >"${GAME_DATA_DART_FILE}"
+
+echo "Formatting dart source code file..."
+dart format "${GAME_DATA_DART_FILE}"
+
+echo "done."
diff --git a/resources/data/dictionary-fr-easy.json b/resources/data/dictionary-fr-easy.json
new file mode 100644
index 0000000000000000000000000000000000000000..c7e2eef36f433a9120637fbf17cb84edb0d15421
--- /dev/null
+++ b/resources/data/dictionary-fr-easy.json
@@ -0,0 +1,193 @@
+[
+  {
+    "category": "ANIMAUX",
+    "clue": "Animal",
+    "words": [
+      "ANACONDA",
+      "AUTRUCHE",
+      "BARRACUDA",
+      "BOUQUETIN",
+      "COCCINELLE",
+      "CROCODILE",
+      "DROMADAIRE",
+      "ELEPHANT",
+      "ESCARGOT",
+      "FOURMILIER",
+      "GRENOUILLE",
+      "HIPPOCAMPE",
+      "HIPPOPOTAME",
+      "KANGOUROU",
+      "LIBELLULE",
+      "PERROQUET",
+      "PIPISTRELLE",
+      "RHINOCEROS",
+      "SAUTERELLE",
+      "TARENTULE"
+    ]
+  },
+  {
+    "category": "FRUITS",
+    "clue": "Fruit",
+    "words": [
+      "AUBERGINE",
+      "BETTERAVE",
+      "CITROUILLE",
+      "CONCOMBRE",
+      "FRAMBOISE",
+      "GROSEILLE",
+      "MANDARINE",
+      "MIRABELLE",
+      "MYRTILLE",
+      "PAMPLEMOUSSE"
+    ]
+  },
+  {
+    "category": "METIERS",
+    "clue": "Métier",
+    "words": [
+      "AGRICULTEUR",
+      "ARCHEOLOGUE",
+      "ARCHITECTE",
+      "ASTRONAUTE",
+      "BIJOUTIER",
+      "BIOLOGISTE",
+      "CHARCUTIER",
+      "CHARPENTIER",
+      "CUISINIER",
+      "ELECTRICIEN",
+      "HORTICULTEUR",
+      "INFIRMIER",
+      "MECANICIEN",
+      "MENUISIER",
+      "METEOROLOGUE",
+      "PHOTOGRAPHE",
+      "PROFESSEUR",
+      "STANDARDISTE",
+      "VETERINAIRE",
+      "VOLCANOLOGUE"
+    ]
+  },
+  {
+    "category": "GEOGRAPHIE",
+    "clue": "Géographie",
+    "words": [
+      "ALLEMAGNE",
+      "ANTARCTIQUE",
+      "ARGENTINE",
+      "ATLANTIQUE",
+      "AUSTRALIE",
+      "EMBOUCHURE",
+      "HEMISPHERE",
+      "HYDROGRAPHIE",
+      "KILIMANDJARO",
+      "LUXEMBOURG",
+      "MADAGASCAR",
+      "MEDITERRANEE",
+      "MISSISSIPPI",
+      "NORMANDIE",
+      "PACIFIQUE",
+      "PLANISPHERE",
+      "STRASBOURG",
+      "SUPERFICIE",
+      "VENEZUELA",
+      "WASHINGTON"
+    ]
+  },
+  {
+    "category": "COULEURS",
+    "clue": "Couleur",
+    "words": [
+      "ROUGE",
+      "BLEU",
+      "VERT",
+      "JAUNE",
+      "VIOLET",
+      "ORANGE",
+      "MARRON",
+      "NOIR",
+      "BLANC",
+      "TURQUOISE",
+      "BEIGE",
+      "ROSE"
+    ]
+  },
+  {
+    "category": "FLEURS",
+    "clue": "Fleur",
+    "words": [
+      "ROSE",
+      "PIVOINE",
+      "TULIPE",
+      "JONQUILLE",
+      "CACTUS",
+      "PETUNIA",
+      "COQUELICOT",
+      "JACINTHE"
+    ]
+  },
+  {
+    "category": "SPORTS",
+    "clue": "Sport ou jeu",
+    "words": [
+      "GYMNASTIQUE",
+      "FOOTBALL",
+      "HANDBALL",
+      "COURSE",
+      "CYCLISME",
+      "RANDONNEE",
+      "NATATION",
+      "KARATE",
+      "ESCRIME",
+      "EQUITATION"
+    ]
+  },
+  {
+    "category": "ALIMENTS",
+    "clue": "Aliment ou plat",
+    "words": [
+      "FROMAGE",
+      "PIZZA",
+      "SAUCISSON",
+      "JAMBON",
+      "SALAMI",
+      "PAELLA",
+      "PATES",
+      "SALADE",
+      "SOUPE",
+      "CHOCOLAT",
+      "OEUF",
+      "CREME",
+      "LAIT",
+      "CORNICHON",
+      "FLAN",
+      "TARTE",
+      "PUREE",
+      "SAUMON",
+      "SANDWICH"
+    ]
+  },
+  {
+    "category": "VEHICULE",
+    "clue": "Véhicule ou moyen de transport",
+    "words": [
+      "VOITURE",
+      "MOTO",
+      "VELO",
+      "TRAIN",
+      "BATEAU",
+      "AVION",
+      "HELICOPTERE",
+      "AUTOBUS",
+      "CAR",
+      "TRAINEAU",
+      "FUSEE",
+      "VOILIER",
+      "PAQUEBOT",
+      "METRO",
+      "SOUS-MARIN",
+      "CAMION",
+      "TRACTEUR",
+      "KAYAK"
+    ]
+  }
+]
diff --git a/resources/ui/build_ui_resources.sh b/resources/ui/build_ui_resources.sh
new file mode 100755
index 0000000000000000000000000000000000000000..2375593637ee4aa2198bceffffa4ba668358dc05
--- /dev/null
+++ b/resources/ui/build_ui_resources.sh
@@ -0,0 +1,141 @@
+#! /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..2622a578dba53ce582afabfc587c2a85a1fb6eaa
--- /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"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#e41578" stroke="#fff" stroke-width=".238"/><path d="m59.387 71.362c1.1248 1.1302 4.0012 1.1302 4.0012 0v-45.921c0-1.1316-2.8832-1.1316-4.0121 0l-37.693 20.918c-1.1289 1.1248-1.1479 2.9551-0.02171 4.084z" fill="#fefeff" stroke="#930e4e" stroke-linecap="round" stroke-linejoin="round" stroke-width="8.257"/><path d="m57.857 68.048c0.96243 0.96706 3.4236 0.96706 3.4236 0v-39.292c0-0.96825-2.467-0.96825-3.4329 0l-32.252 17.898c-0.96594 0.96243-0.9822 2.5285-0.01858 3.4945z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.314"/></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..ac7eefef476f761903fe781b8c86d0c94323550a
--- /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"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#ee7d49" stroke="#fff" stroke-width=".238"/><path d="m61.07 35.601-1.7399 27.837c-0.13442 2.1535-1.9205 3.8312-4.0781 3.8312h-16.84c-2.1576 0-3.9437-1.6777-4.0781-3.8312l-1.7399-27.837h-2.6176c-0.84621 0-1.5323-0.68613-1.5323-1.5323 0-0.84655 0.68613-1.5323 1.5323-1.5323h33.711c0.84621 0 1.5323 0.68578 1.5323 1.5323 0 0.84621-0.68613 1.5323-1.5323 1.5323zm-3.2617 0h-21.953l1.4715 26.674c0.05985 1.0829 0.95531 1.9305 2.0403 1.9305h14.929c1.085 0 1.9804-0.84757 2.0403-1.9305zm-10.977 3.0647c0.78977 0 1.4301 0.6403 1.4301 1.4301v19.614c0 0.78977-0.6403 1.4301-1.4301 1.4301s-1.4301-0.6403-1.4301-1.4301v-19.614c0-0.78977 0.6403-1.4301 1.4301-1.4301zm-6.1293 0c0.80004 0 1.4588 0.62935 1.495 1.4286l0.89647 19.719c0.03182 0.70016-0.50998 1.2933-1.2101 1.3255-0.01915 7.02e-4 -0.03831 1e-3 -0.05781 1e-3 -0.74462 0-1.3596-0.58215-1.4003-1.3261l-1.0757-19.719c-0.0407-0.74701 0.53188-1.3852 1.2786-1.4259 0.02462-0.0014 0.04926-2e-3 0.07388-2e-3zm12.259 0c0.74804 0 1.3541 0.60609 1.3541 1.3541 0 0.02462-3.28e-4 0.04926-0.0017 0.07388l-1.0703 19.618c-0.04379 0.80106-0.70597 1.4281-1.5081 1.4281-0.74804 0-1.3541-0.60609-1.3541-1.3541 0-0.02462 3.49e-4 -0.04925 0.0017-0.07388l1.0703-19.618c0.04379-0.80106 0.70597-1.4281 1.5081-1.4281zm-10.216-12.259h8.1728c2.2567 0 4.086 1.8293 4.086 4.086v2.0433h-16.344v-2.0433c0-2.2567 1.8293-4.086 4.086-4.086zm0.20453 3.0647c-0.67725 0-1.2259 0.54863-1.2259 1.2259v1.8388h10.215v-1.8388c0-0.67725-0.54863-1.2259-1.2259-1.2259z" fill="#fff" fill-rule="evenodd" stroke="#bd4812" stroke-width=".75383"/></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..6ad8b64202d0e70f898c16c520e756fe8a934add
--- /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"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><path d="m39.211 31.236c-0.84086-0.84489-2.9911-0.84489-2.9911 0v34.329c0 0.84594 2.1554 0.84594 2.9993 0l28.178-15.637c0.84392-0.84086 0.85812-2.2091 0.01623-3.053z" fill="#fefeff" stroke="#105ca1" stroke-linecap="round" stroke-linejoin="round" stroke-width="6.1726"/><path d="m40.355 33.714c-0.71948-0.72294-2.5594-0.72294-2.5594 0v29.373c0 0.72383 1.8442 0.72383 2.5663 0l24.11-13.38c0.7221-0.71948 0.73426-1.8902 0.01389-2.6124z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.225"/><path d="m28.369 66.919v-37.591" fill="#105ca2" stroke="#105ca2" stroke-linecap="round" stroke-width="4.0337"/></svg>
diff --git a/resources/ui/images/button_start.svg b/resources/ui/images/button_start.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e9d49d2172b9a0305db82779971e3c1e12f34a70
--- /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"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><path d="m34.852 25.44c-1.1248-1.1302-4.0012-1.1302-4.0012 0v45.921c0 1.1316 2.8832 1.1316 4.0121 0l37.693-20.918c1.1289-1.1248 1.1479-2.9551 0.02171-4.084z" fill="#fefeff" stroke="#105ca1" stroke-linecap="round" stroke-linejoin="round" stroke-width="8.257"/><path d="m36.382 28.754c-0.96243-0.96706-3.4236-0.96706-3.4236 0v39.292c0 0.96825 2.467 0.96825 3.4329 0l32.252-17.898c0.96594-0.96243 0.9822-2.5285 0.01858-3.4945z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.314"/></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/resources/ui/images/game_win.svg b/resources/ui/images/game_win.svg
new file mode 100644
index 0000000000000000000000000000000000000000..fe20923864d0c5d39168eced03038b65106a596b
--- /dev/null
+++ b/resources/ui/images/game_win.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.17604 0 0 .17604 7.9341 1.7716)"><path d="m101.92 496.35c-1.8555 0-3.7109-0.69532-5.1484-2.0898-2.9297-2.8438-3-7.5234-0.15234-10.453l9.1875-9.4648c2.8438-2.9297 7.5234-3 10.453-0.15625s3 7.5234 0.15625 10.453l-9.1914 9.4648c-1.4492 1.4961-3.375 2.2461-5.3047 2.2461z" fill="#ff4e61"/><path d="m201.65 133.26c-1.8516 0-3.7109-0.69531-5.1445-2.0898-2.9297-2.8438-3-7.5234-0.15625-10.449l9.1914-9.4688c2.8438-2.9297 7.5195-3 10.449-0.15625s3 7.5234 0.15625 10.453l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#ff4e61"/><path d="m413.8 100.39c-1.8555 0-3.7109-0.69141-5.1484-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1914-9.4688c2.8438-2.9258 7.5234-2.9961 10.453-0.15234 2.9297 2.8398 3 7.5234 0.15625 10.449l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#5c73bc"/><path d="m413.8 463.77c-1.8555 0-3.7109-0.69532-5.1484-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1914-9.4688c2.8438-2.9258 7.5234-3 10.453-0.15625s3 7.5234 0.15625 10.453l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#fa0"/><path d="m63.07 112.91c-1.8516 0-3.7109-0.69141-5.1445-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1914-9.4687c2.8438-2.9258 7.5234-2.9961 10.453-0.15234 2.9258 2.8438 2.9961 7.5234 0.15234 10.449l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#fa0"/><path d="m12.309 278.82c-1.8516 0-3.7109-0.69141-5.1445-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1875-9.4688c2.8438-2.9297 7.5234-3 10.453-0.15625 2.9297 2.8438 3 7.5234 0.15625 10.453l-9.1914 9.4688c-1.4453 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#2dc471"/><path d="m216.29 278.49-23.996 12.996c-6.2226 3.3711-13.496-2.0742-12.309-9.2148l4.582-27.523c0.47266-2.8359-0.4375-5.7266-2.4375-7.7344l-19.414-19.496c-5.0352-5.0547-2.2578-13.863 4.7031-14.906l26.824-4.0156c2.7656-0.41407 5.1524-2.1992 6.3867-4.7812l12-25.043c3.1133-6.4922 12.102-6.4922 15.215 0l11.996 25.043c1.2383 2.582 3.625 4.3672 6.3867 4.7812l26.828 4.0156c6.957 1.043 9.7344 9.8516 4.6992 14.906l-19.41 19.496c-2 2.0078-2.9141 4.8984-2.4414 7.7344l4.582 27.523c1.1914 7.1406-6.082 12.586-12.305 9.2148l-23.996-12.996c-2.4727-1.3398-5.4258-1.3398-7.8945 0z" fill="#ffd02f"/><path d="m220.24 512c-4.082 0-7.3906-3.3086-7.3906-7.3906v-115.59c0-4.082 3.3086-7.3945 7.3906-7.3945s7.3906 3.3125 7.3906 7.3945v115.59c0 4.082-3.3086 7.3906-7.3906 7.3906z" fill="#5c73bc"/><path d="m220.3 357.42h-0.11328c-4.082 0-7.3945-3.3125-7.3945-7.3945s3.3086-7.3906 7.3945-7.3906h0.11328c4.082 0 7.3906 3.3086 7.3906 7.3906s-3.3086 7.3945-7.3906 7.3945z" fill="#5c73bc"/><path d="m220.3 332h-0.14838c-4.082-0.0156-7.375-3.3398-7.3594-7.4219 0.0195-4.0742 3.3242-7.3594 7.3906-7.3594h0.14848c4.082 0.0156 7.375 3.3398 7.3594 7.4219-0.0156 4.0703-3.3242 7.3594-7.3906 7.3594z" fill="#fa0"/><path d="m87.234 230.89c-1.9297 0-3.8555-0.75-5.3047-2.2422l-79.34-81.738c-2.8438-2.9297-2.7773-7.6094 0.15234-10.449 2.9297-2.8438 7.6094-2.7734 10.453 0.15235l79.344 81.738c2.8438 2.9258 2.7734 7.6094-0.15625 10.449-1.4375 1.3945-3.293 2.0898-5.1484 2.0898z" fill="#ff4e61"/><path d="m113.95 258.5c-1.8633 0-3.7266-0.69922-5.1641-2.1055-2.9219-2.8516-2.9766-7.5312-0.125-10.453l0.082-0.082c2.8516-2.918 7.5312-2.9766 10.453-0.12109 2.9219 2.8516 2.9766 7.5312 0.12109 10.453l-0.0781 0.082c-1.4492 1.4805-3.3672 2.2266-5.2891 2.2266z" fill="#fa0"/><path d="m131.4 276.48c-1.8555 0-3.7109-0.69531-5.1484-2.0898-2.9258-2.8438-2.9961-7.5234-0.15235-10.449l0.0781-0.0859c2.8476-2.9297 7.5273-2.9961 10.453-0.15235 2.9297 2.8438 3 7.5234 0.15625 10.453l-0.082 0.082c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#5c73bc"/><path d="m353.24 227.99c-1.8555 0-3.7109-0.69141-5.1445-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l79.34-81.734c2.8438-2.9297 7.5234-3 10.453-0.15625 2.9297 2.8438 3 7.5234 0.15625 10.453l-79.344 81.734c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#fa0"/><path d="m326.52 255.6c-1.9141 0-3.8242-0.73828-5.2695-2.2109l-0.082-0.082c-2.8633-2.9141-2.8203-7.5938 0.0899-10.453 2.9141-2.8633 7.5938-2.8203 10.453 0.0898l0.082 0.082c2.8594 2.9141 2.8203 7.5938-0.0937 10.453-1.4375 1.4141-3.3086 2.1211-5.1797 2.1211z" fill="#ff4e61"/><path d="m309.07 273.58c-1.9297 0-3.8555-0.75-5.3047-2.2422l-0.082-0.082c-2.8398-2.9297-2.7734-7.6094 0.15625-10.453s7.6094-2.7734 10.453 0.15234l0.082 0.082c2.8398 2.9297 2.7734 7.6094-0.15625 10.453-1.4375 1.3945-3.293 2.0898-5.1484 2.0898z" fill="#fa0"/><path d="m300.65 116.69c-1.2422 0-2.5-0.3125-3.6523-0.97266-3.5469-2.0234-4.7812-6.5391-2.7578-10.082l56.863-99.652c2.0234-3.543 6.5352-4.7773 10.082-2.7539 3.5469 2.0234 4.7812 6.5391 2.7578 10.082l-56.863 99.652c-1.3633 2.3867-3.8594 3.7266-6.4297 3.7266z" fill="#62d38f"/><path d="m281.52 150.33c-1.293 0-2.5977-0.33593-3.7891-1.0469l-0.0976-0.0586c-3.5-2.0938-4.6445-6.6328-2.5469-10.137 2.0938-3.5078 6.6328-4.6445 10.137-2.5508l0.0977 0.0586c3.5039 2.0938 4.6445 6.6328 2.5508 10.137-1.3867 2.3164-3.8359 3.5976-6.3516 3.5976z" fill="#fa0"/><path d="m269.02 172.25c-1.3008 0-2.6172-0.34375-3.8086-1.0625l-0.0977-0.0586c-3.4961-2.1094-4.6211-6.6523-2.5156-10.148 2.1094-3.4961 6.6523-4.6172 10.148-2.5117l0.0976 0.0586c3.4961 2.1094 4.6211 6.6523 2.5117 10.148-1.3867 2.3008-3.832 3.5742-6.3359 3.5742z" fill="#2dc471"/><path d="m139.96 116.69c-2.5703 0-5.0664-1.3398-6.4297-3.7305l-56.863-99.648c-2.0234-3.5469-0.78906-8.0586 2.7539-10.082 3.5469-2.0234 8.0625-0.79297 10.086 2.7539l56.863 99.648c2.0234 3.5469 0.78906 8.0625-2.7539 10.086-1.1562 0.66016-2.4141 0.97266-3.6562 0.97266z" fill="#5c73bc"/><path d="m159.09 150.33c-2.5078 0-4.957-1.2773-6.3438-3.582-2.1016-3.5-0.96875-8.043 2.5273-10.145l0.10157-0.0586c3.5-2.1016 8.0391-0.97266 10.141 2.5273 2.1055 3.5 0.97266 8.0391-2.5273 10.145l-0.0977 0.0586c-1.1914 0.71484-2.5039 1.0547-3.8008 1.0547z" fill="#ff4e61"/><path d="m171.6 172.25c-2.5 0-4.9375-1.2656-6.3281-3.5625-2.1172-3.4922-1-8.0352 2.4883-10.152l0.0977-0.0586c3.4961-2.1133 8.0391-1 10.156 2.4922 2.1133 3.4922 1 8.0352-2.4922 10.152l-0.0977 0.0586c-1.1992 0.72656-2.5195 1.0703-3.8242 1.0703z" fill="#fa0"/><path d="m402.14 357.28-15.523 11.602c-4.0234 3.0117-9.6523-0.043-9.5234-5.1641l0.5039-19.75c0.0508-2.0352-0.87109-3.9648-2.4688-5.1641l-15.508-11.621c-4.0234-3.0156-2.9453-9.4726 1.8242-10.93l18.391-5.6094c1.8906-0.57812 3.3906-2.082 4-4.0156l5.9375-18.785c1.5391-4.875 7.8359-5.8125 10.652-1.5898l10.863 16.285c1.1211 1.6758 2.9688 2.6797 4.9414 2.6797l19.18 0.0117c4.9766 4e-3 7.7891 5.8828 4.7578 9.9492l-11.676 15.672c-1.2031 1.6172-1.5586 3.7383-0.94922 5.6719l5.918 18.797c1.5312 4.875-3.0273 9.4453-7.7148 7.7344l-18.078-6.5977c-1.8594-0.67969-3.9258-0.37109-5.5273 0.82422z" fill="#ffd02f"/><path d="m261.51 512c-4.082 0-7.3906-3.3086-7.3906-7.3906 0-57.23 22.832-95.922 41.984-118.3 20.828-24.332 41.613-35.023 42.488-35.469 3.6406-1.8477 8.0898-0.39063 9.9336 3.2539 1.8438 3.6367 0.39453 8.0781-3.2422 9.9297-0.3125 0.16016-19.5 10.164-38.367 32.395-25.227 29.719-38.016 66.121-38.016 108.2 0 4.082-3.3086 7.3906-7.3906 7.3906z" fill="#ff4e61"/><path d="m102.86 397.35 11.766 15.605c3.0547 4.0469 9.2852 2.7305 10.547-2.2266l4.8633-19.113c0.5-1.9648 1.9102-3.5547 3.7695-4.2461l18.039-6.707c4.6797-1.7383 5.3906-8.25 1.207-11.016l-16.141-10.672c-1.6602-1.1016-2.6914-2.9726-2.7578-5.0039l-0.61719-19.75c-0.15625-5.1211-5.9492-7.832-9.7969-4.5859l-14.84 12.516c-1.5312 1.2891-3.5781 1.7227-5.4726 1.1562l-18.422-5.5c-4.7773-1.4258-9.0703 3.4102-7.2617 8.1836l6.9688 18.41c0.71875 1.8945 0.48438 4.0352-0.625 5.7188l-10.77 16.348c-2.793 4.2422 0.34375 9.9375 5.3125 9.6445l19.145-1.1406c1.9727-0.11719 3.875 0.77343 5.0859 2.3789z" fill="#ffd02f"/><path d="m179.02 512c-4.082 0-7.3906-3.3086-7.3906-7.3906 0-30.059-6.6797-57.559-19.852-81.734-1.9531-3.5859-0.62891-8.0742 2.957-10.027 3.5859-1.9531 8.0742-0.62891 10.027 2.9531 14.363 26.375 21.648 56.254 21.648 88.809 0 4.082-3.3086 7.3906-7.3906 7.3906z" fill="#fa0"/><path d="m268.93 55.898c0-11.285-8.8828-20.434-19.836-20.434-10.957 0-19.836 9.1484-19.836 20.434 0 11.285 8.8789 20.438 19.836 20.438 10.953 0 19.836-9.1523 19.836-20.438z" fill="#ffd02f"/><path d="m373.08 446.81c0-11.285-8.8789-20.434-19.832-20.434-10.957 0-19.836 9.1484-19.836 20.434s8.8789 20.434 19.836 20.434c10.953 0 19.832-9.1484 19.832-20.434z" fill="#5c73bc"/><path d="m44.129 450.86c0-9.0508-7.1211-16.387-15.91-16.387-8.7852 0-15.906 7.3359-15.906 16.387 0 9.0547 7.1211 16.391 15.906 16.391 8.7891 0 15.91-7.3359 15.91-16.391z" fill="#62d38f"/><path d="m88.172 288.35c0-9.0508-7.1211-16.387-15.91-16.387-8.7852 0-15.906 7.3359-15.906 16.387s7.1211 16.391 15.906 16.391c8.7891 0 15.91-7.3398 15.91-16.391z" fill="#5c73bc"/><g fill="#ff4e61"><path d="m210.84 16.391c0-9.0547-7.1211-16.391-15.906-16.391-8.7891 0-15.91 7.3359-15.91 16.391 0 9.0508 7.1211 16.387 15.91 16.387 8.7852 0 15.906-7.3359 15.906-16.387z"/><path d="m365.23 152.88c0-9.0508-7.125-16.391-15.91-16.391-8.7852 0-15.91 7.3398-15.91 16.391s7.125 16.387 15.91 16.387c8.7852 0 15.91-7.3359 15.91-16.387z"/><path d="m139.96 32.746c-1.8555 0-3.7109-0.69141-5.1484-2.0898-2.9297-2.8438-3-7.5195-0.15625-10.449l9.1914-9.4688c2.8438-2.9297 7.5234-3 10.449-0.15625 2.9297 2.8438 3 7.5234 0.15625 10.453l-9.1875 9.4688c-1.4492 1.4922-3.3789 2.2422-5.3047 2.2422z"/></g></g></svg>
diff --git a/resources/ui/images/placeholder.svg b/resources/ui/images/placeholder.svg
new file mode 100644
index 0000000000000000000000000000000000000000..23ace81fbb82a8409cc0710c0f7bddd6381f7256
--- /dev/null
+++ b/resources/ui/images/placeholder.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"/>
diff --git a/resources/ui/skins/default/img1.png b/resources/ui/skins/default/img1.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d3ba948e29a0c2ce6a4a3ca7dbf8f1cd28fa590
Binary files /dev/null and b/resources/ui/skins/default/img1.png differ
diff --git a/resources/ui/skins/default/img2.png b/resources/ui/skins/default/img2.png
new file mode 100644
index 0000000000000000000000000000000000000000..97b41d5a09f1a2e8daab9dccb3183bdb6d0a687f
Binary files /dev/null and b/resources/ui/skins/default/img2.png differ
diff --git a/resources/ui/skins/default/img3.png b/resources/ui/skins/default/img3.png
new file mode 100644
index 0000000000000000000000000000000000000000..e8c4bc5cf6f1e56e9f9b0bf593a767791bc2b6c4
Binary files /dev/null and b/resources/ui/skins/default/img3.png differ
diff --git a/resources/ui/skins/default/img4.png b/resources/ui/skins/default/img4.png
new file mode 100644
index 0000000000000000000000000000000000000000..748b6db01a0bf8cf9cfb804ba81f855dcd6e7261
Binary files /dev/null and b/resources/ui/skins/default/img4.png differ
diff --git a/resources/ui/skins/default/img5.png b/resources/ui/skins/default/img5.png
new file mode 100644
index 0000000000000000000000000000000000000000..76fa28abf8807bc6dd3a6f8801c5be752bdbea0d
Binary files /dev/null and b/resources/ui/skins/default/img5.png differ
diff --git a/resources/ui/skins/default/img6.png b/resources/ui/skins/default/img6.png
new file mode 100644
index 0000000000000000000000000000000000000000..7eebdc2836c7e4244fe6b027c6306e215593dc8e
Binary files /dev/null and b/resources/ui/skins/default/img6.png differ
diff --git a/resources/ui/skins/default/img7.png b/resources/ui/skins/default/img7.png
new file mode 100644
index 0000000000000000000000000000000000000000..a01af2aebf11ba513a9fdd024a8991af94b3f0d8
Binary files /dev/null and b/resources/ui/skins/default/img7.png differ
diff --git a/resources/ui/skins/default/img8.png b/resources/ui/skins/default/img8.png
new file mode 100644
index 0000000000000000000000000000000000000000..7ac728e19c25619218574700b1ae745e43bc8159
Binary files /dev/null and b/resources/ui/skins/default/img8.png differ
diff --git a/resources/ui/skins/default/img9.png b/resources/ui/skins/default/img9.png
new file mode 100644
index 0000000000000000000000000000000000000000..2dabefffaf8c8193ca419976969a2eed3e3416cd
Binary files /dev/null and b/resources/ui/skins/default/img9.png differ