ctucx.git: ctucx.things

simple inventory management web-app

commit bf213d75cce18151896f3612e9c0bd57677443fc
Author: Leah (ctucx) <git@ctu.cx>
Date: Fri, 4 Nov 2022 23:17:56 +0100

init
113 files changed, 16562 insertions(+), 0 deletions(-)
A
.gitignore
|
4
++++
A
LICENSE
|
340
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
package.json
|
37
+++++++++++++++++++++++++++++++++++++
A
public/css/font/OpenSans-Regular.eot
|
0
A
public/css/font/OpenSans-Regular.svg
|
349
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/css/font/OpenSans-Regular.ttf
|
0
A
public/css/font/OpenSans-Regular.woff
|
0
A
public/css/font/OpenSans-Regular.woff2
|
0
A
public/css/font/OpenSans-SemiBold.eot
|
0
A
public/css/font/OpenSans-SemiBold.svg
|
348
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/css/font/OpenSans-SemiBold.ttf
|
0
A
public/css/font/OpenSans-SemiBold.woff
|
0
A
public/css/font/OpenSans-SemiBold.woff2
|
0
A
public/css/fonts.css
|
29
+++++++++++++++++++++++++++++
A
public/editor.html
|
14
++++++++++++++
A
public/images/handle.png
|
0
A
public/images/sprite.png
|
0
A
public/images/sprite.psd
|
0
A
public/images/sprite2x.png
|
0
A
public/index.php
|
198
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/LibraryRenderer.php
|
122
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Autoloader.php
|
88
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Cache.php
|
43
+++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Cache/AbstractCache.php
|
60
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Cache/FilesystemCache.php
|
161
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Cache/NoopCache.php
|
47
+++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Compiler.php
|
689
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Context.php
|
242
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Engine.php
|
829
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Exception.php
|
18
++++++++++++++++++
A
public/lib/Mustache/Exception/InvalidArgumentException.php
|
18
++++++++++++++++++
A
public/lib/Mustache/Exception/LogicException.php
|
18
++++++++++++++++++
A
public/lib/Mustache/Exception/RuntimeException.php
|
18
++++++++++++++++++
A
public/lib/Mustache/Exception/SyntaxException.php
|
41
+++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Exception/UnknownFilterException.php
|
38
++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Exception/UnknownHelperException.php
|
38
++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Exception/UnknownTemplateException.php
|
38
++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/HelperCollection.php
|
172
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/LambdaHelper.php
|
76
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Loader.php
|
27
+++++++++++++++++++++++++++
A
public/lib/Mustache/Loader/ArrayLoader.php
|
79
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Loader/CascadingLoader.php
|
69
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Loader/FilesystemLoader.php
|
135
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Loader/InlineLoader.php
|
123
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Loader/MutableLoader.php
|
31
+++++++++++++++++++++++++++++++
A
public/lib/Mustache/Loader/ProductionFilesystemLoader.php
|
86
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Loader/StringLoader.php
|
39
+++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Logger.php
|
126
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Logger/AbstractLogger.php
|
121
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Logger/StreamLogger.php
|
194
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Parser.php
|
317
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Source.php
|
40
++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Source/FilesystemSource.php
|
77
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Template.php
|
180
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Mustache/Tokenizer.php
|
378
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Parsedown.php
|
1994
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Router.php
|
104
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/TinyHtmlMinifier.php
|
284
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/helpers.php
|
33
+++++++++++++++++++++++++++++++++
A
public/templates/categories.mustache
|
31
+++++++++++++++++++++++++++++++
A
public/templates/item.mustache
|
46
++++++++++++++++++++++++++++++++++++++++++++++
A
public/templates/total.mustache
|
97
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/templates/unitSelect.mustache
|
30
++++++++++++++++++++++++++++++
A
public/templates/view.mustache
|
52
++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/app.js
|
63
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/chart.js
|
427
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/category.vue
|
84
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/changePassword.vue
|
113
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/colorpicker.vue
|
36
++++++++++++++++++++++++++++++++++++
A
src/components/copyList.vue
|
56
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/errors.vue
|
62
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/globalAlerts.vue
|
50
++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/help.vue
|
47
+++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/importCsv.vue
|
172
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/item.vue
|
332
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/itemImage.vue
|
50
++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/itemLink.vue
|
53
+++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/itemViewImage.vue
|
40
++++++++++++++++++++++++++++++++++++++++
A
src/components/libraryItems.vue
|
259
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/libraryLists.vue
|
178
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/list.vue
|
184
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/listSettings.vue
|
130
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/listSummary.vue
|
178
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/modal.vue
|
136
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/moreDropdown.vue
|
46
++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/popover.vue
|
145
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/popoverHover.vue
|
45
+++++++++++++++++++++++++++++++++++++++++++++
A
src/components/share.vue
|
52
++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/sidebar.vue
|
97
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/speedbump.vue
|
69
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/spinner.vue
|
47
+++++++++++++++++++++++++++++++++++++++++++++++
A
src/components/unitSelect.vue
|
147
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/css/_common.scss
|
362
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/css/_edit.scss
|
75
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/css/_globals.scss
|
73
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/css/_list.scss
|
221
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/css/_print.scss
|
39
+++++++++++++++++++++++++++++++++++++++
A
src/css/_share.scss
|
277
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/css/_sprite.scss
|
118
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/css/app.scss
|
5
+++++
A
src/css/view.scss
|
26
++++++++++++++++++++++++++
A
src/dataTypes.js
|
768
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/store.js
|
350
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/utils/color.js
|
120
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/utils/focus.js
|
78
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/utils/mixin.js
|
23
+++++++++++++++++++++++
A
src/utils/utils.js
|
124
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/utils/weight.js
|
22
++++++++++++++++++++++
A
src/view.js
|
103
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/views/editor.vue
|
179
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/views/login.vue
|
110
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
webpack.config.js
|
71
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
yarn.lock
|
1352
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,3 @@
+
+node_modules
+public/dist+
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,339 @@
+GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  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
+this service 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 make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  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.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+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
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the 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 a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE 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.
+
+                     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
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    {description}
+    Copyright (C) {year}  {fullname}
+
+    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 2 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, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision 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, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  {signature of Ty Coon}, 1 April 1989
+  Ty Coon, President of Vice
+
+This 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.+
\ No newline at end of file
diff --git a/package.json b/package.json
@@ -0,0 +1,37 @@
+{
+	"name": "ctucx.things",
+	"type": "module",
+	"version": "1.0.0",
+	"description": "",
+	"scripts": {
+		"build": "mkdir -p ./public/dist && webpack-cli --config ./webpack.config.js"
+	},
+	"repository": {
+		"type": "git",
+		"url":  "https://git.ctu.cx/ctucx.things"
+	},
+	"author": "",
+	"license": "GPL-2.0",
+	"dependencies": {
+		"dragula": "^3.7.2",
+		"sass": "1.32.0",
+		"lodash": "4.17.20",
+
+		"vue": "2.6.12",
+		"vue-color-picker-wheel": "0.4.3",
+		"vue-router": "^3.4.9",
+		"vue-template-compiler": "2.6.12",
+		"vuex": "3.6.0",
+
+		"webpack": "5.11.1",
+
+		"sass-loader": "10.1.0",
+		"css-loader": "3.5.3",
+		"vue-loader": "15.9.6",
+
+		"mini-css-extract-plugin": "1.3.3"
+	},
+	"devDependencies": {
+		"webpack-cli": "^4.3.0"
+	}
+}
diff --git a/public/css/font/OpenSans-Regular.eot b/public/css/font/OpenSans-Regular.eot  Binary files differ.
diff --git a/public/css/font/OpenSans-Regular.svg b/public/css/font/OpenSans-Regular.svg
@@ -0,0 +1,349 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg">
+<defs >
+<font id="OpenSans" horiz-adv-x="1169" ><font-face
+    font-family="Open Sans"
+    units-per-em="2048"
+    panose-1="0 0 0 0 0 0 0 0 0 0"
+    ascent="2189"
+    descent="-600"
+    alphabetic="0" />
+<glyph unicode=" " horiz-adv-x="532" />
+<glyph unicode="!" horiz-adv-x="541" d="M325 406H216L170 1462H371L325 406ZM150 104Q150 174 184 203T269 233Q319 233 353 204T388 104Q388 35 354 4T269 -28Q218 -28 184 3T150 104Z" />
+<glyph unicode="&quot;" horiz-adv-x="816" d="M315 1462L277 934H172L135 1462H315ZM681 1462L644 934H539L502 1462H681Z" />
+<glyph unicode="#" horiz-adv-x="1323" d="M980 899L915 559H1198V432H890L805 0H670L755 432H450L368 0H235L314 432H52V559H339L406 899H128V1024H429L512 1462H649L566 1024H873L956 1462H1088L1005 1024H1270V899H980ZM474 559H779L845 899H540L474 559Z" />
+<glyph unicode="$" horiz-adv-x="1171" d="M518 -119V91Q403 93 299 111T128 158V320Q197 288 305 264T518 238V678Q386 715 300 762T170 879T127 1046Q127 1148 175 1220T312 1334T518 1381V1554H640V1383Q747 1380 835 1360T998 1307L946 1168Q878 1195 799
+1214T640 1240V803Q774 764 864 720T1001 611T1047 443Q1047 297 940 208T640 99V-119H518ZM640 247Q763 259 823 306T884 432Q884 488 861 524T784 586T640 637V247ZM518 845V1236Q443 1231 392 1208T316 1148T290 1060Q290 1001 312 962T385 895T518 845Z" />
+<glyph unicode="%" horiz-adv-x="1693" d="M399 1483Q549 1483 626 1364T704 1026Q704 808 629 687T399 565Q255 565 179 686T102 1026Q102 1244 175 1363T399 1483ZM399 1364Q318 1364 280 1280T242 1026Q242 857 280 771T399 685Q483 685 524 771T565 1026Q565
+1194 525 1279T399 1364ZM1325 1462L514 0H368L1179 1462H1325ZM1286 897Q1435 897 1513 778T1591 440Q1591 223 1516 102T1286 -20Q1141 -20 1065 101T989 440Q989 658 1061 777T1286 897ZM1286 777Q1205 777 1167 693T1129 440Q1129 271 1167 186T1286 100Q1370
+100 1411 184T1452 440Q1452 608 1412 692T1286 777Z" />
+<glyph unicode="&amp;" horiz-adv-x="1492" d="M623 1485Q731 1485 809 1448T930 1340T973 1171Q973 1041 889 951T674 787L1080 393Q1135 457 1170 541T1229 725H1397Q1365 593 1315 482T1188 287L1481 0H1256L1075 177Q1011 118 938 74T775 5T570 -20Q430 -20
+327 26T168 161T111 379Q111 485 150 561T261 696T433 811Q386 863 344 916T276 1031T250 1167Q250 1267 295 1338T424 1447T623 1485ZM533 706Q455 661 400 617T315 518T285 385Q285 268 364 198T575 127Q711 127 806 172T969 281L533 706ZM617 1348Q526 1348
+470 1301T413 1168Q413 1092 454 1028T571 886Q696 955 752 1019T809 1171Q809 1250 757 1299T617 1348Z" />
+<glyph unicode="&apos;" horiz-adv-x="449" d="M315 1462L277 934H172L135 1462H315Z" />
+<glyph unicode="(" horiz-adv-x="604" d="M82 561Q82 730 114 890T214 1195T383 1462H542Q397 1272 324 1040T251 563Q251 403 283 248T380 -52T540 -324H383Q281 -204 214 -63T115 237T82 561Z" />
+<glyph unicode=")" horiz-adv-x="604" d="M522 563Q522 396 490 238T391 -63T221 -324H64Q160 -197 224 -52T321 248T354 564Q354 727 321 885T224 1189T62 1462H221Q324 1339 390 1195T489 891T522 563Z" />
+<glyph unicode="*" horiz-adv-x="1128" d="M651 1556L613 1159L1008 1274L1034 1099L656 1060L900 735L738 646L557 1002L391 646L223 735L465 1060L89 1099L117 1274L506 1159L467 1556H651Z" />
+<glyph unicode="+" horiz-adv-x="1171" d="M652 790H1064V654H652V230H515V654H103V790H515V1216H652V790Z" />
+<glyph unicode="," horiz-adv-x="530" d="M365 238L378 215Q360 142 333 59T273 -107T207 -264H83Q104 -184 124 -96T160 79T187 238H365Z" />
+<glyph unicode="-" horiz-adv-x="659" d="M82 476V624H578V476H82Z" />
+<glyph unicode="." horiz-adv-x="538" d="M150 104Q150 174 184 203T267 233Q318 233 353 204T388 104Q388 35 353 4T267 -28Q218 -28 184 3T150 104Z" />
+<glyph unicode="/" horiz-adv-x="751" d="M729 1462L185 0H21L566 1462H729Z" />
+<glyph unicode="0" horiz-adv-x="1171" d="M1067 733Q1067 555 1040 415T955 178T805 31T584 -20Q421 -20 315 69T156 326T103 733Q103 967 150 1135T304 1394T584 1485Q749 1485 856 1396T1015 1138T1067 733ZM270 733Q270 529 301 393T401 190T584 122Q697 122
+766 189T867 392T899 733Q899 934 868 1069T768 1273T584 1342Q469 1342 400 1274T301 1070T270 733Z" />
+<glyph unicode="1" horiz-adv-x="1171" d="M719 0H557V1036Q557 1095 557 1137T559 1215T564 1288Q533 1256 506 1234T439 1178L272 1044L185 1157L581 1462H719V0Z" />
+<glyph unicode="2" horiz-adv-x="1171" d="M1059 0H101V139L492 536Q601 646 675 732T789 901T828 1085Q828 1209 755 1274T561 1340Q456 1340 375 1304T209 1202L120 1314Q178 1363 246 1401T393 1461T561 1483Q696 1483 794 1436T945 1302T999 1095Q999 979
+953 880T824 683T630 476L312 159V152H1059V0Z" />
+<glyph unicode="3" horiz-adv-x="1171" d="M1005 1121Q1005 1023 967 951T861 834T701 770V762Q875 740 962 650T1050 414Q1050 287 991 189T809 36T495 -20Q379 -20 281 -2T92 60V216Q183 171 290 146T497 120Q697 120 786 199T875 417Q875 512 826 570T684 656T461
+684H315V826H462Q581 826 664 861T790 959T834 1110Q834 1221 760 1281T559 1342Q481 1342 417 1326T297 1282T185 1217L101 1331Q181 1393 296 1438T557 1483Q781 1483 893 1381T1005 1121Z" />
+<glyph unicode="4" horiz-adv-x="1171" d="M1132 339H913V0H751V339H44V479L740 1470H913V489H1132V339ZM751 489V967Q751 1022 752 1066T755 1149T758 1223T761 1292H753Q734 1252 710 1208T660 1128L209 489H751Z" />
+<glyph unicode="5" horiz-adv-x="1171" d="M563 894Q712 894 822 844T992 697T1053 464Q1053 314 988 206T801 39T509 -20Q395 -20 297 0T132 60V218Q205 174 309 148T511 122Q622 122 705 157T835 265T882 448Q882 594 793 673T510 753Q448 753 374 743T252 721L168
+776L224 1462H951V1310H366L329 869Q367 877 427 885T563 894Z" />
+<glyph unicode="6" horiz-adv-x="1171" d="M116 625Q116 757 134 883T197 1117T317 1308T506 1436T779 1483Q824 1483 876 1479T962 1464V1321Q925 1334 878 1340T782 1346Q596 1346 490 1265T336 1046T282 734H293Q324 784 372 824T488 889T648 913Q776 913 871
+861T1019 710T1072 470Q1072 319 1016 209T857 40T610 -20Q503 -20 413 21T257 143T153 344T116 625ZM608 120Q744 120 826 207T908 470Q908 614 835 698T615 782Q515 782 441 741T326 636T285 508Q285 442 304 374T364 248T465 155T608 120Z" />
+<glyph unicode="7" horiz-adv-x="1171" d="M290 0L890 1310H93V1462H1068V1334L472 0H290Z" />
+<glyph unicode="8" horiz-adv-x="1171" d="M584 1483Q711 1483 809 1443T962 1326T1018 1135Q1018 1046 980 980T876 864T732 775Q826 732 901 678T1021 551T1065 378Q1065 255 1006 166T839 28T588 -20Q433 -20 325 26T160 160T103 371Q103 472 146 546T260 675T415
+766Q342 806 282 857T186 976T150 1136Q150 1247 206 1324T361 1442T584 1483ZM266 370Q266 258 345 186T584 113Q736 113 819 185T902 376Q902 449 864 504T756 604T591 684L555 697Q463 659 399 613T300 507T266 370ZM582 1347Q464 1347 389 1291T314 1128Q314
+1052 350 1000T450 910T591 840Q667 872 726 910T819 1002T853 1129Q853 1235 779 1291T582 1347Z" />
+<glyph unicode="9" horiz-adv-x="1171" d="M1061 839Q1061 706 1043 580T980 345T859 154T669 26T395 -21Q352 -21 297 -16T207 0V144Q244 131 294 124T392 116Q579 116 686 196T840 415T893 727H881Q851 679 803 639T685 574T524 549Q397 549 303 601T156 752T103
+991Q103 1142 160 1252T322 1423T567 1483Q675 1483 765 1442T921 1320T1024 1118T1061 839ZM567 1342Q434 1342 352 1255T269 993Q269 848 340 764T559 680Q661 680 735 721T850 825T891 954Q891 1020 872 1088T812 1213T711 1306T567 1342Z" />
+<glyph unicode=":" horiz-adv-x="538" d="M150 104Q150 174 184 203T267 233Q318 233 353 204T388 104Q388 35 353 4T267 -28Q218 -28 184 3T150 104ZM150 991Q150 1063 184 1092T267 1122Q318 1122 353 1093T388 991Q388 923 353 892T267 861Q218 861 184 892T150 991Z" />
+<glyph unicode=";" horiz-adv-x="538" d="M348 238L362 215Q344 143 316 60T256 -107T191 -264H65Q86 -185 106 -97T143 79T171 238H348ZM146 991Q146 1063 180 1092T263 1122Q316 1122 350 1093T384 991Q384 923 350 892T263 861Q214 861 180 892T146 991Z" />
+<glyph unicode="&lt;" horiz-adv-x="1171" d="M1065 243L103 669V764L1065 1240V1092L283 723L1065 390V243Z" />
+<glyph unicode="=" horiz-adv-x="1171" d="M115 858V993H1053V858H115ZM115 449V584H1053V449H115Z" />
+<glyph unicode="&gt;" horiz-adv-x="1171" d="M103 390L886 721L103 1092V1240L1065 764V669L103 243V390Z" />
+<glyph unicode="?" horiz-adv-x="884" d="M288 406V458Q288 538 303 595T355 703T460 809Q538 874 583 919T649 1011T669 1122Q669 1226 602 1281T413 1337Q314 1337 237 1312T89 1252L31 1386Q113 1429 209 1456T423 1483Q616 1483 722 1388T828 1125Q828 1032
+798 967T713 847T583 731Q517 675 482 633T434 546T421 439V406H288ZM244 104Q244 174 277 203T362 233Q411 233 445 204T480 104Q480 35 446 4T362 -28Q310 -28 277 3T244 104Z" />
+<glyph unicode="@" horiz-adv-x="1836" d="M1719 730Q1719 635 1698 545T1632 382T1523 266T1368 223Q1275 223 1220 277T1154 405H1145Q1107 326 1034 275T853 223Q697 223 613 326T528 602Q528 736 581 839T732 1002T963 1062Q1052 1062 1136 1047T1270 1014L1250
+611Q1249 574 1248 555T1247 524Q1247 416 1285 379T1379 341Q1446 341 1491 393T1561 534T1585 731Q1585 922 1508 1057T1296 1263T984 1334Q809 1334 674 1279T445 1124T303 885T255 580Q255 376 327 233T540 16T883 -59Q1005 -59 1118 -32T1320 29V-101Q1232
+-138 1123 -161T883 -185Q642 -185 471 -95T209 166T118 574Q118 767 177 929T349 1210T622 1394T984 1460Q1199 1460 1364 1372T1624 1120T1719 730ZM677 598Q677 465 730 403T877 341Q993 341 1047 428T1110 658L1122 919Q1093 928 1052 934T965 941Q862 941
+799 892T706 765T677 598Z" />
+<glyph unicode="A" horiz-adv-x="1295" d="M1117 0L937 464H351L172 0H0L572 1468H725L1293 0H1117ZM886 615L715 1076Q709 1094 696 1135T668 1220T645 1291Q635 1250 624 1210T601 1135T582 1076L408 615H886Z" />
+<glyph unicode="B" horiz-adv-x="1323" d="M200 1462H614Q888 1462 1026 1380T1164 1101Q1164 1016 1132 949T1037 837T884 776V766Q980 751 1054 711T1170 599T1212 416Q1212 281 1150 188T973 48T703 0H200V1462ZM370 835H650Q841 835 914 898T988 1082Q988
+1207 901 1262T622 1317H370V835ZM370 692V145H674Q869 145 950 221T1031 428Q1031 511 995 570T877 660T659 692H370Z" />
+<glyph unicode="C" horiz-adv-x="1290" d="M825 1333Q704 1333 608 1292T444 1172T340 982T304 732Q304 548 361 413T533 203T820 129Q918 129 1004 145T1173 187V39Q1093 9 1005 -5T796 -20Q573 -20 424 72T200 334T125 733Q125 899 171 1036T307 1274T527 1428T827
+1483Q938 1483 1041 1461T1227 1398L1159 1254Q1089 1286 1006 1309T825 1333Z" />
+<glyph unicode="D" horiz-adv-x="1486" d="M1361 745Q1361 498 1271 333T1011 84T597 0H200V1462H641Q864 1462 1025 1381T1273 1140T1361 745ZM1182 739Q1182 936 1117 1064T925 1254T615 1317H370V146H577Q879 146 1030 295T1182 739Z" />
+<glyph unicode="E" horiz-adv-x="1138" d="M1014 0H200V1462H1014V1312H370V839H977V691H370V150H1014V0Z" />
+<glyph unicode="F" horiz-adv-x="1057" d="M370 0H200V1462H1014V1312H370V776H975V627H370V0Z" />
+<glyph unicode="G" horiz-adv-x="1489" d="M825 766H1336V57Q1221 18 1100 -1T828 -20Q600 -20 444 71T206 330T125 731Q125 958 214 1127T473 1389T881 1483Q1003 1483 1112 1461T1316 1397L1251 1249Q1170 1284 1073 1309T871 1334Q692 1334 565 1260T370 1051T302
+731Q302 548 361 413T545 202T867 127Q966 127 1037 138T1166 166V614H825V766Z" />
+<glyph unicode="H" horiz-adv-x="1510" d="M1308 0H1138V689H370V0H200V1462H370V839H1138V1462H1308V0Z" />
+<glyph unicode="I" horiz-adv-x="572" d="M200 0V1462H370V0H200Z" />
+<glyph unicode="J" horiz-adv-x="550" d="M-11 -385Q-61 -385 -99 -378T-164 -359V-214Q-132 -224 -95 -229T-15 -235Q41 -235 88 -213T163 -138T191 8V1462H362V21Q362 -116 317 -206T188 -340T-11 -385Z" />
+<glyph unicode="K" horiz-adv-x="1254" d="M1254 0H1053L526 711L370 571V0H200V1462H370V733Q427 798 487 862T606 993L1033 1462H1232L650 828L1254 0Z" />
+<glyph unicode="L" horiz-adv-x="1069" d="M200 0V1462H370V152H1019V0H200Z" />
+<glyph unicode="M" horiz-adv-x="1842" d="M843 0L352 1294H344Q348 1253 351 1194T356 1066T358 924V0H200V1462H452L915 246H922L1392 1462H1642V0H1474V936Q1474 1001 1476 1066T1481 1190T1487 1292H1479L982 0H843Z" />
+<glyph unicode="N" horiz-adv-x="1542" d="M1343 0H1147L350 1228H342Q345 1179 349 1117T355 984T358 840V0H200V1462H395L1189 238H1196Q1194 273 1191 337T1186 476T1183 615V1462H1343V0Z" />
+<glyph unicode="O" horiz-adv-x="1593" d="M1468 733Q1468 564 1425 426T1298 188T1088 34T798 -20Q628 -20 502 34T292 188T167 427T125 735Q125 959 199 1128T423 1391T801 1485Q1018 1485 1166 1392T1391 1131T1468 733ZM304 733Q304 547 357 411T519 201T798
+127Q968 127 1076 201T1237 411T1289 733Q1289 1016 1171 1175T801 1335Q631 1335 521 1262T358 1055T304 733Z" />
+<glyph unicode="P" horiz-adv-x="1232" d="M582 1462Q865 1462 995 1352T1126 1035Q1126 942 1096 859T997 712T819 612T548 575H370V0H200V1462H582ZM566 1317H370V721H529Q669 721 762 751T903 848T950 1028Q950 1174 857 1245T566 1317Z" />
+<glyph unicode="Q" horiz-adv-x="1593" d="M1468 733Q1468 553 1419 407T1274 160T1033 13L1377 -348H1134L851 -18Q838 -18 825 -19T798 -20Q628 -20 502 34T292 188T167 427T125 735Q125 959 199 1128T423 1391T801 1485Q1018 1485 1166 1392T1391 1131T1468
+733ZM304 733Q304 547 357 411T519 201T798 127Q968 127 1076 201T1237 411T1289 733Q1289 1016 1171 1175T801 1335Q631 1335 521 1262T358 1055T304 733Z" />
+<glyph unicode="R" horiz-adv-x="1264" d="M595 1462Q775 1462 892 1418T1068 1282T1126 1050Q1126 934 1084 857T974 731T829 657L1230 0H1032L674 610H370V0H200V1462H595ZM585 1315H370V754H602Q781 754 865 827T950 1042Q950 1191 861 1253T585 1315Z" />
+<glyph unicode="S" horiz-adv-x="1123" d="M1025 389Q1025 259 961 168T780 28T507 -20Q424 -20 350 -12T214 11T105 48V211Q180 180 288 154T514 127Q624 127 700 156T815 241T855 375Q855 450 822 500T713 592T504 681Q411 714 340 753T221 843T149 959T124
+1110Q124 1227 183 1310T348 1438T591 1483Q708 1483 807 1461T990 1402L937 1256Q858 1289 770 1311T587 1333Q493 1333 429 1306T330 1228T296 1109Q296 1032 328 981T432 891T622 808Q751 761 841 709T978 581T1025 389Z" />
+<glyph unicode="T" horiz-adv-x="1128" d="M649 0H478V1312H18V1462H1107V1312H649V0Z" />
+<glyph unicode="U" horiz-adv-x="1493" d="M1306 1462V516Q1306 361 1244 240T1055 50T739 -20Q468 -20 327 127T185 520V1462H356V515Q356 329 454 228T749 127Q883 127 968 175T1095 311T1137 514V1462H1306Z" />
+<glyph unicode="V" horiz-adv-x="1221" d="M1221 1462L696 0H525L0 1462H178L520 499Q541 441 557 388T587 286T610 191Q620 237 633 286T663 389T701 502L1041 1462H1221Z" />
+<glyph unicode="W" horiz-adv-x="1891" d="M1861 1462L1470 0H1299L1009 984Q996 1026 985 1068T963 1149T947 1217T937 1262Q935 1247 930 1218T916 1151T896 1070T871 983L589 0H418L30 1462H207L442 545Q454 499 464 455T483 368T499 286T512 208Q517 247 525
+289T542 376T563 465T588 555L851 1462H1026L1300 548Q1314 501 1326 455T1347 366T1364 283T1378 208Q1385 257 1395 311T1418 424T1448 546L1683 1462H1861Z" />
+<glyph unicode="X" horiz-adv-x="1183" d="M1176 0H983L588 644L187 0H6L493 762L40 1462H229L594 879L961 1462H1141L689 765L1176 0Z" />
+<glyph unicode="Y" horiz-adv-x="1145" d="M573 729L962 1462H1145L658 567V0H488V559L0 1462H186L573 729Z" />
+<glyph unicode="Z" horiz-adv-x="1172" d="M1093 0H78V128L865 1310H105V1462H1072V1334L284 152H1093V0Z" />
+<glyph unicode="[" horiz-adv-x="670" d="M619 -324H166V1462H619V1326H328V-186H619V-324Z" />
+<glyph unicode="\" horiz-adv-x="751" d="M185 1462L731 0H566L21 1462H185Z" />
+<glyph unicode="]" horiz-adv-x="670" d="M51 -186H342V1326H51V1462H505V-324H51V-186Z" />
+<glyph unicode="^" horiz-adv-x="1171" d="M80 549L519 1473H615L1092 549H943L569 1295L229 549H80Z" />
+<glyph unicode="_" horiz-adv-x="897" d="M901 -307H-4V-184H901V-307Z" />
+<glyph unicode="`" horiz-adv-x="568" d="M280 1569Q304 1523 339 1467T414 1357T487 1265V1241H374Q338 1270 296 1310T211 1394T135 1479T82 1549V1569H280Z" />
+<glyph unicode="a" horiz-adv-x="1138" d="M585 1114Q781 1114 876 1026T971 745V0H850L818 162H810Q764 102 714 62T599 1T438 -20Q338 -20 261 15T139 121T94 301Q94 465 224 553T620 649L809 657V724Q809 866 748 923T576 980Q490 980 412 955T264 896L213
+1022Q287 1060 383 1087T585 1114ZM807 540L640 533Q435 525 351 466T267 299Q267 205 324 160T475 115Q621 115 714 196T807 439V540Z" />
+<glyph unicode="b" horiz-adv-x="1253" d="M341 1556V1167Q341 1100 338 1037T332 939H341Q386 1013 471 1064T688 1115Q894 1115 1016 973T1139 549Q1139 364 1083 237T925 45T684 -20Q554 -20 471 28T342 147H329L295 0H175V1556H341ZM661 976Q542 976 472 930T372
+790T341 553V544Q341 337 410 228T661 118Q814 118 890 230T967 550Q967 762 892 869T661 976Z" />
+<glyph unicode="c" horiz-adv-x="981" d="M614 -20Q466 -20 353 41T177 227T114 542Q114 741 180 867T364 1055T630 1116Q712 1116 788 1100T914 1058L864 919Q814 939 749 955T626 971Q512 971 437 922T324 778T286 544Q286 411 322 317T431 174T613 124Q700
+124 770 142T897 186V38Q842 10 775 -5T614 -20Z" />
+<glyph unicode="d" horiz-adv-x="1253" d="M565 -20Q357 -20 236 122T114 544Q114 827 238 971T568 1116Q655 1116 720 1093T832 1032T911 944H923Q919 975 915 1029T911 1117V1556H1077V0H943L918 156H911Q880 107 833 67T720 4T565 -20ZM591 118Q767 118 840
+218T913 515V545Q913 754 844 866T591 978Q438 978 362 861T286 540Q286 338 361 228T591 118Z" />
+<glyph unicode="e" horiz-adv-x="1150" d="M597 1116Q737 1116 837 1054T990 881T1043 620V517H286Q289 324 382 223T644 122Q748 122 828 141T994 197V51Q911 14 830 -3T637 -20Q479 -20 362 44T179 234T114 540Q114 717 173 846T341 1046T597 1116ZM595 980Q462
+980 383 893T289 650H869Q868 748 839 822T749 938T595 980Z" />
+<glyph unicode="f" horiz-adv-x="689" d="M663 966H390V0H224V966H30V1046L224 1101V1174Q224 1312 265 1398T384 1526T574 1567Q637 1567 689 1556T782 1531L739 1400Q705 1411 663 1420T576 1430Q481 1430 436 1369T390 1176V1096H663V966Z" />
+<glyph unicode="g" horiz-adv-x="1112" d="M481 -492Q265 -492 148 -412T31 -186Q31 -83 96 -10T278 87Q235 107 205 147T174 239Q174 299 207 344T310 432Q224 467 171 550T117 745Q117 863 166 946T308 1074T533 1118Q562 1118 591 1116T648 1109T695 1098H1071V991L869
+966Q899 927 919 872T939 750Q939 586 828 490T523 393Q477 393 429 401Q380 374 355 341T329 265Q329 233 348 214T405 187T494 178H687Q866 178 961 103T1057 -116Q1057 -298 909 -395T481 -492ZM486 -362Q622 -362 711 -335T845 -256T890 -133Q890 -67 860 -34T772
+11T630 23H440Q366 23 311 0T227 -68T197 -180Q197 -269 272 -315T486 -362ZM529 514Q648 514 708 574T768 749Q768 872 707 933T527 995Q413 995 352 932T290 746Q290 634 352 574T529 514Z" />
+<glyph unicode="h" horiz-adv-x="1256" d="M341 1556V1091Q341 1051 339 1011T332 936H343Q377 994 429 1033T549 1093T691 1114Q823 1114 911 1072T1044 942T1089 714V0H925V703Q925 840 863 908T671 976Q549 976 477 930T373 793T341 573V0H175V1556H341Z" />
+<glyph unicode="i" horiz-adv-x="517" d="M341 1096V0H175V1096H341ZM260 1506Q301 1506 330 1480T360 1397Q360 1342 331 1315T260 1288Q217 1288 189 1315T160 1397Q160 1453 188 1479T260 1506Z" />
+<glyph unicode="j" horiz-adv-x="517" d="M43 -492Q-8 -492 -46 -485T-112 -467V-332Q-81 -342 -49 -347T23 -353Q91 -353 133 -315T175 -177V1096H341V-173Q341 -273 309 -344T211 -454T43 -492ZM160 1397Q160 1453 188 1479T260 1506Q301 1506 330 1480T360
+1397Q360 1342 331 1315T260 1288Q217 1288 189 1315T160 1397Z" />
+<glyph unicode="k" horiz-adv-x="1076" d="M340 1556V748Q340 708 337 651T332 549H339Q360 575 400 626T469 708L833 1096H1028L587 628L1060 0H860L473 519L340 397V0H175V1556H340Z" />
+<glyph unicode="l" horiz-adv-x="517" d="M342 0H175V1556H342V0Z" />
+<glyph unicode="m" horiz-adv-x="1896" d="M1365 1116Q1546 1116 1638 1022T1730 718V0H1566V710Q1566 843 1509 909T1338 976Q1179 976 1107 884T1035 613V0H870V710Q870 799 845 858T769 946T641 976Q532 976 466 931T371 798T341 580V0H175V1096H309L334 941H343Q376
+998 426 1037T538 1096T670 1116Q795 1116 879 1070T1002 928H1011Q1065 1023 1159 1069T1365 1116Z" />
+<glyph unicode="n" horiz-adv-x="1256" d="M694 1116Q889 1116 989 1021T1089 714V0H925V703Q925 840 863 908T671 976Q489 976 415 873T341 574V0H175V1096H309L334 938H343Q378 996 432 1035T553 1095T694 1116Z" />
+<glyph unicode="o" horiz-adv-x="1232" d="M1120 550Q1120 415 1085 309T984 130T825 19T613 -20Q503 -20 412 18T254 130T151 309T114 550Q114 730 175 856T349 1049T620 1116Q770 1116 882 1049T1057 856T1120 550ZM286 550Q286 418 321 321T429 171T617 118Q731
+118 804 171T913 321T948 550Q948 681 913 776T805 924T616 976Q445 976 366 863T286 550Z" />
+<glyph unicode="p" horiz-adv-x="1253" d="M690 1116Q895 1116 1017 975T1139 551Q1139 364 1083 237T926 45T686 -20Q599 -20 533 3T420 65T342 150H330Q333 111 337 56T342 -40V-490H175V1096H312L334 934H342Q374 984 420 1025T532 1091T690 1116ZM661 976Q547
+976 478 932T376 801T342 581V549Q342 410 372 314T473 168T663 118Q765 118 833 173T934 326T968 553Q968 747 893 861T661 976Z" />
+<glyph unicode="q" horiz-adv-x="1253" d="M910 -490V-20Q910 19 912 70T919 158H908Q862 82 777 31T558 -20Q357 -20 235 122T113 546Q113 731 169 858T327 1050T567 1116Q698 1116 781 1065T911 939H919L944 1096H1076V-490H910ZM589 118Q705 118 775 161T877
+293T912 512V547Q912 759 840 868T589 978Q435 978 360 861T285 542Q285 341 360 230T589 118Z" />
+<glyph unicode="r" horiz-adv-x="837" d="M673 1116Q706 1116 742 1113T806 1103L785 949Q758 956 725 960T663 964Q597 964 539 938T436 862T367 743T342 588V0H175V1096H313L331 894H338Q372 955 420 1005T531 1086T673 1116Z" />
+<glyph unicode="s" horiz-adv-x="976" d="M884 300Q884 195 832 124T682 16T449 -20Q334 -20 250 -2T103 49V202Q170 169 263 142T453 115Q595 115 659 161T723 286Q723 331 698 365T612 432T446 504Q341 544 265 583T147 680T105 828Q105 967 217 1041T513 1116Q612
+1116 698 1097T860 1044L804 911Q736 940 659 960T502 980Q387 980 326 942T264 838Q264 787 292 754T384 692T550 624Q653 586 728 546T843 448T884 300Z" />
+<glyph unicode="t" horiz-adv-x="730" d="M529 116Q570 116 613 123T683 140V11Q654 -2 603 -11T502 -20Q415 -20 344 10T231 114T188 316V966H32V1047L189 1112L255 1350H355V1096H676V966H355V321Q355 218 402 167T529 116Z" />
+<glyph unicode="u" horiz-adv-x="1256" d="M1080 1096V0H944L920 154H911Q877 97 823 58T702 0T558 -20Q428 -20 340 22T208 152T163 378V1096H331V390Q331 253 393 186T582 118Q704 118 776 164T881 299T913 519V1096H1080Z" />
+<glyph unicode="v" horiz-adv-x="1023" d="M416 0L0 1096H178L419 433Q444 365 470 283T506 151H513Q525 201 553 283T604 433L845 1096H1023L606 0H416Z" />
+<glyph unicode="w" horiz-adv-x="1587" d="M1067 2L872 640Q859 681 848 720T827 796T810 864T797 919H790Q786 896 779 865T763 796T742 719T717 637L513 2H326L24 1098H196L354 493Q370 433 384 375T409 265T425 175H433Q439 200 446 235T463 309T484 388T506
+463L708 1098H887L1082 464Q1097 416 1111 364T1138 264T1155 177H1163Q1167 211 1178 261T1203 371T1234 493L1394 1098H1563L1260 2H1067Z" />
+<glyph unicode="x" horiz-adv-x="1072" d="M436 561L57 1096H247L536 674L824 1096H1012L633 561L1033 0H843L536 447L227 0H39L436 561Z" />
+<glyph unicode="y" horiz-adv-x="1026" d="M2 1096H180L422 460Q443 404 461 353T493 254T515 163H522Q536 213 562 294T618 461L847 1096H1026L549 -161Q511 -262 461 -337T338 -452T164 -493Q117 -493 81 -488T19 -475V-342Q41 -347 72 -351T138 -355Q200 -355
+245 -332T324 -263T381 -156L441 -2L2 1096Z" />
+<glyph unicode="z" horiz-adv-x="960" d="M879 0H80V110L681 966H118V1096H866V973L273 129H879V0Z" />
+<glyph unicode="{" horiz-adv-x="768" d="M702 -324Q578 -323 489 -288T352 -181T304 -3V303Q304 374 276 417T193 481T57 501V639Q138 640 193 659T276 722T304 836V1144Q304 1251 354 1321T493 1427T702 1462V1326Q628 1324 576 1303T496 1239T468 1128V827Q468
+723 415 660T252 577V565Q364 546 416 483T468 315V8Q468 -60 495 -102T574 -165T702 -186V-324Z" />
+<glyph unicode="|" horiz-adv-x="1125" d="M492 1557H631V-496H492V1557Z" />
+<glyph unicode="}" horiz-adv-x="768" d="M67 -324V-186Q141 -184 193 -164T272 -101T300 10V313Q300 418 353 481T516 563V575Q405 595 353 658T300 825V1129Q300 1198 273 1241T194 1304T67 1326V1462Q191 1461 280 1426T416 1321T464 1142V838Q464 766 492
+723T575 659T712 639V501Q631 501 576 481T492 418T464 305V-5Q464 -111 414 -182T275 -288T67 -324Z" />
+<glyph unicode="~" horiz-adv-x="1171" d="M554 658Q483 690 434 702T338 715Q281 715 218 681T103 595V744Q153 797 214 824T349 851Q411 851 469 838T616 786Q689 755 737 742T830 729Q889 729 952 763T1065 849V702Q1017 650 956 622T821 593Q761 593 702 606T554
+658Z" />
+<glyph unicode="&#xa0;" horiz-adv-x="532" />
+<glyph unicode="&#xa1;" horiz-adv-x="541" d="M212 681H323L369 -374H166L212 681ZM388 985Q388 915 354 886T269 856Q219 856 185 885T150 985Q150 1053 184 1084T269 1116Q319 1116 353 1085T388 985Z" />
+<glyph unicode="&#xa2;" horiz-adv-x="1171" d="M720 1483V1318Q797 1315 867 1299T989 1260L941 1121Q886 1142 820 1157T697 1173Q582 1173 506 1125T393 982T355 743Q355 601 392 509T502 371T686 325Q774 325 842 342T972 385V240Q917 213 858 197T718 179V-20H590V184Q465
+202 374 264T234 444T185 741Q185 924 235 1043T376 1227T590 1310V1483H720Z" />
+<glyph unicode="&#xa3;" horiz-adv-x="1171" d="M686 1481Q797 1481 885 1458T1043 1400L983 1266Q922 1295 848 1318T690 1342Q569 1342 506 1278T443 1072V785H859V658H443V436Q443 352 423 297T371 208T300 152H1092V0H68V141Q129 155 176 189T249 283T276
+434V658H77V785H276V1090Q276 1214 326 1301T469 1434T686 1481Z" />
+<glyph unicode="&#xa4;" horiz-adv-x="1171" d="M183 723Q183 786 203 845T258 954L121 1095L213 1185L351 1051Q400 1086 460 1105T586 1125Q650 1125 708 1106T816 1051L955 1185L1047 1095L912 955Q945 907 966 848T988 723Q988 659 969 599T912 489L1045 351L955
+262L816 396Q768 362 709 343T586 323Q520 323 459 342T351 398L213 263L122 352L258 491Q224 540 204 599T183 723ZM311 723Q311 646 347 584T446 485T585 448Q663 448 726 485T826 584T863 723Q863 801 826 864T726 964T586 1002Q509 1002 447 965T348 864T311
+723Z" />
+<glyph unicode="&#xa5;" horiz-adv-x="1171" d="M584 741L961 1462H1136L716 691H980V568H665V394H980V271H665V0H503V271H187V394H503V568H187V691H447L31 1462H208L584 741Z" />
+<glyph unicode="&#xa6;" horiz-adv-x="1125" d="M492 1557H631V780H492V1557ZM492 282H631V-496H492V282Z" />
+<glyph unicode="&#xa7;" horiz-adv-x="1052" d="M140 809Q140 910 191 973T309 1067Q233 1106 190 1161T147 1302Q147 1424 250 1495T546 1566Q659 1566 738 1547T892 1497L841 1369Q772 1397 702 1416T535 1436Q410 1436 356 1402T302 1306Q302 1265 328 1234T414
+1174T576 1108Q680 1071 755 1028T870 926T911 784Q911 680 864 613T754 511Q827 474 867 420T908 285Q908 146 793 69T468 -9Q355 -9 271 9T122 58V202Q167 181 225 162T347 130T475 118Q630 118 689 164T749 272Q749 314 727 345T647 406T479 477Q374 516 298
+558T181 659T140 809ZM283 827Q283 774 312 734T405 660T576 585L630 566Q682 595 724 642T767 760Q767 814 738 856T637 935T441 1010Q379 994 331 946T283 827Z" />
+<glyph unicode="&#xa8;" horiz-adv-x="1187" d="M310 1394Q310 1444 336 1467T400 1490Q439 1490 465 1467T492 1394Q492 1345 466 1321T400 1296Q362 1296 336 1320T310 1394ZM694 1394Q694 1444 720 1467T783 1490Q821 1490 848 1467T875 1394Q875 1345 848
+1321T783 1296Q746 1296 720 1320T694 1394Z" />
+<glyph unicode="&#xa9;" horiz-adv-x="1704" d="M852 -20Q689 -20 552 36T313 193T156 431T100 731Q100 894 156 1031T313 1270T552 1427T852 1483Q1009 1483 1145 1427T1385 1269T1546 1030T1604 731Q1604 569 1548 432T1391 193T1152 36T852 -20ZM884 274Q682
+274 580 398T478 731Q478 864 526 966T667 1127T892 1186Q958 1186 1023 1170T1145 1125L1089 1009Q1039 1035 990 1048T894 1062Q767 1062 697 974T627 731Q627 571 690 485T891 399Q941 399 998 412T1108 446V324Q1059 302 1007 288T884 274ZM852 82Q985 82 1101
+130T1306 265T1444 471T1494 731Q1494 865 1447 982T1313 1189T1110 1329T852 1380Q712 1380 595 1332T391 1197T257 991T209 731Q209 597 256 480T389 273T593 133T852 82Z" />
+<glyph unicode="&#xaa;" horiz-adv-x="723" d="M360 1479Q490 1479 556 1424T622 1250V800H531L508 887Q469 844 412 816T278 787Q214 787 167 809T94 876T68 988Q68 1056 103 1102T210 1173T391 1202L503 1206V1253Q503 1322 461 1350T356 1378Q301 1378 250
+1364T152 1325L109 1420Q161 1446 226 1462T360 1479ZM503 1118L405 1114Q281 1110 236 1078T191 988Q191 935 222 911T306 886Q410 886 456 935T503 1068V1118Z" />
+<glyph unicode="&#xab;" horiz-adv-x="1015" d="M79 556L419 965L538 897L251 544L538 191L419 122L79 529V556ZM477 556L822 965L939 897L653 544L939 191L822 122L477 529V556Z" />
+<glyph unicode="&#xac;" horiz-adv-x="1171" d="M1060 790V263H926V654H103V790H1060Z" />
+<glyph unicode="&#xad;" horiz-adv-x="659" d="M82 476V624H578V476H82Z" />
+<glyph unicode="&#xae;" horiz-adv-x="1704" d="M575 284V1177H836Q999 1177 1077 1112T1155 914Q1155 817 1106 761T992 679L1229 284H1065L858 640H720V284H575ZM720 758H831Q914 758 961 799T1009 910Q1009 987 965 1021T829 1055H720V758ZM852 -20Q689 -20
+552 36T313 193T156 431T100 731Q100 894 156 1031T313 1270T552 1427T852 1483Q1009 1483 1145 1427T1385 1269T1546 1030T1604 731Q1604 569 1548 432T1391 193T1152 36T852 -20ZM852 82Q985 82 1101 130T1306 265T1444 471T1494 731Q1494 865 1447 982T1313
+1189T1110 1329T852 1380Q712 1380 595 1332T391 1197T257 991T209 731Q209 597 256 480T389 273T593 133T852 82Z" />
+<glyph unicode="&#xaf;" horiz-adv-x="1024" d="M1030 1556H-6V1683H1030V1556Z" />
+<glyph unicode="&#xb0;" horiz-adv-x="877" d="M438 859Q342 859 270 898T158 1007T117 1170Q117 1263 156 1333T268 1443T438 1483Q533 1483 605 1444T719 1334T760 1170Q760 1078 719 1008T606 898T438 859ZM440 973Q538 973 587 1027T636 1170Q636 1262 586
+1316T440 1371Q339 1371 290 1317T241 1170Q241 1082 289 1028T440 973Z" />
+<glyph unicode="&#xb1;" horiz-adv-x="1171" d="M103 0V135H1066V0H103ZM652 795H1064V659H652V235H515V659H103V795H515V1221H652V795Z" />
+<glyph unicode="&#xb2;" horiz-adv-x="712" d="M627 852H50V956L287 1188Q369 1268 413 1319T473 1411T490 1499Q490 1565 451 1600T346 1636Q285 1636 234 1613T129 1548L62 1637Q122 1687 192 1717T348 1747Q478 1747 552 1683T626 1506Q626 1438 599 1381T518
+1266T387 1135L217 971H627V852Z" />
+<glyph unicode="&#xb3;" horiz-adv-x="712" d="M339 1747Q482 1747 555 1684T628 1523Q628 1439 584 1386T477 1314V1308Q558 1292 605 1240T653 1104Q653 985 568 911T304 837Q229 837 164 850T37 893V1014Q105 982 174 964T305 946Q413 946 464 989T516 1108Q516
+1184 457 1218T289 1253H170V1359H289Q393 1359 441 1400T490 1508Q490 1573 448 1605T341 1637Q277 1637 222 1616T111 1559L43 1648Q105 1693 175 1720T339 1747Z" />
+<glyph unicode="&#xb4;" horiz-adv-x="568" d="M487 1569V1549Q467 1520 433 1480T356 1395T272 1310T193 1241H82V1265Q114 1303 152 1356T226 1466T286 1569H487Z" />
+<glyph unicode="&#xb5;" horiz-adv-x="1266" d="M1091 1096V0H956L930 152H921Q887 98 840 60T731 1T587 -20Q501 -20 441 7T340 80H332Q335 56 337 21T340 -59T341 -157V-492H175V1096H341V388Q341 255 405 187T598 118Q722 118 793 164T894 300T925 519V1096H1091Z" />
+<glyph unicode="&#xb6;" horiz-adv-x="1341" d="M1117 -260H1006V1449H790V-260H678V577Q647 568 608 564T532 559Q407 559 315 607T173 764T122 1053Q122 1242 177 1352T331 1509T563 1556H1117V-260Z" />
+<glyph unicode="&#xb7;" horiz-adv-x="538" d="M150 714Q150 784 184 813T267 843Q318 843 353 814T388 714Q388 645 353 614T267 582Q218 582 184 613T150 714Z" />
+<glyph unicode="&#xb8;" horiz-adv-x="454" d="M427 -286Q427 -383 352 -437T132 -492Q101 -492 73 -489T28 -481V-376Q47 -380 77 -383T137 -386Q212 -386 252 -364T292 -290Q292 -235 239 -210T102 -176L191 0H302L248 -112Q298 -122 338 -143T403 -199T427 -286Z" />
+<glyph unicode="&#xb9;" horiz-adv-x="712" d="M481 1729V852H346V1418Q346 1454 347 1485T349 1547T353 1607Q332 1588 306 1567T251 1527L142 1450L76 1544L343 1729H481Z" />
+<glyph unicode="&#xba;" horiz-adv-x="765" d="M701 1135Q701 971 615 879T381 787Q242 787 155 876T67 1135Q67 1301 152 1390T385 1480Q482 1480 552 1440T662 1322T701 1135ZM188 1135Q188 1014 234 952T382 889Q484 889 530 951T577 1135Q577 1255 531 1316T383
+1377Q282 1377 235 1317T188 1135Z" />
+<glyph unicode="&#xbb;" horiz-adv-x="1015" d="M937 530L592 122L476 191L762 545L476 897L592 965L937 557V530ZM536 530L194 122L77 191L363 545L77 897L194 965L536 557V530Z" />
+<glyph unicode="&#xbc;" horiz-adv-x="1516" d="M263 0L1141 1462H1285L406 0H263ZM336 586V1152Q336 1188 337 1219T339 1281T343 1341Q322 1322 296 1301T241 1260L132 1184L66 1278L333 1462H470V586H336ZM1227 0V205H825V303L1230 883H1369V319H1497V205H1369V0H1227ZM960
+319H1227V526Q1227 570 1228 625T1232 729Q1221 704 1191 657T1137 576L960 319Z" />
+<glyph unicode="&#xbd;" horiz-adv-x="1573" d="M208 0L1087 1462H1230L351 0H208ZM313 586V1152Q313 1179 313 1203T315 1251T318 1296T321 1341Q299 1322 274 1301T219 1260L110 1184L44 1278L311 1462H449V586H313ZM911 0V104L1148 336Q1230 416 1274 467T1335
+559T1352 647Q1352 713 1312 748T1207 784Q1145 784 1094 761T990 696L924 785Q983 835 1053 865T1209 895Q1339 895 1413 831T1487 654Q1487 586 1459 529T1378 414T1248 283L1078 119H1488V0H911Z" />
+<glyph unicode="&#xbe;" horiz-adv-x="1594" d="M356 0L1235 1462H1378L500 0H356ZM300 570Q226 570 160 583T33 627V748Q101 715 170 698T302 680Q410 680 461 723T513 842Q513 918 453 952T285 987H166V1093H285Q389 1093 437 1134T486 1242Q486 1307 444 1339T337
+1371Q273 1371 218 1350T107 1293L39 1381Q101 1427 171 1454T336 1481Q478 1481 551 1418T624 1256Q624 1173 580 1120T474 1048V1041Q554 1025 602 973T650 838Q650 719 564 645T300 570ZM1306 0V205H904V303L1309 883H1448V319H1576V205H1448V0H1306ZM1040 319H1306V526Q1306
+570 1307 625T1312 729Q1299 704 1269 657T1216 576L1040 319Z" />
+<glyph unicode="&#xbf;" horiz-adv-x="884" d="M593 684V632Q593 553 578 495T526 387T421 281Q343 216 297 171T232 79T212 -32Q212 -136 279 -191T468 -247Q567 -247 644 -222T792 -162L850 -296Q768 -339 673 -366T458 -393Q265 -393 159 -298T53 -35Q53 58
+83 123T168 243T298 359Q365 415 399 457T447 544T460 651V684H593ZM637 986Q637 916 605 887T519 857Q470 857 436 886T401 986Q401 1055 435 1086T519 1118Q572 1118 604 1087T637 986Z" />
+<glyph unicode="&#xc0;" horiz-adv-x="1295" d="M1117 0L937 464H351L172 0H0L572 1468H725L1293 0H1117ZM886 615L715 1076Q709 1094 696 1135T668 1220T645 1291Q635 1250 624 1210T601 1135T582 1076L408 615H886ZM577 1936Q601 1890 636 1834T711 1724T784
+1632V1608H671Q635 1637 593 1677T508 1761T432 1846T379 1916V1936H577Z" />
+<glyph unicode="&#xc1;" horiz-adv-x="1295" d="M1117 0L937 464H351L172 0H0L572 1468H725L1293 0H1117ZM886 615L715 1076Q709 1094 696 1135T668 1220T645 1291Q635 1250 624 1210T601 1135T582 1076L408 615H886ZM935 1936V1916Q915 1887 881 1847T804 1762T720
+1677T641 1608H530V1632Q562 1670 600 1723T674 1833T734 1936H935Z" />
+<glyph unicode="&#xc2;" horiz-adv-x="1295" d="M1117 0L937 464H351L172 0H0L572 1468H725L1293 0H1117ZM886 615L715 1076Q709 1094 696 1135T668 1220T645 1291Q635 1250 624 1210T601 1135T582 1076L408 615H886ZM732 1935Q757 1890 802 1834T897 1723T986
+1630V1608H868Q814 1643 757 1694T646 1800Q592 1745 537 1695T427 1608H313V1630Q351 1670 399 1724T492 1834T563 1935H732Z" />
+<glyph unicode="&#xc3;" horiz-adv-x="1295" d="M1117 0L937 464H351L172 0H0L572 1468H725L1293 0H1117ZM886 615L715 1076Q709 1094 696 1135T668 1220T645 1291Q635 1250 624 1210T601 1135T582 1076L408 615H886ZM269 1611Q275 1671 292 1718T337 1798T403
+1848T489 1866Q535 1866 577 1848T657 1806T730 1764T799 1745Q847 1745 873 1774T914 1868H1012Q999 1751 943 1682T792 1612Q748 1612 707 1630T628 1672T554 1714T482 1733Q433 1733 407 1704T367 1611H269Z" />
+<glyph unicode="&#xc4;" horiz-adv-x="1295" d="M1117 0L937 464H351L172 0H0L572 1468H725L1293 0H1117ZM886 615L715 1076Q709 1094 696 1135T668 1220T645 1291Q635 1250 624 1210T601 1135T582 1076L408 615H886ZM362 1761Q362 1811 388 1834T452 1857Q491
+1857 517 1834T544 1761Q544 1712 518 1688T452 1663Q414 1663 388 1687T362 1761ZM746 1761Q746 1811 772 1834T835 1857Q873 1857 900 1834T927 1761Q927 1712 900 1688T835 1663Q798 1663 772 1687T746 1761Z" />
+<glyph unicode="&#xc5;" horiz-adv-x="1295" d="M1117 0L937 464H351L172 0H0L572 1468H725L1293 0H1117ZM886 615L715 1076Q709 1094 696 1135T668 1220T645 1291Q635 1250 624 1210T601 1135T582 1076L408 615H886ZM643 1372Q546 1372 484 1429T422 1588Q422
+1688 483 1745T643 1802Q738 1802 803 1745T868 1590Q868 1487 804 1430T643 1372ZM643 1468Q696 1468 729 1500T763 1588Q763 1643 729 1675T643 1707Q593 1707 559 1675T524 1588Q524 1533 555 1501T643 1468Z" />
+<glyph unicode="&#xc6;" horiz-adv-x="1778" d="M1665 0H901V464H396L174 0H-2L685 1462H1665V1312H1071V839H1626V691H1071V150H1665V0ZM462 615H901V1310H786L462 615Z" />
+<glyph unicode="&#xc7;" horiz-adv-x="1290" d="M825 1333Q704 1333 608 1292T444 1172T340 982T304 732Q304 548 361 413T533 203T820 129Q918 129 1004 145T1173 187V39Q1093 9 1005 -5T796 -20Q573 -20 424 72T200 334T125 733Q125 899 171 1036T307 1274T527
+1428T827 1483Q938 1483 1041 1461T1227 1398L1159 1254Q1089 1286 1006 1309T825 1333ZM961 -286Q961 -383 886 -437T666 -492Q635 -492 607 -489T562 -481V-376Q581 -380 611 -383T671 -386Q746 -386 786 -364T826 -290Q826 -235 773 -210T636 -176L725 0H836L782
+-112Q832 -122 872 -143T937 -199T961 -286Z" />
+<glyph unicode="&#xc8;" horiz-adv-x="1138" d="M1014 0H200V1462H1014V1312H370V839H977V691H370V150H1014V0ZM557 1936Q581 1890 616 1834T691 1724T764 1632V1608H651Q615 1637 573 1677T488 1761T412 1846T359 1916V1936H557Z" />
+<glyph unicode="&#xc9;" horiz-adv-x="1138" d="M1014 0H200V1462H1014V1312H370V839H977V691H370V150H1014V0ZM916 1936V1916Q896 1887 862 1847T785 1762T701 1677T622 1608H511V1632Q543 1670 581 1723T655 1833T715 1936H916Z" />
+<glyph unicode="&#xca;" horiz-adv-x="1138" d="M1014 0H200V1462H1014V1312H370V839H977V691H370V150H1014V0ZM712 1935Q737 1890 782 1834T877 1723T966 1630V1608H848Q794 1643 737 1694T626 1800Q572 1745 517 1695T407 1608H293V1630Q331 1670 379 1724T472
+1834T543 1935H712Z" />
+<glyph unicode="&#xcb;" horiz-adv-x="1138" d="M1014 0H200V1462H1014V1312H370V839H977V691H370V150H1014V0ZM343 1761Q343 1811 369 1834T433 1857Q472 1857 498 1834T525 1761Q525 1712 499 1688T433 1663Q395 1663 369 1687T343 1761ZM727 1761Q727 1811
+753 1834T816 1857Q854 1857 881 1834T908 1761Q908 1712 881 1688T816 1663Q779 1663 753 1687T727 1761Z" />
+<glyph unicode="&#xcc;" horiz-adv-x="572" d="M200 0V1462H370V0H200ZM186 1936Q210 1890 245 1834T320 1724T393 1632V1608H280Q244 1637 202 1677T117 1761T41 1846T-12 1916V1936H186Z" />
+<glyph unicode="&#xcd;" horiz-adv-x="572" d="M200 0V1462H370V0H200ZM585 1936V1916Q565 1887 531 1847T454 1762T370 1677T291 1608H180V1632Q212 1670 250 1723T324 1833T384 1936H585Z" />
+<glyph unicode="&#xce;" horiz-adv-x="572" d="M200 0V1462H370V0H200ZM369 1935Q394 1890 439 1834T534 1723T623 1630V1608H505Q451 1643 394 1694T283 1800Q229 1745 174 1695T64 1608H-50V1630Q-12 1670 36 1724T129 1834T200 1935H369Z" />
+<glyph unicode="&#xcf;" horiz-adv-x="572" d="M200 0V1462H370V0H200ZM6 1761Q6 1811 32 1834T96 1857Q135 1857 161 1834T188 1761Q188 1712 162 1688T96 1663Q58 1663 32 1687T6 1761ZM390 1761Q390 1811 416 1834T479 1857Q517 1857 544 1834T571 1761Q571
+1712 544 1688T479 1663Q442 1663 416 1687T390 1761Z" />
+<glyph unicode="&#xd0;" horiz-adv-x="1486" d="M641 1462Q863 1462 1024 1381T1273 1140T1361 745Q1361 498 1271 333T1010 84T595 0H213V649H58V798H213V1462H641ZM615 1317H382V798H754V649H382V146H577Q880 146 1031 295T1182 739Q1182 936 1116 1064T924
+1254T615 1317Z" />
+<glyph unicode="&#xd1;" horiz-adv-x="1542" d="M1343 0H1147L350 1228H342Q345 1179 349 1117T355 984T358 840V0H200V1462H395L1189 238H1196Q1194 273 1191 337T1186 476T1183 615V1462H1343V0ZM398 1611Q404 1671 421 1718T466 1798T532 1848T618 1866Q664
+1866 706 1848T786 1806T859 1764T928 1745Q976 1745 1002 1774T1043 1868H1141Q1128 1751 1072 1682T921 1612Q877 1612 836 1630T757 1672T683 1714T611 1733Q562 1733 536 1704T496 1611H398Z" />
+<glyph unicode="&#xd2;" horiz-adv-x="1593" d="M1468 733Q1468 564 1425 426T1298 188T1088 34T798 -20Q628 -20 502 34T292 188T167 427T125 735Q125 959 199 1128T423 1391T801 1485Q1018 1485 1166 1392T1391 1131T1468 733ZM304 733Q304 547 357 411T519
+201T798 127Q968 127 1076 201T1237 411T1289 733Q1289 1016 1171 1175T801 1335Q631 1335 521 1262T358 1055T304 733ZM730 1936Q754 1890 789 1834T864 1724T937 1632V1608H824Q788 1637 746 1677T661 1761T585 1846T532 1916V1936H730Z" />
+<glyph unicode="&#xd3;" horiz-adv-x="1593" d="M1468 733Q1468 564 1425 426T1298 188T1088 34T798 -20Q628 -20 502 34T292 188T167 427T125 735Q125 959 199 1128T423 1391T801 1485Q1018 1485 1166 1392T1391 1131T1468 733ZM304 733Q304 547 357 411T519
+201T798 127Q968 127 1076 201T1237 411T1289 733Q1289 1016 1171 1175T801 1335Q631 1335 521 1262T358 1055T304 733ZM1087 1936V1916Q1067 1887 1033 1847T956 1762T872 1677T793 1608H682V1632Q714 1670 752 1723T826 1833T886 1936H1087Z" />
+<glyph unicode="&#xd4;" horiz-adv-x="1593" d="M1468 733Q1468 564 1425 426T1298 188T1088 34T798 -20Q628 -20 502 34T292 188T167 427T125 735Q125 959 199 1128T423 1391T801 1485Q1018 1485 1166 1392T1391 1131T1468 733ZM304 733Q304 547 357 411T519
+201T798 127Q968 127 1076 201T1237 411T1289 733Q1289 1016 1171 1175T801 1335Q631 1335 521 1262T358 1055T304 733ZM884 1935Q909 1890 954 1834T1049 1723T1138 1630V1608H1020Q966 1643 909 1694T798 1800Q744 1745 689 1695T579 1608H465V1630Q503 1670
+551 1724T644 1834T715 1935H884Z" />
+<glyph unicode="&#xd5;" horiz-adv-x="1593" d="M1468 733Q1468 564 1425 426T1298 188T1088 34T798 -20Q628 -20 502 34T292 188T167 427T125 735Q125 959 199 1128T423 1391T801 1485Q1018 1485 1166 1392T1391 1131T1468 733ZM304 733Q304 547 357 411T519
+201T798 127Q968 127 1076 201T1237 411T1289 733Q1289 1016 1171 1175T801 1335Q631 1335 521 1262T358 1055T304 733ZM420 1611Q426 1671 443 1718T488 1798T554 1848T640 1866Q686 1866 728 1848T808 1806T881 1764T950 1745Q998 1745 1024 1774T1065 1868H1163Q1150
+1751 1094 1682T943 1612Q899 1612 858 1630T779 1672T705 1714T633 1733Q584 1733 558 1704T518 1611H420Z" />
+<glyph unicode="&#xd6;" horiz-adv-x="1593" d="M1468 733Q1468 564 1425 426T1298 188T1088 34T798 -20Q628 -20 502 34T292 188T167 427T125 735Q125 959 199 1128T423 1391T801 1485Q1018 1485 1166 1392T1391 1131T1468 733ZM304 733Q304 547 357 411T519
+201T798 127Q968 127 1076 201T1237 411T1289 733Q1289 1016 1171 1175T801 1335Q631 1335 521 1262T358 1055T304 733ZM514 1761Q514 1811 540 1834T604 1857Q643 1857 669 1834T696 1761Q696 1712 670 1688T604 1663Q566 1663 540 1687T514 1761ZM898 1761Q898
+1811 924 1834T987 1857Q1025 1857 1052 1834T1079 1761Q1079 1712 1052 1688T987 1663Q950 1663 924 1687T898 1761Z" />
+<glyph unicode="&#xd7;" horiz-adv-x="1171" d="M940 1174L1034 1077L680 723L1033 369L938 272L582 624L233 272L134 369L487 723L133 1075L232 1174L584 818L940 1174Z" />
+<glyph unicode="&#xd8;" horiz-adv-x="1593" d="M1468 733Q1468 564 1425 426T1298 188T1088 34T798 -20Q680 -20 584 5T414 82L312 -62L196 14L306 170Q215 270 170 413T125 735Q125 959 199 1128T423 1391T801 1485Q908 1485 1001 1460T1169 1388L1267 1527L1382
+1448L1276 1300Q1369 1202 1418 1059T1468 733ZM1289 733Q1289 867 1261 975T1178 1160L505 212Q561 172 634 150T798 127Q968 127 1076 201T1237 411T1289 733ZM304 733Q304 603 330 498T408 315L1078 1257Q1024 1295 955 1315T801 1335Q631 1335 521 1262T358
+1055T304 733Z" />
+<glyph unicode="&#xd9;" horiz-adv-x="1493" d="M1306 1462V516Q1306 361 1244 240T1055 50T739 -20Q468 -20 327 127T185 520V1462H356V515Q356 329 454 228T749 127Q883 127 968 175T1095 311T1137 514V1462H1306ZM679 1936Q703 1890 738 1834T813 1724T886
+1632V1608H773Q737 1637 695 1677T610 1761T534 1846T481 1916V1936H679Z" />
+<glyph unicode="&#xda;" horiz-adv-x="1493" d="M1306 1462V516Q1306 361 1244 240T1055 50T739 -20Q468 -20 327 127T185 520V1462H356V515Q356 329 454 228T749 127Q883 127 968 175T1095 311T1137 514V1462H1306ZM1037 1936V1916Q1017 1887 983 1847T906 1762T822
+1677T743 1608H632V1632Q664 1670 702 1723T776 1833T836 1936H1037Z" />
+<glyph unicode="&#xdb;" horiz-adv-x="1493" d="M1306 1462V516Q1306 361 1244 240T1055 50T739 -20Q468 -20 327 127T185 520V1462H356V515Q356 329 454 228T749 127Q883 127 968 175T1095 311T1137 514V1462H1306ZM834 1935Q859 1890 904 1834T999 1723T1088
+1630V1608H970Q916 1643 859 1694T748 1800Q694 1745 639 1695T529 1608H415V1630Q453 1670 501 1724T594 1834T665 1935H834Z" />
+<glyph unicode="&#xdc;" horiz-adv-x="1493" d="M1306 1462V516Q1306 361 1244 240T1055 50T739 -20Q468 -20 327 127T185 520V1462H356V515Q356 329 454 228T749 127Q883 127 968 175T1095 311T1137 514V1462H1306ZM465 1761Q465 1811 491 1834T555 1857Q594
+1857 620 1834T647 1761Q647 1712 621 1688T555 1663Q517 1663 491 1687T465 1761ZM849 1761Q849 1811 875 1834T938 1857Q976 1857 1003 1834T1030 1761Q1030 1712 1003 1688T938 1663Q901 1663 875 1687T849 1761Z" />
+<glyph unicode="&#xdd;" horiz-adv-x="1145" d="M573 729L962 1462H1145L658 567V0H488V559L0 1462H186L573 729ZM863 1936V1916Q843 1887 809 1847T732 1762T648 1677T569 1608H458V1632Q490 1670 528 1723T602 1833T662 1936H863Z" />
+<glyph unicode="&#xde;" horiz-adv-x="1232" d="M1127 782Q1127 688 1097 605T1000 458T821 358T546 321H370V0H200V1462H370V1206H579Q869 1206 998 1096T1127 782ZM370 466H528Q672 466 765 496T905 594T951 773Q951 920 860 990T565 1061H370V466Z" />
+<glyph unicode="&#xdf;" horiz-adv-x="1275" d="M1050 1268Q1050 1197 1021 1146T950 1056T865 984T793 919T764 846Q764 815 778 791T832 737T946 657Q1018 609 1071 560T1152 452T1181 309Q1181 197 1134 124T1003 16T805 -20Q708 -20 636 -3T510 47V198Q547
+177 593 158T693 127T800 115Q916 115 967 164T1019 298Q1019 351 1001 390T940 466T826 550Q743 604 694 648T624 736T603 837Q603 902 631 946T701 1025T784 1091T854 1162T882 1258Q882 1346 810 1387T622 1428Q545 1428 481 1407T379 1334T341 1191V0H175V1191Q175
+1328 233 1410T392 1530T622 1567Q751 1567 847 1534T996 1434T1050 1268Z" />
+<glyph unicode="&#xe0;" horiz-adv-x="1138" d="M585 1114Q781 1114 876 1026T971 745V0H850L818 162H810Q764 102 714 62T599 1T438 -20Q338 -20 261 15T139 121T94 301Q94 465 224 553T620 649L809 657V724Q809 866 748 923T576 980Q490 980 412 955T264 896L213
+1022Q287 1060 383 1087T585 1114ZM807 540L640 533Q435 525 351 466T267 299Q267 205 324 160T475 115Q621 115 714 196T807 439V540ZM500 1569Q524 1523 559 1467T634 1357T707 1265V1241H594Q558 1270 516 1310T431 1394T355 1479T302 1549V1569H500Z" />
+<glyph unicode="&#xe1;" horiz-adv-x="1138" d="M585 1114Q781 1114 876 1026T971 745V0H850L818 162H810Q764 102 714 62T599 1T438 -20Q338 -20 261 15T139 121T94 301Q94 465 224 553T620 649L809 657V724Q809 866 748 923T576 980Q490 980 412 955T264 896L213
+1022Q287 1060 383 1087T585 1114ZM807 540L640 533Q435 525 351 466T267 299Q267 205 324 160T475 115Q621 115 714 196T807 439V540ZM859 1569V1549Q839 1520 805 1480T728 1395T644 1310T565 1241H454V1265Q486 1303 524 1356T598 1466T658 1569H859Z" />
+<glyph unicode="&#xe2;" horiz-adv-x="1138" d="M585 1114Q781 1114 876 1026T971 745V0H850L818 162H810Q764 102 714 62T599 1T438 -20Q338 -20 261 15T139 121T94 301Q94 465 224 553T620 649L809 657V724Q809 866 748 923T576 980Q490 980 412 955T264 896L213
+1022Q287 1060 383 1087T585 1114ZM807 540L640 533Q435 525 351 466T267 299Q267 205 324 160T475 115Q621 115 714 196T807 439V540ZM655 1568Q680 1523 725 1467T820 1356T909 1263V1241H791Q737 1276 680 1327T569 1433Q515 1378 460 1328T350 1241H236V1263Q274
+1303 322 1357T415 1467T486 1568H655Z" />
+<glyph unicode="&#xe3;" horiz-adv-x="1138" d="M585 1114Q781 1114 876 1026T971 745V0H850L818 162H810Q764 102 714 62T599 1T438 -20Q338 -20 261 15T139 121T94 301Q94 465 224 553T620 649L809 657V724Q809 866 748 923T576 980Q490 980 412 955T264 896L213
+1022Q287 1060 383 1087T585 1114ZM807 540L640 533Q435 525 351 466T267 299Q267 205 324 160T475 115Q621 115 714 196T807 439V540ZM191 1244Q197 1304 214 1351T259 1431T325 1481T411 1499Q457 1499 499 1481T579 1439T652 1397T721 1378Q769 1378 795 1407T836
+1501H934Q921 1384 865 1315T714 1245Q670 1245 629 1263T550 1305T476 1347T404 1366Q355 1366 329 1337T289 1244H191Z" />
+<glyph unicode="&#xe4;" horiz-adv-x="1138" d="M585 1114Q781 1114 876 1026T971 745V0H850L818 162H810Q764 102 714 62T599 1T438 -20Q338 -20 261 15T139 121T94 301Q94 465 224 553T620 649L809 657V724Q809 866 748 923T576 980Q490 980 412 955T264 896L213
+1022Q287 1060 383 1087T585 1114ZM807 540L640 533Q435 525 351 466T267 299Q267 205 324 160T475 115Q621 115 714 196T807 439V540ZM542 1394Q542 1444 568 1467T632 1490Q671 1490 697 1467T724 1394Q724 1345 698 1321T632 1296Q594 1296 568 1320T542 1394ZM926
+1394Q926 1444 952 1467T1015 1490Q1053 1490 1080 1467T1107 1394Q1107 1345 1080 1321T1015 1296Q978 1296 952 1320T926 1394Z" />
+<glyph unicode="&#xe5;" horiz-adv-x="1138" d="M585 1114Q781 1114 876 1026T971 745V0H850L818 162H810Q764 102 714 62T599 1T438 -20Q338 -20 261 15T139 121T94 301Q94 465 224 553T620 649L809 657V724Q809 866 748 923T576 980Q490 980 412 955T264 896L213
+1022Q287 1060 383 1087T585 1114ZM807 540L640 533Q435 525 351 466T267 299Q267 205 324 160T475 115Q621 115 714 196T807 439V540ZM569 1242Q472 1242 410 1299T348 1458Q348 1558 409 1615T569 1672Q664 1672 729 1615T794 1460Q794 1357 730 1300T569 1242ZM569
+1338Q622 1338 655 1370T689 1458Q689 1513 655 1545T569 1577Q519 1577 485 1545T450 1458Q450 1403 481 1371T569 1338Z" />
+<glyph unicode="&#xe6;" horiz-adv-x="1766" d="M1235 1116Q1368 1116 1463 1054T1610 881T1660 624V519H951Q955 317 1038 220T1277 122Q1377 122 1454 141T1612 197V51Q1530 14 1452 -3T1272 -20Q1176 -20 1097 7T957 88T856 220Q811 147 756 93T624 10T441
+-20Q341 -20 263 15T139 121T94 301Q94 410 149 485T316 602T595 649L781 657V728Q781 867 718 923T547 980Q466 980 388 956T239 896L187 1022Q261 1061 360 1087T560 1114Q689 1114 771 1069T893 923Q946 1014 1033 1065T1235 1116ZM777 540L620 533Q427 525
+347 466T267 299Q267 205 321 160T467 115Q557 115 627 151T737 259T777 439V540ZM1233 980Q1112 980 1039 896T955 650H1484Q1485 749 1458 823T1375 939T1233 980Z" />
+<glyph unicode="&#xe7;" horiz-adv-x="981" d="M614 -20Q466 -20 353 41T177 227T114 542Q114 741 180 867T364 1055T630 1116Q712 1116 788 1100T914 1058L864 919Q814 939 749 955T626 971Q512 971 437 922T324 778T286 544Q286 411 322 317T431 174T613 124Q700
+124 770 142T897 186V38Q842 10 775 -5T614 -20ZM777 -286Q777 -383 702 -437T482 -492Q451 -492 423 -489T378 -481V-376Q397 -380 427 -383T487 -386Q562 -386 602 -364T642 -290Q642 -235 589 -210T452 -176L541 0H652L598 -112Q648 -122 688 -143T753 -199T777
+-286Z" />
+<glyph unicode="&#xe8;" horiz-adv-x="1150" d="M597 1116Q737 1116 837 1054T990 881T1043 620V517H286Q289 324 382 223T644 122Q748 122 828 141T994 197V51Q911 14 830 -3T637 -20Q479 -20 362 44T179 234T114 540Q114 717 173 846T341 1046T597 1116ZM595
+980Q462 980 383 893T289 650H869Q868 748 839 822T749 938T595 980ZM514 1569Q538 1523 573 1467T648 1357T721 1265V1241H608Q572 1270 530 1310T445 1394T369 1479T316 1549V1569H514Z" />
+<glyph unicode="&#xe9;" horiz-adv-x="1150" d="M597 1116Q737 1116 837 1054T990 881T1043 620V517H286Q289 324 382 223T644 122Q748 122 828 141T994 197V51Q911 14 830 -3T637 -20Q479 -20 362 44T179 234T114 540Q114 717 173 846T341 1046T597 1116ZM595
+980Q462 980 383 893T289 650H869Q868 748 839 822T749 938T595 980ZM872 1569V1549Q852 1520 818 1480T741 1395T657 1310T578 1241H467V1265Q499 1303 537 1356T611 1466T671 1569H872Z" />
+<glyph unicode="&#xea;" horiz-adv-x="1150" d="M597 1116Q737 1116 837 1054T990 881T1043 620V517H286Q289 324 382 223T644 122Q748 122 828 141T994 197V51Q911 14 830 -3T637 -20Q479 -20 362 44T179 234T114 540Q114 717 173 846T341 1046T597 1116ZM595
+980Q462 980 383 893T289 650H869Q868 748 839 822T749 938T595 980ZM669 1568Q694 1523 739 1467T834 1356T923 1263V1241H805Q751 1276 694 1327T583 1433Q529 1378 474 1328T364 1241H250V1263Q288 1303 336 1357T429 1467T500 1568H669Z" />
+<glyph unicode="&#xeb;" horiz-adv-x="1150" d="M597 1116Q737 1116 837 1054T990 881T1043 620V517H286Q289 324 382 223T644 122Q748 122 828 141T994 197V51Q911 14 830 -3T637 -20Q479 -20 362 44T179 234T114 540Q114 717 173 846T341 1046T597 1116ZM595
+980Q462 980 383 893T289 650H869Q868 748 839 822T749 938T595 980ZM556 1394Q556 1444 582 1467T646 1490Q685 1490 711 1467T738 1394Q738 1345 712 1321T646 1296Q608 1296 582 1320T556 1394ZM940 1394Q940 1444 966 1467T1029 1490Q1067 1490 1094 1467T1121
+1394Q1121 1345 1094 1321T1029 1296Q992 1296 966 1320T940 1394Z" />
+<glyph unicode="&#xec;" horiz-adv-x="517" d="M341 0H175V1096H341V0ZM446 1569Q470 1523 505 1467T580 1357T653 1265V1241H540Q504 1270 462 1310T377 1394T301 1479T248 1549V1569H446Z" />
+<glyph unicode="&#xed;" horiz-adv-x="517" d="M341 0H175V1096H341V0ZM548 1569V1549Q528 1520 494 1480T417 1395T333 1310T254 1241H143V1265Q175 1303 213 1356T287 1466T347 1569H548Z" />
+<glyph unicode="&#xee;" horiz-adv-x="517" d="M341 0H175V1096H341V0ZM344 1568Q369 1523 414 1467T509 1356T598 1263V1241H480Q426 1276 369 1327T258 1433Q204 1378 149 1328T39 1241H-75V1263Q-37 1303 11 1357T104 1467T175 1568H344Z" />
+<glyph unicode="&#xef;" horiz-adv-x="517" d="M341 0H175V1096H341V0ZM-25 1394Q-25 1444 1 1467T65 1490Q104 1490 130 1467T157 1394Q157 1345 131 1321T65 1296Q27 1296 1 1320T-25 1394ZM359 1394Q359 1444 385 1467T448 1490Q486 1490 513 1467T540 1394Q540
+1345 513 1321T448 1296Q411 1296 385 1320T359 1394Z" />
+<glyph unicode="&#xf0;" horiz-adv-x="1228" d="M439 1565Q507 1534 572 1497T695 1417L930 1554L1002 1452L798 1333Q893 1244 964 1128T1075 869T1115 562Q1115 372 1055 242T882 46T610 -20Q464 -20 352 40T176 211T113 475Q113 628 172 738T338 907T591 967Q666
+967 727 954T835 913T915 845L924 848Q892 969 824 1072T666 1256L399 1102L328 1206L559 1339Q516 1369 468 1397T371 1451L439 1565ZM616 832Q501 832 427 790T318 667T282 469Q282 365 317 285T426 161T614 116Q785 116 866 220T947 522Q947 584 928 639T868
+738T766 807T616 832Z" />
+<glyph unicode="&#xf1;" horiz-adv-x="1256" d="M694 1116Q889 1116 989 1021T1089 714V0H925V703Q925 840 863 908T671 976Q489 976 415 873T341 574V0H175V1096H309L334 938H343Q378 996 432 1035T553 1095T694 1116ZM254 1244Q260 1304 277 1351T322 1431T388
+1481T474 1499Q520 1499 562 1481T642 1439T715 1397T784 1378Q832 1378 858 1407T899 1501H997Q984 1384 928 1315T777 1245Q733 1245 692 1263T613 1305T539 1347T467 1366Q418 1366 392 1337T352 1244H254Z" />
+<glyph unicode="&#xf2;" horiz-adv-x="1232" d="M1120 550Q1120 415 1085 309T984 130T825 19T613 -20Q503 -20 412 18T254 130T151 309T114 550Q114 730 175 856T349 1049T620 1116Q770 1116 882 1049T1057 856T1120 550ZM286 550Q286 418 321 321T429 171T617
+118Q731 118 804 171T913 321T948 550Q948 681 913 776T805 924T616 976Q445 976 366 863T286 550ZM548 1569Q572 1523 607 1467T682 1357T755 1265V1241H642Q606 1270 564 1310T479 1394T403 1479T350 1549V1569H548Z" />
+<glyph unicode="&#xf3;" horiz-adv-x="1232" d="M1120 550Q1120 415 1085 309T984 130T825 19T613 -20Q503 -20 412 18T254 130T151 309T114 550Q114 730 175 856T349 1049T620 1116Q770 1116 882 1049T1057 856T1120 550ZM286 550Q286 418 321 321T429 171T617
+118Q731 118 804 171T913 321T948 550Q948 681 913 776T805 924T616 976Q445 976 366 863T286 550ZM907 1569V1549Q887 1520 853 1480T776 1395T692 1310T613 1241H502V1265Q534 1303 572 1356T646 1466T706 1569H907Z" />
+<glyph unicode="&#xf4;" horiz-adv-x="1232" d="M1120 550Q1120 415 1085 309T984 130T825 19T613 -20Q503 -20 412 18T254 130T151 309T114 550Q114 730 175 856T349 1049T620 1116Q770 1116 882 1049T1057 856T1120 550ZM286 550Q286 418 321 321T429 171T617
+118Q731 118 804 171T913 321T948 550Q948 681 913 776T805 924T616 976Q445 976 366 863T286 550ZM703 1568Q728 1523 773 1467T868 1356T957 1263V1241H839Q785 1276 728 1327T617 1433Q563 1378 508 1328T398 1241H284V1263Q322 1303 370 1357T463 1467T534
+1568H703Z" />
+<glyph unicode="&#xf5;" horiz-adv-x="1232" d="M1120 550Q1120 415 1085 309T984 130T825 19T613 -20Q503 -20 412 18T254 130T151 309T114 550Q114 730 175 856T349 1049T620 1116Q770 1116 882 1049T1057 856T1120 550ZM286 550Q286 418 321 321T429 171T617
+118Q731 118 804 171T913 321T948 550Q948 681 913 776T805 924T616 976Q445 976 366 863T286 550ZM240 1244Q246 1304 263 1351T308 1431T374 1481T460 1499Q506 1499 548 1481T628 1439T701 1397T770 1378Q818 1378 844 1407T885 1501H983Q970 1384 914 1315T763
+1245Q719 1245 678 1263T599 1305T525 1347T453 1366Q404 1366 378 1337T338 1244H240Z" />
+<glyph unicode="&#xf6;" horiz-adv-x="1232" d="M1120 550Q1120 415 1085 309T984 130T825 19T613 -20Q503 -20 412 18T254 130T151 309T114 550Q114 730 175 856T349 1049T620 1116Q770 1116 882 1049T1057 856T1120 550ZM286 550Q286 418 321 321T429 171T617
+118Q731 118 804 171T913 321T948 550Q948 681 913 776T805 924T616 976Q445 976 366 863T286 550ZM334 1394Q334 1444 360 1467T424 1490Q463 1490 489 1467T516 1394Q516 1345 490 1321T424 1296Q386 1296 360 1320T334 1394ZM718 1394Q718 1444 744 1467T807
+1490Q845 1490 872 1467T899 1394Q899 1345 872 1321T807 1296Q770 1296 744 1320T718 1394Z" />
+<glyph unicode="&#xf7;" horiz-adv-x="1171" d="M103 654V790H1066V654H103ZM584 253Q538 253 507 281T476 371Q476 436 507 462T584 488Q628 488 659 462T690 371Q690 310 659 282T584 253ZM584 955Q538 955 507 983T476 1073Q476 1137 507 1163T584 1189Q628
+1189 659 1163T690 1073Q690 1012 659 984T584 955Z" />
+<glyph unicode="&#xf8;" horiz-adv-x="1232" d="M1120 550Q1120 370 1059 243T884 48T613 -20Q534 -20 467 -2T342 54L254 -67L145 8L242 140Q181 215 148 317T114 550Q114 820 249 968T620 1116Q698 1116 767 1096T892 1038L977 1157L1089 1084L992 952Q1052
+879 1086 778T1120 550ZM286 550Q286 468 298 400T339 279L807 919Q770 946 722 961T616 976Q445 976 366 863T286 550ZM948 550Q948 629 935 696T896 814L428 173Q463 146 511 132T617 118Q731 118 804 171T913 321T948 550Z" />
+<glyph unicode="&#xf9;" horiz-adv-x="1256" d="M1080 1096V0H944L920 154H911Q877 97 823 58T702 0T558 -20Q428 -20 340 22T208 152T163 378V1096H331V390Q331 253 393 186T582 118Q704 118 776 164T881 299T913 519V1096H1080ZM560 1569Q584 1523 619 1467T694
+1357T767 1265V1241H654Q618 1270 576 1310T491 1394T415 1479T362 1549V1569H560Z" />
+<glyph unicode="&#xfa;" horiz-adv-x="1256" d="M1080 1096V0H944L920 154H911Q877 97 823 58T702 0T558 -20Q428 -20 340 22T208 152T163 378V1096H331V390Q331 253 393 186T582 118Q704 118 776 164T881 299T913 519V1096H1080ZM918 1569V1549Q898 1520 864
+1480T787 1395T703 1310T624 1241H513V1265Q545 1303 583 1356T657 1466T717 1569H918Z" />
+<glyph unicode="&#xfb;" horiz-adv-x="1256" d="M1080 1096V0H944L920 154H911Q877 97 823 58T702 0T558 -20Q428 -20 340 22T208 152T163 378V1096H331V390Q331 253 393 186T582 118Q704 118 776 164T881 299T913 519V1096H1080ZM714 1568Q739 1523 784 1467T879
+1356T968 1263V1241H850Q796 1276 739 1327T628 1433Q574 1378 519 1328T409 1241H295V1263Q333 1303 381 1357T474 1467T545 1568H714Z" />
+<glyph unicode="&#xfc;" horiz-adv-x="1256" d="M1080 1096V0H944L920 154H911Q877 97 823 58T702 0T558 -20Q428 -20 340 22T208 152T163 378V1096H331V390Q331 253 393 186T582 118Q704 118 776 164T881 299T913 519V1096H1080ZM345 1394Q345 1444 371 1467T435
+1490Q474 1490 500 1467T527 1394Q527 1345 501 1321T435 1296Q397 1296 371 1320T345 1394ZM729 1394Q729 1444 755 1467T818 1490Q856 1490 883 1467T910 1394Q910 1345 883 1321T818 1296Q781 1296 755 1320T729 1394Z" />
+<glyph unicode="&#xfd;" horiz-adv-x="1026" d="M2 1096H180L422 460Q443 404 461 353T493 254T515 163H522Q536 213 562 294T618 461L847 1096H1026L549 -161Q511 -262 461 -337T338 -452T164 -493Q117 -493 81 -488T19 -475V-342Q41 -347 72 -351T138 -355Q200
+-355 245 -332T324 -263T381 -156L441 -2L2 1096ZM802 1569V1549Q782 1520 748 1480T671 1395T587 1310T508 1241H397V1265Q429 1303 467 1356T541 1466T601 1569H802Z" />
+<glyph unicode="&#xfe;" horiz-adv-x="1253" d="M1139 551Q1139 364 1083 237T926 45T688 -20Q600 -20 534 3T421 64T342 150H330Q332 132 335 98T340 27T342 -33V-490H175V1556H342V1095Q342 1064 340 1015T336 937H343Q375 986 421 1026T534 1091T690 1116Q895
+1116 1017 973T1139 551ZM968 553Q968 765 894 870T663 976Q491 976 418 878T342 585V549Q342 341 413 230T663 118Q766 118 833 168T934 316T968 553Z" />
+<glyph unicode="&#xff;" horiz-adv-x="1026" d="M2 1096H180L422 460Q443 404 461 353T493 254T515 163H522Q536 213 562 294T618 461L847 1096H1026L549 -161Q511 -262 461 -337T338 -452T164 -493Q117 -493 81 -488T19 -475V-342Q41 -347 72 -351T138 -355Q200
+-355 245 -332T324 -263T381 -156L441 -2L2 1096ZM485 1394Q485 1444 511 1467T575 1490Q614 1490 640 1467T667 1394Q667 1345 641 1321T575 1296Q537 1296 511 1320T485 1394ZM869 1394Q869 1444 895 1467T958 1490Q996 1490 1023 1467T1050 1394Q1050 1345 1023
+1321T958 1296Q921 1296 895 1320T869 1394Z" />
+<glyph unicode="&#x2013;" horiz-adv-x="1024" d="M82 476V624H942V476H82Z" />
+<glyph unicode="&#x2014;" horiz-adv-x="2048" d="M82 476V624H1966V476H82Z" />
+<glyph unicode="&#x2018;" horiz-adv-x="347" d="M39 961L27 983Q45 1056 73 1139T134 1306T200 1462H322Q302 1385 282 1296T244 1120T216 961H39Z" />
+<glyph unicode="&#x2019;" horiz-adv-x="347" d="M306 1462L321 1440Q303 1367 275 1284T214 1117T148 961H26Q41 1018 56 1083T86 1215T111 1345T131 1462H306Z" />
+<glyph unicode="&#x201a;" horiz-adv-x="501" d="M345 237L360 215Q342 142 314 59T253 -108T187 -264H65Q80 -207 95 -142T125 -10T150 120T170 237H345Z" />
+<glyph unicode="&#x201c;" horiz-adv-x="714" d="M689 1462Q668 1385 648 1296T610 1119T583 961H406L391 983Q409 1056 437 1139T499 1305T567 1462H689ZM321 1462Q300 1385 280 1296T242 1119T215 961H39L27 983Q45 1056 72 1139T133 1305T200 1462H321Z" />
+<glyph unicode="&#x201d;" horiz-adv-x="714" d="M673 1462L688 1440Q670 1366 642 1283T581 1117T515 961H390Q411 1037 432 1126T470 1303T497 1462H673ZM305 1462L319 1440Q302 1366 274 1283T212 1117T146 961H26Q41 1018 56 1083T85 1215T110 1345T129 1462H305Z" />
+<glyph unicode="&#x201e;" horiz-adv-x="837" d="M712 237L727 215Q709 141 681 58T620 -108T554 -264H429Q450 -188 471 -99T509 78T536 237H712ZM344 237L358 215Q341 141 313 58T251 -108T185 -264H65Q80 -207 95 -142T124 -10T149 120T168 237H344Z" />
+<glyph unicode="&#x2022;" horiz-adv-x="770" d="M171 748Q171 835 199 887T275 964T385 988Q446 988 494 964T571 887T599 748Q599 664 571 611T495 532T385 507Q324 507 276 532T199 610T171 748Z" />
+<glyph unicode="&#x2039;" horiz-adv-x="615" d="M79 556L419 965L538 897L251 544L538 191L419 122L79 529V556Z" />
+<glyph unicode="&#x203a;" horiz-adv-x="615" d="M194 965L536 557V530L194 122L77 191L363 545L77 897L194 965Z" />
+</font>
+</defs>
+</svg>
diff --git a/public/css/font/OpenSans-Regular.ttf b/public/css/font/OpenSans-Regular.ttf  Binary files differ.
diff --git a/public/css/font/OpenSans-Regular.woff b/public/css/font/OpenSans-Regular.woff  Binary files differ.
diff --git a/public/css/font/OpenSans-Regular.woff2 b/public/css/font/OpenSans-Regular.woff2  Binary files differ.
diff --git a/public/css/font/OpenSans-SemiBold.eot b/public/css/font/OpenSans-SemiBold.eot  Binary files differ.
diff --git a/public/css/font/OpenSans-SemiBold.svg b/public/css/font/OpenSans-SemiBold.svg
@@ -0,0 +1,348 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg">
+<defs >
+<font id="OpenSans" horiz-adv-x="1169" ><font-face
+    font-family="Open Sans SemiBold"
+    units-per-em="2048"
+    panose-1="0 0 0 0 0 0 0 0 0 0"
+    ascent="2189"
+    descent="-600"
+    alphabetic="0" />
+<glyph unicode=" " horiz-adv-x="532" />
+<glyph unicode="!" horiz-adv-x="564" d="M371 446H194L146 1462H419L371 446ZM134 124Q134 206 176 239T281 273Q342 273 385 240T428 124Q428 44 385 9T281 -27Q219 -27 177 8T134 124Z" />
+<glyph unicode="&quot;" horiz-adv-x="892" d="M364 1462L324 934H173L134 1462H364ZM758 1462L719 934H568L529 1462H758Z" />
+<glyph unicode="#" horiz-adv-x="1323" d="M990 872L934 586H1204V419H902L821 0H643L725 419H475L396 0H222L299 419H49V586H331L388 872H123V1039H418L498 1461H676L596 1039H848L929 1461H1102L1022 1039H1274V872H990ZM507 586H757L814 872H563L507 586Z" />
+<glyph unicode="$" horiz-adv-x="1171" d="M518 -119V87Q394 89 290 109T109 163V376Q186 340 299 311T518 278V653Q373 702 283 755T150 878T108 1048Q108 1150 159 1223T303 1340T518 1391V1554H648V1394Q759 1391 853 1370T1034 1310L961 1124Q886 1154 805
+1173T648 1199V833Q770 793 865 747T1015 632T1070 450Q1070 303 962 208T648 93V-119H518ZM648 286Q744 298 791 336T838 437Q838 479 819 508T758 560T648 605V286ZM518 882V1195Q460 1189 420 1171T360 1124T340 1055Q340 1011 358 980T415 926T518 882Z" />
+<glyph unicode="%" horiz-adv-x="1769" d="M404 1483Q565 1483 648 1364T731 1026Q731 809 652 687T404 565Q247 565 165 687T83 1026Q83 1244 160 1363T404 1483ZM405 1319Q340 1319 310 1246T279 1025Q279 878 309 804T405 729Q471 729 503 803T536 1025Q536
+1172 504 1245T405 1319ZM1386 1462L575 0H382L1193 1462H1386ZM1359 898Q1520 898 1603 779T1687 441Q1687 225 1608 103T1359 -19Q1202 -19 1120 103T1038 441Q1038 659 1115 778T1359 898ZM1360 734Q1295 734 1265 661T1234 440Q1234 293 1264 219T1360 144Q1427
+144 1459 217T1491 440Q1491 587 1459 660T1360 734Z" />
+<glyph unicode="&amp;" horiz-adv-x="1514" d="M629 1484Q742 1484 827 1447T961 1340T1009 1168Q1009 1033 925 940T713 774L1058 438Q1107 505 1139 585T1194 755H1437Q1406 631 1351 511T1212 290L1509 0H1208L1060 145Q998 95 928 58T774 1T591 -20Q438 -20
+327 29T157 169T97 387Q97 492 132 567T234 700T392 810Q342 867 305 921T249 1032T229 1156Q229 1258 279 1331T419 1444T629 1484ZM531 667Q473 630 431 593T367 510T344 405Q344 302 416 242T604 182Q705 182 781 212T915 289L531 667ZM625 1298Q554 1298 503
+1261T451 1146Q451 1083 484 1028T578 908Q683 966 734 1022T786 1152Q786 1222 739 1260T625 1298Z" />
+<glyph unicode="&apos;" horiz-adv-x="497" d="M364 1462L324 934H173L134 1462H364Z" />
+<glyph unicode="(" horiz-adv-x="649" d="M82 561Q82 729 114 889T214 1194T383 1462H588Q445 1271 373 1039T300 563Q300 404 332 248T428 -52T586 -324H383Q281 -204 214 -62T115 238T82 561Z" />
+<glyph unicode=")" horiz-adv-x="649" d="M567 562Q567 396 535 238T437 -62T266 -324H64Q158 -197 222 -52T318 249T350 564Q350 726 318 884T221 1188T62 1462H266Q370 1339 436 1194T535 890T567 562Z" />
+<glyph unicode="*" horiz-adv-x="1122" d="M670 1556L630 1174L1014 1283L1044 1070L685 1038L918 727L724 622L555 957L404 623L202 727L433 1038L76 1071L110 1283L487 1174L447 1556H670Z" />
+<glyph unicode="+" horiz-adv-x="1171" d="M673 811H1073V633H673V229H495V633H96V811H495V1219H673V811Z" />
+<glyph unicode="," horiz-adv-x="557" d="M405 238L419 215Q401 143 373 60T312 -106T245 -264H73Q94 -183 113 -95T149 80T176 238H405Z" />
+<glyph unicode="-" horiz-adv-x="659" d="M72 450V649H588V450H72Z" />
+<glyph unicode="." horiz-adv-x="561" d="M134 124Q134 206 176 239T280 273Q341 273 384 240T428 124Q428 44 385 9T280 -27Q219 -27 177 8T134 124Z" />
+<glyph unicode="/" horiz-adv-x="799" d="M783 1462L238 0H18L563 1462H783Z" />
+<glyph unicode="0" horiz-adv-x="1171" d="M1082 732Q1082 555 1055 416T968 179T814 31T584 -20Q415 -20 305 69T142 327T89 732Q89 968 137 1136T295 1395T584 1485Q754 1485 864 1396T1028 1138T1082 732ZM326 732Q326 547 350 423T432 238T584 176Q679 176
+736 237T818 422T844 732Q844 916 819 1040T737 1226T584 1289Q488 1289 432 1227T351 1040T326 732Z" />
+<glyph unicode="1" horiz-adv-x="1171" d="M783 0H547V941Q547 988 548 1035T550 1127T555 1213Q534 1190 504 1163T439 1107L271 973L153 1122L587 1462H783V0Z" />
+<glyph unicode="2" horiz-adv-x="1171" d="M1082 0H92V177L471 561Q581 673 652 754T757 909T792 1068Q792 1174 731 1228T570 1283Q475 1283 395 1246T228 1139L99 1294Q160 1346 229 1389T385 1457T581 1483Q720 1483 821 1434T976 1296T1031 1091Q1031 973
+984 873T850 676T643 464L390 217V206H1082V0Z" />
+<glyph unicode="3" horiz-adv-x="1171" d="M1026 1128Q1026 1029 986 956T877 837T716 770V763Q891 741 980 653T1070 420Q1070 293 1009 194T822 37T498 -20Q379 -20 278 -1T85 60V269Q178 223 283 198T482 173Q665 173 743 241T821 431Q821 509 781 559T656
+634T438 659H309V848H439Q566 848 643 878T754 963T789 1089Q789 1183 728 1235T547 1288Q473 1288 413 1271T301 1228T204 1172L91 1335Q174 1397 289 1440T557 1483Q782 1483 904 1387T1026 1128Z" />
+<glyph unicode="4" horiz-adv-x="1171" d="M1135 321H937V0H705V321H40V499L708 1466H937V516H1135V321ZM705 516V879Q705 924 706 970T710 1060T714 1139T717 1200H709Q690 1160 667 1119T618 1038L258 516H705Z" />
+<glyph unicode="5" horiz-adv-x="1171" d="M589 914Q730 914 837 864T1005 716T1065 477Q1065 323 1000 212T809 40T502 -20Q387 -20 288 0T116 60V272Q191 230 296 204T497 177Q599 177 672 207T784 299T824 456Q824 582 744 650T492 719Q432 719 365 709T252
+686L149 747L204 1462H964V1255H411L379 891Q415 899 465 906T589 914Z" />
+<glyph unicode="6" horiz-adv-x="1171" d="M94 623Q94 752 112 877T176 1111T300 1303T501 1433T792 1481Q836 1481 891 1477T982 1464V1269Q943 1280 897 1286T804 1292Q620 1292 517 1223T370 1033T319 757H331Q361 807 406 846T516 908T667 932Q795 932 890
+879T1036 724T1088 479Q1088 324 1030 212T864 40T608 -20Q499 -20 406 20T242 140T133 340T94 623ZM604 174Q718 174 787 248T857 477Q857 602 796 675T611 748Q527 748 464 712T367 621T332 509Q332 451 349 392T400 284T485 204T604 174Z" />
+<glyph unicode="7" horiz-adv-x="1171" d="M259 0L833 1256H74V1462H1086V1301L512 0H259Z" />
+<glyph unicode="8" horiz-adv-x="1171" d="M585 1482Q712 1482 814 1443T976 1325T1036 1132Q1036 1043 1000 977T901 862T762 774Q848 732 920 678T1037 550T1082 379Q1082 256 1020 167T846 29T587 -20Q428 -20 316 27T146 161T88 371Q88 472 128 546T235 674T384
+765Q315 806 258 857T168 976T134 1133Q134 1247 195 1324T358 1442T585 1482ZM312 380Q312 284 380 222T583 160Q717 160 787 221T857 382Q857 446 822 495T727 582T595 654L563 667Q485 634 429 593T343 499T312 380ZM583 1301Q486 1301 424 1254T361 1117Q361
+1054 391 1009T474 932T589 871Q650 898 699 930T778 1008T808 1118Q808 1206 746 1253T583 1301Z" />
+<glyph unicode="9" horiz-adv-x="1171" d="M1080 839Q1080 709 1062 584T998 350T873 158T672 27T380 -20Q337 -20 281 -16T188 -3V193Q226 182 273 176T368 169Q553 169 656 238T803 428T853 703H841Q812 655 768 616T659 553T501 529Q375 529 282 582T137 736T85
+981Q85 1136 144 1248T311 1420T565 1481Q675 1481 768 1442T932 1322T1041 1121T1080 839ZM568 1287Q457 1287 387 1213T317 984Q317 859 377 786T561 713Q647 713 709 748T806 839T841 951Q841 1010 825 1069T774 1177T689 1256T568 1287Z" />
+<glyph unicode=":" horiz-adv-x="561" d="M134 124Q134 206 176 239T280 273Q341 273 384 240T428 124Q428 44 385 9T280 -27Q219 -27 177 8T134 124ZM134 980Q134 1063 176 1097T280 1131Q341 1131 384 1097T428 980Q428 901 385 866T280 830Q219 830 177 865T134 980Z" />
+<glyph unicode=";" horiz-adv-x="561" d="M396 238L411 215Q393 143 365 60T303 -106T237 -264H64Q85 -184 104 -95T141 80T168 238H396ZM132 980Q132 1063 174 1097T278 1131Q340 1131 383 1097T426 980Q426 901 383 866T278 830Q217 830 175 865T132 980Z" />
+<glyph unicode="&lt;" horiz-adv-x="1171" d="M1073 223L96 655V774L1073 1260V1066L340 723L1073 416V223Z" />
+<glyph unicode="=" horiz-adv-x="1171" d="M102 833V1009H1067V833H102ZM102 434V611H1067V434H102Z" />
+<glyph unicode="&gt;" horiz-adv-x="1171" d="M96 416L829 722L96 1066V1260L1073 774V655L96 223V416Z" />
+<glyph unicode="?" horiz-adv-x="931" d="M282 446V509Q282 583 299 638T356 741T464 843Q534 898 575 938T635 1018T654 1113Q654 1198 597 1242T435 1287Q343 1287 262 1261T102 1196L19 1372Q110 1422 216 1452T451 1483Q655 1483 766 1384T878 1123Q878 1034
+850 971T768 854T636 741Q573 691 540 655T494 581T481 492V446H282ZM244 124Q244 206 286 239T391 273Q451 273 494 240T537 124Q537 44 494 9T391 -27Q329 -27 287 8T244 124Z" />
+<glyph unicode="@" horiz-adv-x="1837" d="M1727 732Q1727 637 1705 546T1638 381T1526 263T1366 219Q1282 219 1226 264T1153 375H1140Q1100 310 1030 265T857 219Q687 219 596 325T504 607Q504 743 560 847T719 1012T963 1072Q1056 1072 1149 1056T1297 1020L1277
+609Q1275 580 1275 560T1274 531Q1274 434 1305 402T1378 369Q1434 369 1472 418T1531 550T1551 733Q1551 914 1478 1042T1276 1237T984 1304Q816 1304 687 1251T469 1102T335 875T290 585Q290 391 359 256T562 50T891 -20Q1012 -20 1132 6T1351 70V-91Q1258 -130
+1143 -154T895 -178Q648 -178 473 -88T204 172T110 579Q110 769 170 929T345 1209T620 1395T983 1461Q1200 1461 1368 1373T1631 1121T1727 732ZM694 603Q694 481 744 425T880 369Q990 369 1038 450T1094 663L1106 904Q1080 911 1045 915T970 920Q870 920 810 875T722
+757T694 603Z" />
+<glyph unicode="A" horiz-adv-x="1354" d="M1098 0L955 406H396L253 0H0L544 1468H810L1353 0H1098ZM893 612L754 1012Q746 1038 731 1085T701 1182T676 1266Q666 1225 652 1175T624 1080T603 1012L463 612H893Z" />
+<glyph unicode="B" horiz-adv-x="1350" d="M192 1462H627Q910 1462 1054 1380T1198 1097Q1198 1013 1169 946T1083 835T945 776V766Q1030 751 1096 713T1201 604T1240 420Q1240 286 1177 192T996 49T717 0H192V1462ZM432 859H662Q825 859 888 912T951 1067Q951
+1171 877 1217T640 1263H432V859ZM432 665V201H685Q853 201 921 266T989 442Q989 510 959 560T859 637T673 665H432Z" />
+<glyph unicode="C" horiz-adv-x="1298" d="M815 1279Q710 1279 628 1241T488 1132T401 959T371 730Q371 560 419 438T565 250T813 184Q906 184 994 202T1176 251V47Q1087 12 996 -4T786 -20Q562 -20 415 73T195 335T122 731Q122 897 168 1035T302 1273T519 1428T816
+1483Q926 1483 1032 1459T1230 1389L1146 1191Q1070 1227 987 1253T815 1279Z" />
+<glyph unicode="D" horiz-adv-x="1501" d="M1379 745Q1379 498 1287 333T1020 84T598 0H192V1462H642Q869 1462 1034 1381T1289 1140T1379 745ZM1129 738Q1129 918 1073 1034T907 1206T637 1263H432V201H602Q867 201 998 336T1129 738Z" />
+<glyph unicode="E" horiz-adv-x="1143" d="M1020 0H192V1462H1020V1260H432V863H983V662H432V203H1020V0Z" />
+<glyph unicode="F" horiz-adv-x="1091" d="M430 0H192V1462H1018V1260H430V804H980V603H430V0Z" />
+<glyph unicode="G" horiz-adv-x="1486" d="M782 794H1328V60Q1213 22 1090 1T814 -20Q591 -20 437 68T202 325T122 732Q122 961 211 1129T470 1390T882 1483Q1001 1483 1112 1460T1317 1395L1233 1197Q1159 1232 1067 1256T876 1280Q719 1280 606 1212T431 1021T370
+729Q370 566 419 443T572 252T843 183Q926 183 984 192T1091 212V589H782V794Z" />
+<glyph unicode="H" horiz-adv-x="1539" d="M1345 0H1106V660H432V0H192V1462H432V864H1106V1462H1345V0Z" />
+<glyph unicode="I" horiz-adv-x="625" d="M192 0V1462H432V0H192Z" />
+<glyph unicode="J" horiz-adv-x="614" d="M10 -407Q-45 -407 -86 -401T-158 -383V-182Q-126 -190 -89 -196T-10 -202Q45 -202 90 -181T161 -105T188 50V1462H428V58Q428 -105 376 -207T230 -358T10 -407Z" />
+<glyph unicode="K" horiz-adv-x="1307" d="M1307 0H1031L576 664L432 547V0H192V1462H432V763Q476 817 521 871T611 979L1023 1462H1294L748 822L1307 0Z" />
+<glyph unicode="L" horiz-adv-x="1113" d="M192 0V1462H432V204H1053V0H192Z" />
+<glyph unicode="M" horiz-adv-x="1887" d="M823 0L402 1221H393Q396 1180 400 1109T407 957T410 802V0H192V1462H529L934 295H940L1359 1462H1695V0H1466V814Q1466 881 1468 959T1474 1107T1480 1219H1472L1035 0H823Z" />
+<glyph unicode="N" horiz-adv-x="1604" d="M1412 0H1117L401 1167H392Q396 1108 399 1042T405 906T410 764V0H192V1462H485L1200 303H1207Q1205 354 1202 420T1197 556T1193 693V1462H1412V0Z" />
+<glyph unicode="O" horiz-adv-x="1612" d="M1490 733Q1490 564 1448 426T1320 188T1107 34T807 -20Q632 -20 504 34T291 188T164 427T122 735Q122 961 196 1129T423 1391T809 1485Q1037 1485 1188 1392T1414 1131T1490 733ZM374 733Q374 564 420 441T561 251T807
+184Q958 184 1053 250T1193 440T1238 733Q1238 990 1136 1135T809 1281Q659 1281 563 1215T420 1027T374 733Z" />
+<glyph unicode="P" horiz-adv-x="1259" d="M617 1462Q900 1462 1030 1346T1160 1021Q1160 926 1131 841T1034 690T858 586T588 548H432V0H192V1462H617ZM601 1263H432V748H563Q676 748 755 774T875 859T916 1012Q916 1139 840 1201T601 1263Z" />
+<glyph unicode="Q" horiz-adv-x="1612" d="M1490 733Q1490 562 1446 421T1314 180T1092 29L1440 -348H1120L845 -19Q835 -19 826 -19T807 -20Q632 -20 504 34T291 188T164 427T122 735Q122 961 196 1129T423 1391T809 1485Q1037 1485 1188 1392T1414 1131T1490
+733ZM374 733Q374 564 420 441T561 251T807 184Q958 184 1053 250T1193 440T1238 733Q1238 990 1136 1135T809 1281Q659 1281 563 1215T420 1027T374 733Z" />
+<glyph unicode="R" horiz-adv-x="1308" d="M603 1462Q792 1462 915 1416T1099 1276T1160 1037Q1160 929 1120 853T1015 727T876 647L1291 0H1020L667 586H432V0H192V1462H603ZM587 1262H432V784H598Q765 784 840 846T916 1029Q916 1156 836 1209T587 1262Z" />
+<glyph unicode="S" horiz-adv-x="1126" d="M1036 398Q1036 268 973 174T791 30T508 -20Q428 -20 355 -11T218 15T100 58V284Q189 245 299 214T523 182Q616 182 677 206T769 276T799 383Q799 448 764 493T659 575T482 656Q410 685 345 722T228 809T148 927T118
+1089Q118 1213 177 1301T345 1436T599 1483Q714 1483 815 1459T1015 1391L939 1198Q849 1235 765 1257T592 1279Q516 1279 463 1256T383 1192T355 1093Q355 1029 386 986T484 906T656 826Q777 776 861 721T991 590T1036 398Z" />
+<glyph unicode="T" horiz-adv-x="1157" d="M699 0H458V1258H30V1462H1126V1258H699V0Z" />
+<glyph unicode="U" horiz-adv-x="1521" d="M1340 1462V516Q1340 363 1276 242T1081 51T754 -20Q473 -20 327 127T180 520V1462H420V541Q420 358 506 271T763 184Q881 184 955 225T1065 347T1101 542V1462H1340Z" />
+<glyph unicode="V" horiz-adv-x="1276" d="M1276 1462L765 0H511L0 1462H246L554 546Q568 506 584 449T615 334T638 231Q646 276 660 334T691 449T721 547L1030 1462H1276Z" />
+<glyph unicode="W" horiz-adv-x="1936" d="M1921 1462L1539 0H1277L1033 876Q1024 908 1013 952T992 1042T974 1126T963 1186Q961 1165 954 1127T938 1044T917 954T897 875L657 0H396L15 1462H256L467 605Q477 564 487 517T507 421T525 328T538 245Q543 283 551
+329T568 424T588 518T610 599L848 1462H1082L1325 596Q1336 559 1346 513T1367 419T1385 326T1398 245Q1405 294 1416 357T1441 485T1469 605L1679 1462H1921Z" />
+<glyph unicode="X" horiz-adv-x="1275" d="M1271 0H998L630 599L260 0H3L489 758L36 1462H301L641 908L979 1462H1236L781 751L1271 0Z" />
+<glyph unicode="Y" horiz-adv-x="1212" d="M606 795L953 1462H1212L726 568V0H487V559L0 1462H261L606 795Z" />
+<glyph unicode="Z" horiz-adv-x="1179" d="M1115 0H64V165L808 1258H87V1462H1095V1298L351 204H1115V0Z" />
+<glyph unicode="[" horiz-adv-x="674" d="M623 -324H155V1462H623V1289H366V-149H623V-324Z" />
+<glyph unicode="\" horiz-adv-x="799" d="M237 1462L783 0H562L17 1462H237Z" />
+<glyph unicode="]" horiz-adv-x="674" d="M51 -149H308V1289H51V1462H520V-324H51V-149Z" />
+<glyph unicode="^" horiz-adv-x="1171" d="M64 535L502 1472H622L1108 535H914L566 1229L256 535H64Z" />
+<glyph unicode="_" horiz-adv-x="870" d="M874 -315H-4V-184H874V-315Z" />
+<glyph unicode="`" horiz-adv-x="655" d="M352 1569Q379 1523 418 1467T499 1358T573 1267V1241H416Q378 1269 331 1309T236 1394T147 1479T82 1549V1569H352Z" />
+<glyph unicode="a" horiz-adv-x="1188" d="M602 1128Q812 1128 919 1035T1027 745V0H860L815 157H807Q760 97 710 58T595 0T435 -20Q336 -20 258 16T135 128T90 318Q90 489 217 575T603 670L795 677V735Q795 850 742 899T591 948Q508 948 430 924T278 865L202
+1031Q283 1074 386 1101T602 1128ZM794 529L651 524Q475 518 404 464T333 316Q333 234 382 197T511 159Q633 159 713 228T794 433V529Z" />
+<glyph unicode="b" horiz-adv-x="1275" d="M403 1556V1181Q403 1116 400 1054T393 956H403Q448 1028 528 1077T735 1127Q932 1127 1052 983T1172 555Q1172 367 1117 239T963 46T729 -20Q603 -20 526 26T404 135H387L344 0H168V1556H403ZM673 936Q573 936 515 896T430
+777T403 575V554Q403 369 461 271T675 173Q798 173 864 272T930 558Q930 746 865 841T673 936Z" />
+<glyph unicode="c" horiz-adv-x="1017" d="M614 -20Q457 -20 342 41T165 228T103 548Q103 750 171 878T359 1067T636 1128Q735 1128 815 1109T952 1061L882 873Q821 898 757 915T634 932Q537 932 473 889T377 761T345 550Q345 428 377 345T472 219T627 176Q718
+176 790 198T926 255V51Q863 15 791 -2T614 -20Z" />
+<glyph unicode="d" horiz-adv-x="1275" d="M540 -20Q342 -20 223 124T103 551Q103 836 224 982T545 1128Q629 1128 692 1106T800 1045T878 960H889Q884 991 878 1049T871 1158V1556H1107V0H923L881 151H871Q841 103 796 65T688 3T540 -20ZM606 171Q757 171 819
+258T883 519V550Q883 736 823 835T604 935Q477 935 411 833T344 547Q344 364 410 268T606 171Z" />
+<glyph unicode="e" horiz-adv-x="1180" d="M609 1128Q757 1128 863 1067T1026 894T1083 626V500H344Q348 339 430 252T662 165Q768 165 852 185T1027 246V55Q944 16 858 -2T652 -20Q489 -20 366 43T173 233T103 546Q103 732 166 862T343 1060T609 1128ZM609 951Q498
+951 430 879T349 668H853Q852 751 826 815T746 915T609 951Z" />
+<glyph unicode="f" horiz-adv-x="741" d="M721 928H452V0H217V928H36V1041L217 1110V1187Q217 1328 261 1411T389 1530T589 1567Q665 1567 728 1555T834 1526L773 1348Q739 1359 697 1368T607 1378Q527 1378 490 1328T452 1182V1107H721V928Z" />
+<glyph unicode="g" horiz-adv-x="1135" d="M484 -492Q259 -492 139 -411T19 -184Q19 -82 83 -12T266 86Q221 106 189 148T156 243Q156 305 191 349T296 434Q209 471 157 554T104 751Q104 871 155 955T304 1084T541 1129Q571 1129 606 1126T672 1118T719 1108H1102V977L914
+942Q941 904 957 856T973 750Q973 578 855 480T528 381Q478 383 431 389Q395 367 376 341T357 281Q357 254 376 238T432 213T524 205H715Q897 205 993 128T1089 -98Q1089 -287 933 -389T484 -492ZM493 -327Q616 -327 700 -303T827 -235T870 -129Q870 -75 843 -46T762
+-6T628 5H454Q389 5 339 -15T262 -75T234 -169Q234 -245 301 -286T493 -327ZM539 535Q642 535 692 591T742 751Q742 863 691 919T538 975Q439 975 388 918T336 749Q336 648 387 592T539 535Z" />
+<glyph unicode="h" horiz-adv-x="1301" d="M403 1556V1165Q403 1104 400 1046T392 956H405Q440 1014 491 1051T605 1108T741 1127Q868 1127 957 1085T1093 953T1141 722V0H906V678Q906 807 853 871T689 936Q582 936 520 892T430 761T403 550V0H168V1556H403Z" />
+<glyph unicode="i" horiz-adv-x="571" d="M403 1107V0H168V1107H403ZM287 1531Q341 1531 380 1502T420 1402Q420 1332 381 1303T287 1273Q231 1273 193 1302T154 1402Q154 1473 192 1502T287 1531Z" />
+<glyph unicode="j" horiz-adv-x="571" d="M57 -492Q5 -492 -42 -485T-121 -467V-279Q-87 -289 -55 -294T18 -299Q83 -299 125 -263T168 -126V1107H403V-147Q403 -248 368 -326T256 -448T57 -492ZM154 1402Q154 1473 192 1502T287 1531Q341 1531 380 1502T420 1402Q420
+1332 381 1303T287 1273Q231 1273 193 1302T154 1402Z" />
+<glyph unicode="k" horiz-adv-x="1173" d="M403 1556V804Q403 753 399 693T391 579H396Q422 614 458 661T528 743L866 1107H1136L693 631L1165 0H889L535 486L403 373V0H168V1556H403Z" />
+<glyph unicode="l" horiz-adv-x="571" d="M404 0H168V1556H404V0Z" />
+<glyph unicode="m" horiz-adv-x="1954" d="M1419 1128Q1605 1128 1699 1032T1794 724V0H1559V682Q1559 808 1510 872T1361 936Q1221 936 1160 847T1098 587V0H863V682Q863 766 842 822T776 907T665 936Q568 936 511 892T428 763T403 553V0H168V1107H351L384 958H397Q431
+1016 481 1053T594 1109T723 1128Q847 1128 932 1085T1059 952H1077Q1129 1042 1222 1085T1419 1128Z" />
+<glyph unicode="n" horiz-adv-x="1301" d="M745 1128Q932 1128 1036 1032T1141 722V0H906V678Q906 807 853 871T689 936Q528 936 466 837T403 550V0H168V1107H351L384 957H397Q433 1015 486 1052T606 1109T745 1128Z" />
+<glyph unicode="o" horiz-adv-x="1250" d="M1148 556Q1148 418 1112 311T1007 131T841 19T622 -20Q508 -20 413 18T249 130T141 311T103 556Q103 739 166 866T348 1061T629 1128Q782 1128 899 1061T1082 866T1148 556ZM345 556Q345 435 374 349T466 218T626 172Q724
+172 786 217T877 349T906 556Q906 677 877 761T786 890T625 935Q479 935 412 837T345 556Z" />
+<glyph unicode="p" horiz-adv-x="1275" d="M736 1128Q933 1128 1052 984T1172 556Q1172 368 1117 240T962 46T730 -20Q646 -20 584 2T478 59T404 137H390Q395 97 399 49T404 -40V-491H168V1107H360L393 954H404Q435 1001 479 1041T587 1104T736 1128ZM673 936Q576
+936 518 898T433 782T404 589V556Q404 433 429 348T514 218T676 173Q762 173 818 220T903 354T931 559Q931 738 868 837T673 936Z" />
+<glyph unicode="q" horiz-adv-x="1275" d="M871 -491V-21Q871 19 873 66T882 152H870Q825 79 745 30T536 -20Q341 -20 222 124T103 552Q103 740 158 868T312 1062T544 1128Q671 1128 750 1079T878 957H886L912 1107H1107V-491H871ZM606 169Q706 169 766 207T853
+322T882 515V551Q882 739 821 837T604 935Q472 935 408 833T344 548Q344 364 408 267T606 169Z" />
+<glyph unicode="r" horiz-adv-x="884" d="M729 1128Q757 1128 790 1125T847 1117L825 897Q804 903 774 906T719 909Q656 909 599 889T498 827T429 723T404 579V0H168V1107H352L384 912H395Q428 971 477 1020T589 1098T729 1128Z" />
+<glyph unicode="s" horiz-adv-x="997" d="M912 316Q912 207 859 132T705 19T456 -20Q340 -20 257 -4T99 47V250Q178 213 276 186T463 159Q579 159 630 195T681 293Q681 329 661 357T585 416T422 490Q317 532 245 574T136 675T99 828Q99 975 215 1051T524 1128Q626
+1128 716 1108T894 1048L820 871Q745 904 670 925T516 947Q425 947 378 919T330 839Q330 800 353 773T432 719T590 652Q689 614 761 573T873 471T912 316Z" />
+<glyph unicode="t" horiz-adv-x="810" d="M580 170Q626 170 671 178T753 199V21Q714 4 652 -8T523 -20Q429 -20 354 11T235 120T191 333V928H40V1033L202 1116L279 1353H427V1107H744V928H427V336Q427 252 469 211T580 170Z" />
+<glyph unicode="u" horiz-adv-x="1301" d="M1133 1107V0H948L916 149H903Q868 92 814 55T694 -1T555 -20Q430 -20 342 22T206 154T159 384V1107H395V428Q395 299 447 235T611 171Q718 171 781 215T870 345T897 556V1107H1133Z" />
+<glyph unicode="v" horiz-adv-x="1094" d="M421 0L0 1107H249L477 457Q499 395 517 323T542 202H550Q557 253 577 324T618 457L846 1107H1094L673 0H421Z" />
+<glyph unicode="w" horiz-adv-x="1670" d="M1073 1L933 516Q923 554 909 608T881 719T856 826T838 902H830Q825 875 814 826T789 719T762 606T737 512L590 1H332L22 1108H260L404 558Q418 502 431 437T455 313T470 216H478Q482 243 489 284T505 371T523 457T540
+523L710 1108H967L1130 523Q1141 485 1154 428T1177 313T1191 217H1199Q1203 251 1213 309T1238 434T1267 558L1414 1108H1648L1336 1H1073Z" />
+<glyph unicode="x" horiz-adv-x="1128" d="M413 566L43 1107H311L564 718L818 1107H1085L713 566L1104 0H835L564 415L292 0H25L413 566Z" />
+<glyph unicode="y" horiz-adv-x="1096" d="M1 1107H257L484 475Q499 432 511 391T533 309T549 229H555Q565 281 583 345T625 475L843 1107H1096L621 -152Q580 -260 521 -336T380 -452T195 -492Q145 -492 108 -487T45 -475V-287Q66 -292 98 -296T164 -300Q227 -300
+273 -275T351 -205T404 -101L443 3L1 1107Z" />
+<glyph unicode="z" horiz-adv-x="980" d="M909 0H68V145L627 926H102V1107H893V947L345 181H909V0Z" />
+<glyph unicode="{" horiz-adv-x="788" d="M714 -324Q564 -323 472 -291T338 -193T296 -24V287Q296 355 267 396T182 456T44 475V664Q126 664 182 682T267 742T296 851V1164Q296 1267 339 1333T474 1430T714 1462V1282Q650 1280 604 1264T534 1212T510 1112V812Q510
+713 454 654T285 576V564Q398 547 454 488T510 329V26Q510 -37 534 -73T604 -125T714 -142V-324Z" />
+<glyph unicode="|" horiz-adv-x="1127" d="M474 1554H653V-480H474V1554Z" />
+<glyph unicode="}" horiz-adv-x="788" d="M75 -324V-142Q138 -140 183 -125T253 -73T278 27V328Q278 427 335 486T503 563V575Q391 594 335 653T278 811V1113Q278 1176 254 1212T185 1265T75 1282V1462Q224 1462 316 1430T450 1332T492 1163V852Q492 784 521 743T606
+683T744 664V475Q662 475 606 456T521 396T492 288V-25Q492 -127 449 -193T314 -292T75 -324Z" />
+<glyph unicode="~" horiz-adv-x="1171" d="M552 637Q479 669 428 681T330 694Q273 694 210 659T96 573V763Q146 817 208 844T347 871Q408 871 467 859T619 807Q693 775 743 763T839 751Q898 751 960 785T1073 871V682Q1024 629 962 601T823 573Q763 573 704 585T552 637Z" />
+<glyph unicode="&#xa0;" horiz-adv-x="532" />
+<glyph unicode="&#xa1;" horiz-adv-x="564" d="M190 644H368L416 -371H142L190 644ZM428 967Q428 885 385 851T280 817Q220 817 177 851T134 967Q134 1046 177 1081T280 1117Q342 1117 385 1082T428 967Z" />
+<glyph unicode="&#xa2;" horiz-adv-x="1171" d="M731 1483V1322Q818 1318 889 1300T1015 1256L946 1069Q882 1094 818 1110T695 1127Q597 1127 533 1085T437 957T405 743Q405 617 437 535T533 412T689 371Q781 371 849 389T989 439V239Q930 210 868 194T730 174V-20H577V180Q448
+199 356 263T214 445T164 741Q164 929 215 1048T359 1232T577 1314V1483H731Z" />
+<glyph unicode="&#xa3;" horiz-adv-x="1171" d="M693 1482Q804 1482 898 1459T1067 1401L990 1219Q925 1248 853 1268T708 1289Q614 1289 559 1238T503 1068V828H899V655H503V472Q503 394 482 342T427 259T356 206H1111V0H75V195Q134 214 177 246T245 332T269
+470V655H81V828H269V1078Q269 1215 324 1304T475 1438T693 1482Z" />
+<glyph unicode="&#xa4;" horiz-adv-x="1171" d="M186 723Q186 782 203 836T250 937L117 1071L237 1190L369 1059Q415 1089 471 1106T585 1123Q644 1123 697 1107T798 1058L931 1190L1052 1073L920 939Q949 894 967 839T985 723Q985 664 969 609T920 507L1049 375L931
+258L798 388Q753 359 699 343T585 326Q525 326 469 342T368 389L237 260L119 377L250 509Q220 556 203 610T186 723ZM353 723Q353 659 384 606T468 523T585 492Q650 492 703 523T788 606T820 723Q820 789 789 842T704 926T585 958Q521 958 468 927T384 842T353
+723Z" />
+<glyph unicode="&#xa5;" horiz-adv-x="1171" d="M584 801L905 1462H1149L747 703H977V553H696V397H977V246H696V0H472V246H190V397H472V553H190V703H415L19 1462H265L584 801Z" />
+<glyph unicode="&#xa6;" horiz-adv-x="1127" d="M474 1554H653V758H474V1554ZM474 316H653V-480H474V316Z" />
+<glyph unicode="&#xa7;" horiz-adv-x="1024" d="M131 805Q131 900 176 962T282 1058Q212 1098 173 1154T134 1291Q134 1417 241 1492T536 1567Q644 1567 727 1546T891 1490L824 1331Q756 1361 685 1383T528 1406Q425 1406 380 1378T334 1295Q334 1258 358 1230T435
+1174T577 1113Q677 1076 750 1031T863 925T903 782Q903 680 862 617T762 517Q830 478 866 425T902 294Q902 151 785 69T462 -14Q351 -14 266 5T114 59V234Q163 211 222 190T345 156T465 143Q599 143 649 183T699 279Q699 317 681 344T611 400T461 467Q357 508 284
+551T171 654T131 805ZM314 825Q314 778 341 741T425 671T576 601L610 588Q651 614 684 655T718 757Q718 805 694 843T608 915T438 986Q389 971 352 929T314 825Z" />
+<glyph unicode="&#xa8;" horiz-adv-x="1215" d="M295 1400Q295 1460 328 1487T409 1515Q457 1515 491 1488T526 1400Q526 1342 492 1313T409 1284Q361 1284 328 1313T295 1400ZM688 1400Q688 1460 721 1487T803 1515Q851 1515 885 1488T920 1400Q920 1342 886
+1313T803 1284Q755 1284 722 1313T688 1400Z" />
+<glyph unicode="&#xa9;" horiz-adv-x="1704" d="M852 -20Q689 -20 552 36T313 193T156 431T100 731Q100 891 156 1028T315 1267T554 1426T852 1483Q1009 1483 1145 1427T1385 1270T1546 1031T1604 731Q1604 569 1549 432T1392 193T1153 36T852 -20ZM884 268Q678
+268 576 395T473 731Q473 866 521 970T663 1133T892 1193Q956 1193 1021 1177T1144 1132L1086 1005Q1036 1031 988 1045T894 1059Q770 1059 702 972T633 731Q633 573 694 488T891 403Q940 403 998 416T1107 450V318Q1058 296 1007 282T884 268ZM852 97Q982 97 1095
+143T1294 275T1428 477T1477 731Q1477 862 1431 977T1300 1180T1101 1317T852 1367Q717 1367 603 1320T403 1188T272 986T225 731Q225 596 271 481T401 279T600 145T852 97Z" />
+<glyph unicode="&#xaa;" horiz-adv-x="754" d="M385 1479Q523 1479 591 1415T659 1228V782H546L519 881Q478 830 419 800T284 770Q215 770 164 793T86 863T58 982Q58 1056 96 1103T209 1174T394 1202L500 1206V1238Q500 1296 463 1323T363 1351Q312 1351 259 1335T152
+1294L98 1409Q157 1438 229 1458T385 1479ZM500 1103L401 1098Q294 1093 258 1061T222 983Q222 937 249 916T318 895Q409 895 454 940T500 1060V1103Z" />
+<glyph unicode="&#xab;" horiz-adv-x="1138" d="M81 565L436 997L605 903L322 553L605 202L436 108L81 538V565ZM533 565L890 997L1059 903L776 553L1059 202L890 108L533 538V565Z" />
+<glyph unicode="&#xac;" horiz-adv-x="1171" d="M1071 811V256H894V633H96V811H1071Z" />
+<glyph unicode="&#xad;" horiz-adv-x="659" d="M72 450V649H588V450H72Z" />
+<glyph unicode="&#xae;" horiz-adv-x="1704" d="M568 284V1183H835Q1000 1183 1078 1115T1156 914Q1156 817 1107 761T995 680L1232 284H1057L856 634H724V284H568ZM724 761H830Q912 761 955 802T999 910Q999 985 959 1018T828 1051H724V761ZM852 -20Q689 -20
+552 36T313 193T156 431T100 731Q100 891 156 1028T315 1267T554 1426T852 1483Q1009 1483 1145 1427T1385 1270T1546 1031T1604 731Q1604 569 1549 432T1392 193T1153 36T852 -20ZM852 97Q982 97 1095 143T1294 275T1428 477T1477 731Q1477 862 1431 977T1300
+1180T1101 1317T852 1367Q717 1367 603 1320T403 1188T272 986T225 731Q225 596 271 481T401 279T600 145T852 97Z" />
+<glyph unicode="&#xaf;" horiz-adv-x="1024" d="M1030 1556H-6V1720H1030V1556Z" />
+<glyph unicode="&#xb0;" horiz-adv-x="877" d="M438 826Q338 826 262 867T142 983T99 1154Q99 1250 141 1324T260 1441T438 1483Q539 1483 615 1441T734 1325T778 1154Q778 1057 735 983T615 868T438 826ZM439 978Q521 978 568 1026T615 1154Q615 1237 567 1285T439
+1333Q357 1333 310 1285T262 1154Q262 1074 309 1026T439 978Z" />
+<glyph unicode="&#xb1;" horiz-adv-x="1171" d="M96 0V177H1074V0H96ZM673 844H1073V666H673V262H495V666H96V844H495V1252H673V844Z" />
+<glyph unicode="&#xb2;" horiz-adv-x="744" d="M665 852H53V988L283 1214Q355 1284 394 1329T448 1411T464 1489Q464 1541 433 1568T348 1596Q298 1596 251 1574T150 1508L55 1629Q117 1682 192 1715T366 1748Q497 1748 574 1684T652 1503Q652 1438 627 1383T551
+1271T421 1141L284 1012H665V852Z" />
+<glyph unicode="&#xb3;" horiz-adv-x="744" d="M358 1747Q492 1747 569 1685T647 1520Q647 1443 605 1392T487 1316V1307Q578 1288 626 1233T674 1100Q674 980 587 909T318 837Q243 837 177 852T48 899V1055Q115 1019 182 998T318 976Q409 976 452 1011T495 1110Q495
+1169 448 1202T298 1236H183V1369H288Q392 1369 431 1406T471 1498Q471 1549 437 1577T346 1606Q290 1606 242 1586T137 1528L52 1643Q114 1689 187 1718T358 1747Z" />
+<glyph unicode="&#xb4;" horiz-adv-x="655" d="M573 1569V1549Q549 1520 508 1480T419 1395T324 1310T239 1241H82V1267Q115 1305 155 1358T235 1467T302 1569H573Z" />
+<glyph unicode="&#xb5;" horiz-adv-x="1309" d="M1142 1107V0H959L924 151H912Q882 96 841 58T745 0T620 -20Q547 -20 492 5T401 75H394Q397 53 399 14T402 -72T403 -164V-492H168V1107H403V427Q403 300 457 236T623 171Q730 171 792 215T880 346T906 556V1107H1142Z" />
+<glyph unicode="&#xb6;" horiz-adv-x="1341" d="M1142 -260H1006V1403H815V-260H678V568Q647 559 610 555T532 550Q407 550 314 598T169 755T118 1048Q118 1240 173 1351T329 1509T563 1556H1142V-260Z" />
+<glyph unicode="&#xb7;" horiz-adv-x="561" d="M134 718Q134 800 176 833T280 867Q341 867 384 834T428 718Q428 638 385 603T280 567Q219 567 177 602T134 718Z" />
+<glyph unicode="&#xb8;" horiz-adv-x="437" d="M423 -268Q423 -374 352 -433T121 -492Q83 -492 51 -487T-4 -476V-339Q20 -345 56 -350T122 -355Q174 -355 204 -337T235 -276Q235 -230 190 -203T57 -165L141 0H293L252 -86Q298 -98 336 -121T399 -180T423 -268Z" />
+<glyph unicode="&#xb9;" horiz-adv-x="744" d="M533 1729V852H346V1358Q346 1390 347 1427T350 1499T354 1556Q337 1539 313 1517T265 1477L172 1408L84 1519L368 1729H533Z" />
+<glyph unicode="&#xba;" horiz-adv-x="780" d="M719 1126Q719 957 630 864T388 770Q245 770 154 862T62 1126Q62 1296 150 1388T392 1480Q489 1480 562 1439T677 1318T719 1126ZM224 1126Q224 1015 263 959T390 902Q476 902 515 958T555 1126Q555 1236 516 1291T390
+1346Q304 1346 264 1292T224 1126Z" />
+<glyph unicode="&#xbb;" horiz-adv-x="1138" d="M1058 539L700 108L532 202L814 553L532 903L700 997L1058 565V539ZM604 539L248 108L80 202L362 553L80 903L248 997L604 565V539Z" />
+<glyph unicode="&#xbc;" horiz-adv-x="1608" d="M291 0L1136 1462H1328L483 0H291ZM318 586V1092Q318 1124 319 1161T321 1233T325 1290Q309 1273 284 1251T236 1211L143 1142L56 1253L340 1462H504V586H318ZM1273 0V179H881V304L1276 883H1463V319H1589V179H1463V0H1273ZM1040
+319H1273V505Q1273 548 1274 599T1279 699Q1268 672 1241 623T1192 543L1040 319Z" />
+<glyph unicode="&#xbd;" horiz-adv-x="1682" d="M264 0L1109 1462H1300L455 0H264ZM306 586V1092Q306 1116 306 1143T308 1197T311 1249T314 1290Q297 1273 273 1251T225 1211L132 1142L45 1253L329 1462H493V586H306ZM990 0V136L1220 362Q1292 432 1331 477T1386
+559T1402 637Q1402 689 1370 717T1286 745Q1235 745 1188 723T1087 656L993 777Q1055 830 1130 863T1303 896Q1434 896 1512 832T1590 651Q1590 586 1565 531T1488 419T1358 289L1221 160H1602V0H990Z" />
+<glyph unicode="&#xbe;" horiz-adv-x="1663" d="M374 0L1219 1462H1410L566 0H374ZM331 570Q257 570 191 585T62 633V789Q129 752 196 731T331 710Q422 710 465 745T509 844Q509 903 462 936T312 970H196V1103H301Q406 1103 445 1140T484 1232Q484 1283 451 1311T359
+1340Q303 1340 255 1320T150 1262L66 1377Q128 1423 201 1452T372 1481Q505 1481 582 1418T660 1253Q660 1177 618 1126T500 1050V1040Q591 1021 639 966T688 834Q688 714 601 642T331 570ZM1328 0V179H936V304L1331 883H1518V319H1644V179H1518V0H1328ZM1096 319H1328V505Q1328
+548 1329 599T1334 699Q1322 672 1295 623T1247 543L1096 319Z" />
+<glyph unicode="&#xbf;" horiz-adv-x="931" d="M650 645V582Q650 509 633 454T577 350T469 247Q399 193 357 153T297 73T278 -23Q278 -107 336 -152T497 -197Q590 -197 670 -171T830 -105L914 -282Q823 -331 717 -362T481 -393Q278 -393 166 -294T54 -32Q54 56
+82 119T165 236T297 350Q361 400 394 436T439 509T451 598V645H650ZM688 967Q688 885 647 851T541 817Q482 817 439 851T395 967Q395 1046 438 1082T541 1118Q605 1118 646 1082T688 967Z" />
+<glyph unicode="&#xc0;" horiz-adv-x="1354" d="M1098 0L955 406H396L253 0H0L544 1468H810L1353 0H1098ZM893 612L754 1012Q746 1038 731 1085T701 1182T676 1266Q666 1225 652 1175T624 1080T603 1012L463 612H893ZM617 1925Q644 1879 683 1823T764 1714T838
+1623V1597H681Q643 1625 596 1665T501 1750T412 1835T347 1905V1925H617Z" />
+<glyph unicode="&#xc1;" horiz-adv-x="1354" d="M1098 0L955 406H396L253 0H0L544 1468H810L1353 0H1098ZM893 612L754 1012Q746 1038 731 1085T701 1182T676 1266Q666 1225 652 1175T624 1080T603 1012L463 612H893ZM1017 1925V1905Q993 1876 952 1836T863 1751T768
+1666T683 1597H526V1623Q559 1661 599 1714T679 1823T746 1925H1017Z" />
+<glyph unicode="&#xc2;" horiz-adv-x="1354" d="M1098 0L955 406H396L253 0H0L544 1468H810L1353 0H1098ZM893 612L754 1012Q746 1038 731 1085T701 1182T676 1266Q666 1225 652 1175T624 1080T603 1012L463 612H893ZM810 1925Q837 1879 882 1823T977 1714T1065
+1622V1597H905Q851 1631 791 1678T677 1781Q623 1726 565 1679T453 1597H294V1622Q332 1661 381 1714T475 1823T547 1925H810Z" />
+<glyph unicode="&#xc3;" horiz-adv-x="1354" d="M1098 0L955 406H396L253 0H0L544 1468H810L1353 0H1098ZM893 612L754 1012Q746 1038 731 1085T701 1182T676 1266Q666 1225 652 1175T624 1080T603 1012L463 612H893ZM280 1598Q286 1668 306 1720T358 1808T433
+1862T526 1880Q570 1880 610 1863T689 1824T764 1784T835 1767Q874 1767 901 1795T943 1882H1066Q1054 1744 987 1672T820 1599Q778 1599 738 1616T659 1656T584 1695T512 1712Q472 1712 445 1684T404 1598H280Z" />
+<glyph unicode="&#xc4;" horiz-adv-x="1354" d="M1098 0L955 406H396L253 0H0L544 1468H810L1353 0H1098ZM893 612L754 1012Q746 1038 731 1085T701 1182T676 1266Q666 1225 652 1175T624 1080T603 1012L463 612H893ZM363 1756Q363 1816 396 1843T477 1871Q525
+1871 559 1844T594 1756Q594 1698 560 1669T477 1640Q429 1640 396 1669T363 1756ZM756 1756Q756 1816 789 1843T871 1871Q919 1871 953 1844T988 1756Q988 1698 954 1669T871 1640Q823 1640 790 1669T756 1756Z" />
+<glyph unicode="&#xc5;" horiz-adv-x="1354" d="M1098 0L955 406H396L253 0H0L544 1468H810L1353 0H1098ZM893 612L754 1012Q746 1038 731 1085T701 1182T676 1266Q666 1225 652 1175T624 1080T603 1012L463 612H893ZM675 1350Q571 1350 506 1410T440 1577Q440
+1682 505 1742T675 1802Q775 1802 844 1742T914 1579Q914 1471 846 1411T675 1350ZM675 1468Q722 1468 752 1497T783 1577Q783 1626 752 1655T675 1684Q629 1684 598 1655T567 1577Q567 1527 594 1498T675 1468Z" />
+<glyph unicode="&#xc6;" horiz-adv-x="1864" d="M1747 0H929V406H431L245 0H-1L670 1462H1747V1260H1169V863H1709V662H1169V203H1747V0ZM520 612H929V1254H808L520 612Z" />
+<glyph unicode="&#xc7;" horiz-adv-x="1298" d="M815 1279Q710 1279 628 1241T488 1132T401 959T371 730Q371 560 419 438T565 250T813 184Q906 184 994 202T1176 251V47Q1087 12 996 -4T786 -20Q562 -20 415 73T195 335T122 731Q122 897 168 1035T302 1273T519
+1428T816 1483Q926 1483 1032 1459T1230 1389L1146 1191Q1070 1227 987 1253T815 1279ZM979 -268Q979 -374 908 -433T677 -492Q639 -492 607 -487T552 -476V-339Q576 -345 612 -350T678 -355Q730 -355 760 -337T791 -276Q791 -230 746 -203T613 -165L697 0H849L808
+-86Q854 -98 892 -121T955 -180T979 -268Z" />
+<glyph unicode="&#xc8;" horiz-adv-x="1143" d="M1020 0H192V1462H1020V1260H432V863H983V662H432V203H1020V0ZM562 1925Q589 1879 628 1823T709 1714T783 1623V1597H626Q588 1625 541 1665T446 1750T357 1835T292 1905V1925H562Z" />
+<glyph unicode="&#xc9;" horiz-adv-x="1143" d="M1020 0H192V1462H1020V1260H432V863H983V662H432V203H1020V0ZM963 1925V1905Q939 1876 898 1836T809 1751T714 1666T629 1597H472V1623Q505 1661 545 1714T625 1823T692 1925H963Z" />
+<glyph unicode="&#xca;" horiz-adv-x="1143" d="M1020 0H192V1462H1020V1260H432V863H983V662H432V203H1020V0ZM755 1925Q782 1879 827 1823T922 1714T1010 1622V1597H850Q796 1631 736 1678T622 1781Q568 1726 510 1679T398 1597H239V1622Q277 1661 326 1714T420
+1823T492 1925H755Z" />
+<glyph unicode="&#xcb;" horiz-adv-x="1143" d="M1020 0H192V1462H1020V1260H432V863H983V662H432V203H1020V0ZM309 1756Q309 1816 342 1843T423 1871Q471 1871 505 1844T540 1756Q540 1698 506 1669T423 1640Q375 1640 342 1669T309 1756ZM702 1756Q702 1816
+735 1843T817 1871Q865 1871 899 1844T934 1756Q934 1698 900 1669T817 1640Q769 1640 736 1669T702 1756Z" />
+<glyph unicode="&#xcc;" horiz-adv-x="625" d="M192 0V1462H432V0H192ZM218 1925Q245 1879 284 1823T365 1714T439 1623V1597H282Q244 1625 197 1665T102 1750T13 1835T-52 1905V1925H218Z" />
+<glyph unicode="&#xcd;" horiz-adv-x="625" d="M192 0V1462H432V0H192ZM678 1925V1905Q654 1876 613 1836T524 1751T429 1666T344 1597H187V1623Q220 1661 260 1714T340 1823T407 1925H678Z" />
+<glyph unicode="&#xce;" horiz-adv-x="625" d="M192 0V1462H432V0H192ZM444 1925Q471 1879 516 1823T611 1714T699 1622V1597H539Q485 1631 425 1678T311 1781Q257 1726 199 1679T87 1597H-72V1622Q-34 1661 15 1714T109 1823T181 1925H444Z" />
+<glyph unicode="&#xcf;" horiz-adv-x="625" d="M192 0V1462H432V0H192ZM2 1756Q2 1816 35 1843T116 1871Q164 1871 198 1844T233 1756Q233 1698 199 1669T116 1640Q68 1640 35 1669T2 1756ZM395 1756Q395 1816 428 1843T510 1871Q558 1871 592 1844T627 1756Q627
+1698 593 1669T510 1640Q462 1640 429 1669T395 1756Z" />
+<glyph unicode="&#xd0;" horiz-adv-x="1501" d="M642 1462Q868 1462 1033 1381T1289 1140T1379 745Q1379 498 1287 333T1020 84T597 0H199V623H53V824H199V1462H642ZM636 1263H438V824H743V623H438V201H601Q865 201 997 336T1129 738Q1129 918 1072 1034T904 1206T636
+1263Z" />
+<glyph unicode="&#xd1;" horiz-adv-x="1604" d="M1412 0H1117L401 1167H392Q396 1108 399 1042T405 906T410 764V0H192V1462H485L1200 303H1207Q1205 354 1202 420T1197 556T1193 693V1462H1412V0ZM408 1598Q414 1668 434 1720T486 1808T561 1862T654 1880Q698
+1880 738 1863T817 1824T892 1784T963 1767Q1002 1767 1029 1795T1071 1882H1194Q1182 1744 1115 1672T948 1599Q906 1599 866 1616T787 1656T712 1695T640 1712Q600 1712 573 1684T532 1598H408Z" />
+<glyph unicode="&#xd2;" horiz-adv-x="1612" d="M1490 733Q1490 564 1448 426T1320 188T1107 34T807 -20Q632 -20 504 34T291 188T164 427T122 735Q122 961 196 1129T423 1391T809 1485Q1037 1485 1188 1392T1414 1131T1490 733ZM374 733Q374 564 420 441T561
+251T807 184Q958 184 1053 250T1193 440T1238 733Q1238 990 1136 1135T809 1281Q659 1281 563 1215T420 1027T374 733ZM748 1925Q775 1879 814 1823T895 1714T969 1623V1597H812Q774 1625 727 1665T632 1750T543 1835T478 1905V1925H748Z" />
+<glyph unicode="&#xd3;" horiz-adv-x="1612" d="M1490 733Q1490 564 1448 426T1320 188T1107 34T807 -20Q632 -20 504 34T291 188T164 427T122 735Q122 961 196 1129T423 1391T809 1485Q1037 1485 1188 1392T1414 1131T1490 733ZM374 733Q374 564 420 441T561
+251T807 184Q958 184 1053 250T1193 440T1238 733Q1238 990 1136 1135T809 1281Q659 1281 563 1215T420 1027T374 733ZM1148 1925V1905Q1124 1876 1083 1836T994 1751T899 1666T814 1597H657V1623Q690 1661 730 1714T810 1823T877 1925H1148Z" />
+<glyph unicode="&#xd4;" horiz-adv-x="1612" d="M1490 733Q1490 564 1448 426T1320 188T1107 34T807 -20Q632 -20 504 34T291 188T164 427T122 735Q122 961 196 1129T423 1391T809 1485Q1037 1485 1188 1392T1414 1131T1490 733ZM374 733Q374 564 420 441T561
+251T807 184Q958 184 1053 250T1193 440T1238 733Q1238 990 1136 1135T809 1281Q659 1281 563 1215T420 1027T374 733ZM940 1925Q967 1879 1012 1823T1107 1714T1195 1622V1597H1035Q981 1631 921 1678T807 1781Q753 1726 695 1679T583 1597H424V1622Q462 1661
+511 1714T605 1823T677 1925H940Z" />
+<glyph unicode="&#xd5;" horiz-adv-x="1612" d="M1490 733Q1490 564 1448 426T1320 188T1107 34T807 -20Q632 -20 504 34T291 188T164 427T122 735Q122 961 196 1129T423 1391T809 1485Q1037 1485 1188 1392T1414 1131T1490 733ZM374 733Q374 564 420 441T561
+251T807 184Q958 184 1053 250T1193 440T1238 733Q1238 990 1136 1135T809 1281Q659 1281 563 1215T420 1027T374 733ZM410 1598Q416 1668 436 1720T488 1808T563 1862T656 1880Q700 1880 740 1863T819 1824T894 1784T965 1767Q1004 1767 1031 1795T1073 1882H1196Q1184
+1744 1117 1672T950 1599Q908 1599 868 1616T789 1656T714 1695T642 1712Q602 1712 575 1684T534 1598H410Z" />
+<glyph unicode="&#xd6;" horiz-adv-x="1612" d="M1490 733Q1490 564 1448 426T1320 188T1107 34T807 -20Q632 -20 504 34T291 188T164 427T122 735Q122 961 196 1129T423 1391T809 1485Q1037 1485 1188 1392T1414 1131T1490 733ZM374 733Q374 564 420 441T561
+251T807 184Q958 184 1053 250T1193 440T1238 733Q1238 990 1136 1135T809 1281Q659 1281 563 1215T420 1027T374 733ZM494 1756Q494 1816 527 1843T608 1871Q656 1871 690 1844T725 1756Q725 1698 691 1669T608 1640Q560 1640 527 1669T494 1756ZM887 1756Q887
+1816 920 1843T1002 1871Q1050 1871 1084 1844T1119 1756Q1119 1698 1085 1669T1002 1640Q954 1640 921 1669T887 1756Z" />
+<glyph unicode="&#xd7;" horiz-adv-x="1171" d="M914 1176L1037 1053L708 723L1035 395L913 270L582 597L257 271L133 396L458 723L131 1050L257 1176L583 849L914 1176Z" />
+<glyph unicode="&#xd8;" horiz-adv-x="1612" d="M1490 733Q1490 564 1448 426T1320 188T1107 34T807 -20Q699 -20 609 1T447 64L351 -76L212 16L312 162Q216 262 169 407T122 735Q122 961 196 1129T423 1391T809 1485Q913 1485 1003 1464T1165 1402L1256 1534L1394
+1442L1297 1303Q1392 1205 1441 1060T1490 733ZM1238 733Q1238 845 1218 936T1157 1095L570 246Q617 216 675 200T807 184Q958 184 1053 250T1193 440T1238 733ZM374 733Q374 622 394 531T454 371L1042 1220Q996 1249 938 1265T809 1281Q659 1281 563 1215T420
+1027T374 733Z" />
+<glyph unicode="&#xd9;" horiz-adv-x="1521" d="M1340 1462V516Q1340 363 1276 242T1081 51T754 -20Q473 -20 327 127T180 520V1462H420V541Q420 358 506 271T763 184Q881 184 955 225T1065 347T1101 542V1462H1340ZM702 1925Q729 1879 768 1823T849 1714T923
+1623V1597H766Q728 1625 681 1665T586 1750T497 1835T432 1905V1925H702Z" />
+<glyph unicode="&#xda;" horiz-adv-x="1521" d="M1340 1462V516Q1340 363 1276 242T1081 51T754 -20Q473 -20 327 127T180 520V1462H420V541Q420 358 506 271T763 184Q881 184 955 225T1065 347T1101 542V1462H1340ZM1102 1925V1905Q1078 1876 1037 1836T948 1751T853
+1666T768 1597H611V1623Q644 1661 684 1714T764 1823T831 1925H1102Z" />
+<glyph unicode="&#xdb;" horiz-adv-x="1521" d="M1340 1462V516Q1340 363 1276 242T1081 51T754 -20Q473 -20 327 127T180 520V1462H420V541Q420 358 506 271T763 184Q881 184 955 225T1065 347T1101 542V1462H1340ZM895 1925Q922 1879 967 1823T1062 1714T1150
+1622V1597H990Q936 1631 876 1678T762 1781Q708 1726 650 1679T538 1597H379V1622Q417 1661 466 1714T560 1823T632 1925H895Z" />
+<glyph unicode="&#xdc;" horiz-adv-x="1521" d="M1340 1462V516Q1340 363 1276 242T1081 51T754 -20Q473 -20 327 127T180 520V1462H420V541Q420 358 506 271T763 184Q881 184 955 225T1065 347T1101 542V1462H1340ZM449 1756Q449 1816 482 1843T563 1871Q611
+1871 645 1844T680 1756Q680 1698 646 1669T563 1640Q515 1640 482 1669T449 1756ZM842 1756Q842 1816 875 1843T957 1871Q1005 1871 1039 1844T1074 1756Q1074 1698 1040 1669T957 1640Q909 1640 876 1669T842 1756Z" />
+<glyph unicode="&#xdd;" horiz-adv-x="1212" d="M606 795L953 1462H1212L726 568V0H487V559L0 1462H261L606 795ZM948 1925V1905Q924 1876 883 1836T794 1751T699 1666T614 1597H457V1623Q490 1661 530 1714T610 1823T677 1925H948Z" />
+<glyph unicode="&#xde;" horiz-adv-x="1259" d="M1161 776Q1161 682 1132 598T1038 448T864 345T597 307H432V0H192V1462H432V1220H626Q906 1220 1033 1102T1161 776ZM432 505H561Q681 505 759 531T877 617T916 772Q916 899 839 960T592 1021H432V505Z" />
+<glyph unicode="&#xdf;" horiz-adv-x="1366" d="M1150 1255Q1150 1181 1121 1128T1050 1035T965 964T893 903T864 840Q864 812 881 789T941 737T1059 657Q1128 612 1178 565T1256 459T1283 318Q1283 205 1233 130T1090 18T865 -20Q767 -20 695 -4T566 45V242Q600
+221 647 202T748 170T852 157Q950 157 997 197T1045 310Q1045 355 1029 387T972 453T855 533Q771 584 722 627T652 714T631 813Q631 877 659 921T729 1000T812 1065T882 1133T910 1223Q910 1299 842 1339T667 1379Q592 1379 533 1358T438 1288T403 1160V0H168V1165Q168
+1307 233 1395T411 1525T667 1567Q808 1567 917 1532T1088 1427T1150 1255Z" />
+<glyph unicode="&#xe0;" horiz-adv-x="1188" d="M602 1128Q812 1128 919 1035T1027 745V0H860L815 157H807Q760 97 710 58T595 0T435 -20Q336 -20 258 16T135 128T90 318Q90 489 217 575T603 670L795 677V735Q795 850 742 899T591 948Q508 948 430 924T278 865L202
+1031Q283 1074 386 1101T602 1128ZM794 529L651 524Q475 518 404 464T333 316Q333 234 382 197T511 159Q633 159 713 228T794 433V529ZM535 1569Q562 1523 601 1467T682 1358T756 1267V1241H599Q561 1269 514 1309T419 1394T330 1479T265 1549V1569H535Z" />
+<glyph unicode="&#xe1;" horiz-adv-x="1188" d="M602 1128Q812 1128 919 1035T1027 745V0H860L815 157H807Q760 97 710 58T595 0T435 -20Q336 -20 258 16T135 128T90 318Q90 489 217 575T603 670L795 677V735Q795 850 742 899T591 948Q508 948 430 924T278 865L202
+1031Q283 1074 386 1101T602 1128ZM794 529L651 524Q475 518 404 464T333 316Q333 234 382 197T511 159Q633 159 713 228T794 433V529ZM935 1569V1549Q911 1520 870 1480T781 1395T686 1310T601 1241H444V1267Q477 1305 517 1358T597 1467T664 1569H935Z" />
+<glyph unicode="&#xe2;" horiz-adv-x="1188" d="M602 1128Q812 1128 919 1035T1027 745V0H860L815 157H807Q760 97 710 58T595 0T435 -20Q336 -20 258 16T135 128T90 318Q90 489 217 575T603 670L795 677V735Q795 850 742 899T591 948Q508 948 430 924T278 865L202
+1031Q283 1074 386 1101T602 1128ZM794 529L651 524Q475 518 404 464T333 316Q333 234 382 197T511 159Q633 159 713 228T794 433V529ZM727 1569Q754 1523 799 1467T894 1358T982 1266V1241H822Q768 1275 708 1322T594 1425Q540 1370 482 1323T370 1241H211V1266Q249
+1305 298 1358T392 1467T464 1569H727Z" />
+<glyph unicode="&#xe3;" horiz-adv-x="1188" d="M602 1128Q812 1128 919 1035T1027 745V0H860L815 157H807Q760 97 710 58T595 0T435 -20Q336 -20 258 16T135 128T90 318Q90 489 217 575T603 670L795 677V735Q795 850 742 899T591 948Q508 948 430 924T278 865L202
+1031Q283 1074 386 1101T602 1128ZM794 529L651 524Q475 518 404 464T333 316Q333 234 382 197T511 159Q633 159 713 228T794 433V529ZM197 1242Q203 1312 223 1364T275 1452T350 1506T443 1524Q487 1524 527 1507T606 1468T681 1428T752 1411Q791 1411 818 1439T860
+1526H983Q971 1388 904 1316T737 1243Q695 1243 655 1260T576 1300T501 1339T429 1356Q389 1356 362 1328T321 1242H197Z" />
+<glyph unicode="&#xe4;" horiz-adv-x="1188" d="M602 1128Q812 1128 919 1035T1027 745V0H860L815 157H807Q760 97 710 58T595 0T435 -20Q336 -20 258 16T135 128T90 318Q90 489 217 575T603 670L795 677V735Q795 850 742 899T591 948Q508 948 430 924T278 865L202
+1031Q283 1074 386 1101T602 1128ZM794 529L651 524Q475 518 404 464T333 316Q333 234 382 197T511 159Q633 159 713 228T794 433V529ZM537 1400Q537 1460 570 1487T651 1515Q699 1515 733 1488T768 1400Q768 1342 734 1313T651 1284Q603 1284 570 1313T537 1400ZM930
+1400Q930 1460 963 1487T1045 1515Q1093 1515 1127 1488T1162 1400Q1162 1342 1128 1313T1045 1284Q997 1284 964 1313T930 1400Z" />
+<glyph unicode="&#xe5;" horiz-adv-x="1188" d="M602 1128Q812 1128 919 1035T1027 745V0H860L815 157H807Q760 97 710 58T595 0T435 -20Q336 -20 258 16T135 128T90 318Q90 489 217 575T603 670L795 677V735Q795 850 742 899T591 948Q508 948 430 924T278 865L202
+1031Q283 1074 386 1101T602 1128ZM794 529L651 524Q475 518 404 464T333 316Q333 234 382 197T511 159Q633 159 713 228T794 433V529ZM596 1241Q492 1241 427 1301T361 1468Q361 1573 426 1633T596 1693Q696 1693 765 1633T835 1470Q835 1362 767 1302T596 1241ZM596
+1359Q643 1359 673 1388T704 1468Q704 1517 673 1546T596 1575Q550 1575 519 1546T488 1468Q488 1418 515 1389T596 1359Z" />
+<glyph unicode="&#xe6;" horiz-adv-x="1822" d="M1274 1128Q1411 1128 1512 1067T1670 894T1725 628V501H1009Q1014 336 1091 251T1312 165Q1412 165 1497 185T1669 246V55Q1586 16 1502 -2T1301 -20Q1207 -20 1127 5T984 79T876 203Q823 131 766 81T631 6T442
+-20Q344 -20 264 16T137 128T90 318Q90 432 145 508T309 624T582 670L771 677V754Q771 858 717 903T571 948Q490 948 414 925T264 867L189 1031Q270 1074 374 1101T586 1128Q707 1128 791 1089T924 967Q983 1045 1070 1086T1274 1128ZM769 529L634 524Q468 518
+401 464T333 316Q333 234 379 197T503 159Q580 159 640 190T734 282T769 433V529ZM1270 951Q1159 951 1092 881T1014 668H1494Q1494 752 1469 815T1395 915T1270 951Z" />
+<glyph unicode="&#xe7;" horiz-adv-x="1017" d="M614 -20Q457 -20 342 41T165 228T103 548Q103 750 171 878T359 1067T636 1128Q735 1128 815 1109T952 1061L882 873Q821 898 757 915T634 932Q537 932 473 889T377 761T345 550Q345 428 377 345T472 219T627 176Q718
+176 790 198T926 255V51Q863 15 791 -2T614 -20ZM806 -268Q806 -374 735 -433T504 -492Q466 -492 434 -487T379 -476V-339Q403 -345 439 -350T505 -355Q557 -355 587 -337T618 -276Q618 -230 573 -203T440 -165L524 0H676L635 -86Q681 -98 719 -121T782 -180T806
+-268Z" />
+<glyph unicode="&#xe8;" horiz-adv-x="1180" d="M609 1128Q757 1128 863 1067T1026 894T1083 626V500H344Q348 339 430 252T662 165Q768 165 852 185T1027 246V55Q944 16 858 -2T652 -20Q489 -20 366 43T173 233T103 546Q103 732 166 862T343 1060T609 1128ZM609
+951Q498 951 430 879T349 668H853Q852 751 826 815T746 915T609 951ZM536 1569Q563 1523 602 1467T683 1358T757 1267V1241H600Q562 1269 515 1309T420 1394T331 1479T266 1549V1569H536Z" />
+<glyph unicode="&#xe9;" horiz-adv-x="1180" d="M609 1128Q757 1128 863 1067T1026 894T1083 626V500H344Q348 339 430 252T662 165Q768 165 852 185T1027 246V55Q944 16 858 -2T652 -20Q489 -20 366 43T173 233T103 546Q103 732 166 862T343 1060T609 1128ZM609
+951Q498 951 430 879T349 668H853Q852 751 826 815T746 915T609 951ZM936 1569V1549Q912 1520 871 1480T782 1395T687 1310T602 1241H445V1267Q478 1305 518 1358T598 1467T665 1569H936Z" />
+<glyph unicode="&#xea;" horiz-adv-x="1180" d="M609 1128Q757 1128 863 1067T1026 894T1083 626V500H344Q348 339 430 252T662 165Q768 165 852 185T1027 246V55Q944 16 858 -2T652 -20Q489 -20 366 43T173 233T103 546Q103 732 166 862T343 1060T609 1128ZM609
+951Q498 951 430 879T349 668H853Q852 751 826 815T746 915T609 951ZM728 1569Q755 1523 800 1467T895 1358T983 1266V1241H823Q769 1275 709 1322T595 1425Q541 1370 483 1323T371 1241H212V1266Q250 1305 299 1358T393 1467T465 1569H728Z" />
+<glyph unicode="&#xeb;" horiz-adv-x="1180" d="M609 1128Q757 1128 863 1067T1026 894T1083 626V500H344Q348 339 430 252T662 165Q768 165 852 185T1027 246V55Q944 16 858 -2T652 -20Q489 -20 366 43T173 233T103 546Q103 732 166 862T343 1060T609 1128ZM609
+951Q498 951 430 879T349 668H853Q852 751 826 815T746 915T609 951ZM538 1400Q538 1460 571 1487T652 1515Q700 1515 734 1488T769 1400Q769 1342 735 1313T652 1284Q604 1284 571 1313T538 1400ZM931 1400Q931 1460 964 1487T1046 1515Q1094 1515 1128 1488T1163
+1400Q1163 1342 1129 1313T1046 1284Q998 1284 965 1313T931 1400Z" />
+<glyph unicode="&#xec;" horiz-adv-x="571" d="M403 0H168V1107H403V0ZM483 1569Q510 1523 549 1467T630 1358T704 1267V1241H547Q509 1269 462 1309T367 1394T278 1479T213 1549V1569H483Z" />
+<glyph unicode="&#xed;" horiz-adv-x="571" d="M403 0H168V1107H403V0ZM627 1569V1549Q603 1520 562 1480T473 1395T378 1310T293 1241H136V1267Q169 1305 209 1358T289 1467T356 1569H627Z" />
+<glyph unicode="&#xee;" horiz-adv-x="571" d="M403 0H168V1107H403V0ZM419 1569Q446 1523 491 1467T586 1358T674 1266V1241H514Q460 1275 400 1322T286 1425Q232 1370 174 1323T62 1241H-97V1266Q-59 1305 -10 1358T84 1467T156 1569H419Z" />
+<glyph unicode="&#xef;" horiz-adv-x="571" d="M403 0H168V1107H403V0ZM-27 1400Q-27 1460 6 1487T87 1515Q135 1515 169 1488T204 1400Q204 1342 170 1313T87 1284Q39 1284 6 1313T-27 1400ZM366 1400Q366 1460 399 1487T481 1515Q529 1515 563 1488T598 1400Q598
+1342 564 1313T481 1284Q433 1284 400 1313T366 1400Z" />
+<glyph unicode="&#xf0;" horiz-adv-x="1248" d="M449 1566Q519 1535 584 1499T706 1421L936 1560L1022 1432L835 1320Q933 1230 1002 1118T1109 868T1146 568Q1146 378 1083 247T902 48T621 -20Q469 -20 352 40T169 212T103 482Q103 640 163 751T331 922T582 981Q654
+981 711 969T811 933T882 871L890 875Q857 976 800 1061T665 1217L417 1069L331 1199L535 1319Q496 1345 453 1371T365 1421L449 1566ZM626 801Q527 801 465 765T373 656T343 478Q343 384 372 313T464 201T625 161Q772 161 839 254T906 527Q906 582 890 631T839
+718T752 779T626 801Z" />
+<glyph unicode="&#xf1;" horiz-adv-x="1301" d="M745 1128Q932 1128 1036 1032T1141 722V0H906V678Q906 807 853 871T689 936Q528 936 466 837T403 550V0H168V1107H351L384 957H397Q433 1015 486 1052T606 1109T745 1128ZM256 1242Q262 1312 282 1364T334 1452T409
+1506T502 1524Q546 1524 586 1507T665 1468T740 1428T811 1411Q850 1411 877 1439T919 1526H1042Q1030 1388 963 1316T796 1243Q754 1243 714 1260T635 1300T560 1339T488 1356Q448 1356 421 1328T380 1242H256Z" />
+<glyph unicode="&#xf2;" horiz-adv-x="1250" d="M1148 556Q1148 418 1112 311T1007 131T841 19T622 -20Q508 -20 413 18T249 130T141 311T103 556Q103 739 166 866T348 1061T629 1128Q782 1128 899 1061T1082 866T1148 556ZM345 556Q345 435 374 349T466 218T626
+172Q724 172 786 217T877 349T906 556Q906 677 877 761T786 890T625 935Q479 935 412 837T345 556ZM567 1569Q594 1523 633 1467T714 1358T788 1267V1241H631Q593 1269 546 1309T451 1394T362 1479T297 1549V1569H567Z" />
+<glyph unicode="&#xf3;" horiz-adv-x="1250" d="M1148 556Q1148 418 1112 311T1007 131T841 19T622 -20Q508 -20 413 18T249 130T141 311T103 556Q103 739 166 866T348 1061T629 1128Q782 1128 899 1061T1082 866T1148 556ZM345 556Q345 435 374 349T466 218T626
+172Q724 172 786 217T877 349T906 556Q906 677 877 761T786 890T625 935Q479 935 412 837T345 556ZM968 1569V1549Q944 1520 903 1480T814 1395T719 1310T634 1241H477V1267Q510 1305 550 1358T630 1467T697 1569H968Z" />
+<glyph unicode="&#xf4;" horiz-adv-x="1250" d="M1148 556Q1148 418 1112 311T1007 131T841 19T622 -20Q508 -20 413 18T249 130T141 311T103 556Q103 739 166 866T348 1061T629 1128Q782 1128 899 1061T1082 866T1148 556ZM345 556Q345 435 374 349T466 218T626
+172Q724 172 786 217T877 349T906 556Q906 677 877 761T786 890T625 935Q479 935 412 837T345 556ZM760 1569Q787 1523 832 1467T927 1358T1015 1266V1241H855Q801 1275 741 1322T627 1425Q573 1370 515 1323T403 1241H244V1266Q282 1305 331 1358T425 1467T497
+1569H760Z" />
+<glyph unicode="&#xf5;" horiz-adv-x="1250" d="M1148 556Q1148 418 1112 311T1007 131T841 19T622 -20Q508 -20 413 18T249 130T141 311T103 556Q103 739 166 866T348 1061T629 1128Q782 1128 899 1061T1082 866T1148 556ZM345 556Q345 435 374 349T466 218T626
+172Q724 172 786 217T877 349T906 556Q906 677 877 761T786 890T625 935Q479 935 412 837T345 556ZM230 1242Q236 1312 256 1364T308 1452T383 1506T476 1524Q520 1524 560 1507T639 1468T714 1428T785 1411Q824 1411 851 1439T893 1526H1016Q1004 1388 937 1316T770
+1243Q728 1243 688 1260T609 1300T534 1339T462 1356Q422 1356 395 1328T354 1242H230Z" />
+<glyph unicode="&#xf6;" horiz-adv-x="1250" d="M1148 556Q1148 418 1112 311T1007 131T841 19T622 -20Q508 -20 413 18T249 130T141 311T103 556Q103 739 166 866T348 1061T629 1128Q782 1128 899 1061T1082 866T1148 556ZM345 556Q345 435 374 349T466 218T626
+172Q724 172 786 217T877 349T906 556Q906 677 877 761T786 890T625 935Q479 935 412 837T345 556ZM313 1400Q313 1460 346 1487T427 1515Q475 1515 509 1488T544 1400Q544 1342 510 1313T427 1284Q379 1284 346 1313T313 1400ZM706 1400Q706 1460 739 1487T821
+1515Q869 1515 903 1488T938 1400Q938 1342 904 1313T821 1284Q773 1284 740 1313T706 1400Z" />
+<glyph unicode="&#xf7;" horiz-adv-x="1171" d="M96 633V811H1074V633H96ZM584 237Q533 237 497 269T460 372Q460 447 496 476T584 506Q634 506 670 477T707 372Q707 302 671 270T584 237ZM584 938Q533 938 497 970T460 1072Q460 1147 496 1176T584 1206Q634 1206
+670 1177T707 1072Q707 1002 671 970T584 938Z" />
+<glyph unicode="&#xf8;" horiz-adv-x="1250" d="M1148 556Q1148 373 1085 244T903 48T622 -20Q551 -20 488 -5T370 40L292 -71L161 19L243 135Q176 210 140 315T103 556Q103 830 244 979T629 1128Q702 1128 767 1111T889 1063L959 1163L1091 1073L1013 965Q1077
+892 1112 789T1148 556ZM345 556Q345 490 353 435T381 337L773 895Q744 914 707 924T625 935Q479 935 412 837T345 556ZM906 556Q906 615 898 666T874 758L486 207Q513 189 549 181T626 172Q724 172 786 217T877 349T906 556Z" />
+<glyph unicode="&#xf9;" horiz-adv-x="1301" d="M1133 1107V0H948L916 149H903Q868 92 814 55T694 -1T555 -20Q430 -20 342 22T206 154T159 384V1107H395V428Q395 299 447 235T611 171Q718 171 781 215T870 345T897 556V1107H1133ZM593 1569Q620 1523 659 1467T740
+1358T814 1267V1241H657Q619 1269 572 1309T477 1394T388 1479T323 1549V1569H593Z" />
+<glyph unicode="&#xfa;" horiz-adv-x="1301" d="M1133 1107V0H948L916 149H903Q868 92 814 55T694 -1T555 -20Q430 -20 342 22T206 154T159 384V1107H395V428Q395 299 447 235T611 171Q718 171 781 215T870 345T897 556V1107H1133ZM993 1569V1549Q969 1520 928
+1480T839 1395T744 1310T659 1241H502V1267Q535 1305 575 1358T655 1467T722 1569H993Z" />
+<glyph unicode="&#xfb;" horiz-adv-x="1301" d="M1133 1107V0H948L916 149H903Q868 92 814 55T694 -1T555 -20Q430 -20 342 22T206 154T159 384V1107H395V428Q395 299 447 235T611 171Q718 171 781 215T870 345T897 556V1107H1133ZM785 1569Q812 1523 857 1467T952
+1358T1040 1266V1241H880Q826 1275 766 1322T652 1425Q598 1370 540 1323T428 1241H269V1266Q307 1305 356 1358T450 1467T522 1569H785Z" />
+<glyph unicode="&#xfc;" horiz-adv-x="1301" d="M1133 1107V0H948L916 149H903Q868 92 814 55T694 -1T555 -20Q430 -20 342 22T206 154T159 384V1107H395V428Q395 299 447 235T611 171Q718 171 781 215T870 345T897 556V1107H1133ZM338 1400Q338 1460 371 1487T452
+1515Q500 1515 534 1488T569 1400Q569 1342 535 1313T452 1284Q404 1284 371 1313T338 1400ZM731 1400Q731 1460 764 1487T846 1515Q894 1515 928 1488T963 1400Q963 1342 929 1313T846 1284Q798 1284 765 1313T731 1400Z" />
+<glyph unicode="&#xfd;" horiz-adv-x="1096" d="M1 1107H257L484 475Q499 432 511 391T533 309T549 229H555Q565 281 583 345T625 475L843 1107H1096L621 -152Q580 -260 521 -336T380 -452T195 -492Q145 -492 108 -487T45 -475V-287Q66 -292 98 -296T164 -300Q227
+-300 273 -275T351 -205T404 -101L443 3L1 1107ZM889 1569V1549Q865 1520 824 1480T735 1395T640 1310T555 1241H398V1267Q431 1305 471 1358T551 1467T618 1569H889Z" />
+<glyph unicode="&#xfe;" horiz-adv-x="1275" d="M1172 556Q1172 368 1118 240T965 46T735 -20Q651 -20 588 1T480 56T404 134H391Q394 115 397 84T402 20T404 -36V-491H168V1556H404V1130Q404 1090 401 1038T394 955H404Q435 1003 480 1042T588 1104T736 1128Q933
+1128 1052 983T1172 556ZM931 559Q931 747 868 841T674 936Q528 936 467 850T404 591V556Q404 371 463 272T676 173Q762 173 818 217T903 348T931 559Z" />
+<glyph unicode="&#xff;" horiz-adv-x="1096" d="M1 1107H257L484 475Q499 432 511 391T533 309T549 229H555Q565 281 583 345T625 475L843 1107H1096L621 -152Q580 -260 521 -336T380 -452T195 -492Q145 -492 108 -487T45 -475V-287Q66 -292 98 -296T164 -300Q227
+-300 273 -275T351 -205T404 -101L443 3L1 1107ZM491 1400Q491 1460 524 1487T605 1515Q653 1515 687 1488T722 1400Q722 1342 688 1313T605 1284Q557 1284 524 1313T491 1400ZM884 1400Q884 1460 917 1487T999 1515Q1047 1515 1081 1488T1116 1400Q1116 1342 1082
+1313T999 1284Q951 1284 918 1313T884 1400Z" />
+<glyph unicode="&#x2013;" horiz-adv-x="1024" d="M82 456V645H942V456H82Z" />
+<glyph unicode="&#x2014;" horiz-adv-x="2048" d="M82 456V645H1966V456H82Z" />
+<glyph unicode="&#x2018;" horiz-adv-x="396" d="M39 961L26 983Q44 1056 72 1139T134 1305T201 1462H371Q352 1383 332 1294T296 1119T268 961H39Z" />
+<glyph unicode="&#x2019;" horiz-adv-x="396" d="M356 1462L371 1440Q353 1367 325 1284T263 1118T196 961H26Q40 1020 55 1085T84 1217T109 1346T128 1462H356Z" />
+<glyph unicode="&#x201a;" horiz-adv-x="543" d="M395 237L410 215Q392 142 364 59T302 -107T235 -264H65Q79 -205 94 -140T123 -8T148 121T167 237H395Z" />
+<glyph unicode="&#x201c;" horiz-adv-x="813" d="M788 1462Q768 1383 748 1294T712 1119T685 961H456L442 983Q460 1056 488 1139T550 1305T618 1462H788ZM371 1462Q351 1383 331 1294T295 1119T267 961H39L26 983Q44 1056 72 1139T133 1305T201 1462H371Z" />
+<glyph unicode="&#x201d;" horiz-adv-x="813" d="M773 1462L788 1440Q770 1367 742 1284T680 1118T613 961H441Q461 1040 481 1129T518 1305T545 1462H773ZM356 1462L370 1440Q352 1367 324 1284T262 1118T195 961H26Q40 1020 55 1085T83 1217T108 1346T127 1462H356Z" />
+<glyph unicode="&#x201e;" horiz-adv-x="944" d="M812 237L827 215Q809 142 781 59T719 -107T652 -264H480Q500 -185 520 -96T557 80T584 237H812ZM395 237L409 215Q391 142 363 59T301 -107T234 -264H65Q79 -205 94 -140T122 -8T147 121T166 237H395Z" />
+<glyph unicode="&#x2022;" horiz-adv-x="770" d="M135 748Q135 849 168 910T257 999T385 1027Q455 1027 512 999T602 910T636 748Q636 650 603 588T512 498T385 469Q314 469 258 497T168 587T135 748Z" />
+<glyph unicode="&#x2039;" horiz-adv-x="685" d="M81 565L436 997L605 903L322 553L605 202L436 108L81 538V565Z" />
+<glyph unicode="&#x203a;" horiz-adv-x="685" d="M248 997L604 565V539L248 108L80 202L362 553L80 903L248 997Z" />
+</font>
+</defs>
+</svg>
diff --git a/public/css/font/OpenSans-SemiBold.ttf b/public/css/font/OpenSans-SemiBold.ttf  Binary files differ.
diff --git a/public/css/font/OpenSans-SemiBold.woff b/public/css/font/OpenSans-SemiBold.woff  Binary files differ.
diff --git a/public/css/font/OpenSans-SemiBold.woff2 b/public/css/font/OpenSans-SemiBold.woff2  Binary files differ.
diff --git a/public/css/fonts.css b/public/css/fonts.css
@@ -0,0 +1,29 @@
+/* === Open Sans - regular */
+@font-face {
+	font-family: 'Open Sans';
+	font-style: normal;
+	font-weight: 400;
+	font-display: swap;
+	src: url("./font/OpenSans-Regular.eot");
+	src: local('Open Sans'),
+		url("./font/OpenSans-Regular.eot") format("embedded-opentype"),
+		url("./font/OpenSans-Regular.woff2") format("woff2"),
+		url("./font/OpenSans-Regular.woff") format("woff"),
+		url("./font/OpenSans-Regular.ttf") format("truetype"),
+		url("./font/OpenSans-Regular.svg") format("svg");
+}
+
+/* === Open Sans - 600 */
+@font-face {
+	font-family: 'Open Sans';
+	font-style: normal;
+	font-weight: 600;
+	font-display: swap;
+	src: url("./font/OpenSans-SemiBold.eot");
+	src: local('Open Sans'),
+		url("./font/OpenSans-SemiBold.eot") format("embedded-opentype"),
+		url("./font/OpenSans-SemiBold.woff2") format("woff2"),
+		url("./font/OpenSans-SemiBold.woff") format("woff"),
+		url("./font/OpenSans-SemiBold.ttf") format("truetype"),
+		url("./font/OpenSans-SemiBold.svg") format("svg");
+}
diff --git a/public/editor.html b/public/editor.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<html class="lp">
+	<head>
+		<title>ctucx.things</title>
+		<link href="/css/fonts.css" rel="stylesheet" type="text/css">
+		<link href="/dist/app.css" rel="stylesheet" type="text/css">
+	</head>
+	<body>
+		<div id="lp">
+			<router-view></router-view>
+		</div>
+		<script src="/dist/app.js"></script>
+	</body>
+</html>
diff --git a/public/images/handle.png b/public/images/handle.png  Binary files differ.
diff --git a/public/images/sprite.png b/public/images/sprite.png  Binary files differ.
diff --git a/public/images/sprite.psd b/public/images/sprite.psd  Binary files differ.
diff --git a/public/images/sprite2x.png b/public/images/sprite2x.png  Binary files differ.
diff --git a/public/index.php b/public/index.php
@@ -0,0 +1,198 @@
+<?php
+define('PATH', __DIR__);
+define('STORAGE_PATH', (getenv('THINGS_STORAGE_PATH') ? getenv('THINGS_STORAGE_PATH') : __DIR__));
+
+require_once PATH.'/lib/helpers.php';
+require_once PATH.'/lib/Router.php';
+require_once PATH.'/lib/LibraryRenderer.php';
+require_once PATH.'/lib/TinyHtmlMinifier.php';
+require_once PATH.'/lib/Mustache/Autoloader.php';
+
+Mustache_Autoloader::register();
+
+$endpoints = [
+	'editor' => function() {
+		exit(file_get_contents('editor.html'));
+	},
+
+	'renderList' => function ($id = NULL) {
+		global $storage;
+
+		if ( $storage['library'] === "" ) exit("No data yet!");
+
+		$library = json_decode($storage['library'], true);
+		$listIds = array_column($library['lists'], 'id');
+
+		if ( array_search($id, $listIds) === false ) {
+			header('Location: /list/'.$library['defaultListId']);
+			exit();
+		}
+
+		$tpl          = new Mustache_Engine(['loader' => new Mustache_Loader_FilesystemLoader(PATH.'/templates')]);
+		$minifier     = new TinyHtmlMinifier([]);
+		$templateData = new LibraryRenderer($library, (int) $id);
+	
+		exit($minifier->minify($tpl->render('view', $templateData)));
+	},
+
+
+	'exportCSV' => function ($id) {
+		global $storage;
+
+		if ( !isset($_COOKIE['lp']) || $_COOKIE['lp'] !== $storage['sessionCookie'] ) {
+			response(401, 'Invalid session');
+		} else {
+			$mgToWeight  = function(int $value, string $unit) {
+				if ( $unit === 'g' ) {
+					return number_format(($value / 1000), 0);
+				} elseif ( $unit === 'kg' ) {
+					return number_format(($value / 1000000), 2);
+				}
+			};
+
+			$library     = json_decode($storage['library'], true);
+			$categoryIds = array_column($library['categories'], 'id');
+			$itemIds     = array_column($library['items'], 'id');
+			$listIds     = array_column($library['lists'], 'id');
+			$listId      = array_search($id, $listIds);
+
+			if ( $listId === false ) exit('List not found!');
+
+			$output = fopen('php://output', 'w');
+			$list   = $library['lists'][$listId];
+
+			header('Content-type: text/csv');
+			fputcsv($output, ['name','category', 'description', 'qty', 'weight', 'unit', 'url', 'price', 'isWorn', 'isConsumable']);
+
+			foreach ( $list['categoryIds'] as $id ) {
+				$categoryId = array_search($id, $categoryIds);
+				$category   = $library['categories'][$categoryId];
+
+				foreach ( $category['categoryItems'] as $item ) {
+					$itemId = array_search($item['itemId'], $itemIds);
+					$item   = array_merge($item, $library['items'][$itemId]);
+
+					fputcsv($output, [
+						$item['name'],
+						$category['name'],
+						$item['description'],
+						$item['qty'],
+						$mgToWeight($item['weight'], $item['authorUnit']),
+						$item['authorUnit'],
+						$item['url'],
+						$item['price'],
+						(bool)$item['worn'],
+						(bool)$item['consumable'],
+					]);
+				}
+			}
+
+			fclose($output);
+		}
+	},
+
+	'getLibrary' => function () {
+		global $storage, $requestBody;
+
+		if ( isset($_COOKIE['lp']) && $_COOKIE['lp'] === $storage['sessionCookie'] ) {
+			response(200, NULL, [
+				'library'   => $storage['library'],
+				'syncToken' => $storage['syncToken'],
+			]);
+		}
+
+		if ( !isset($requestBody['password']) || md5($requestBody['password']) !== $storage['password'] ) {
+			response(404, 'Incorrect credentials!');
+		} else {
+			$storage['sessionCookie'] = bin2hex(random_bytes(40));
+			setcookie("lp", $storage['sessionCookie'], [
+				'expires' => time() + 3600, 
+				'path'    => "/",
+				'domain'  => $_SERVER['HTTP_HOST'],
+				'secure'  => true
+			]);
+
+			file_put_contents(STORAGE_PATH.'/storage.json', json_encode($storage));
+
+			response(200, NULL, [
+				'library'   => $storage['library'],
+				'syncToken' => $storage['syncToken'],
+			]);
+		}
+	},
+
+	'saveLibrary' => function () {
+		global $storage, $requestBody;
+
+		if ( !isset($_COOKIE['lp']) || $_COOKIE['lp'] !== $storage['sessionCookie'] ) {
+			response(401, 'Invalid session');
+		} else {
+			if ( isset($requestBody['library']) && isset($requestBody['syncToken']) ) {
+				if ( $storage['syncToken'] === $requestBody['syncToken'] ) {
+
+					$storage['syncToken']++;
+					$storage['library'] = $requestBody['library'];
+
+					file_put_contents(STORAGE_PATH.'/storage.json', json_encode($storage));
+
+					response(200, 'success', [
+						'syncToken' => $storage['syncToken'],
+					]);
+
+				} else {
+					response(400, 'Your list is out of date - please refresh your browser.');
+				}
+			} else {
+				response(400, 'Missing arguments!');
+			}
+		}
+	},
+
+	'changePassword' => function () {
+		global $storage, $requestBody;
+
+		if ( !isset($_COOKIE['lp']) || $_COOKIE['lp'] !== $storage['sessionCookie'] ) {
+			response(401, 'Invalid session');
+		} else {
+			if ( isset($requestBody['currentPassword']) && isset($requestBody['newPassword']) ) {
+				if ( md5($requestBody['currentPassword']) === $storage['password'] ) {
+					$storage['password'] = md5($requestBody['newPassword']);
+
+					file_put_contents(STORAGE_PATH.'/storage.json', json_encode($storage));
+				} else {
+					response(400, 'Incorrect credentials!');
+				}
+			} else {
+				response(400, 'Missing arguments!');
+			}	
+		}	
+	},
+];
+
+$requestBody = json_decode(file_get_contents('php://input'), true);
+
+if ( !file_exists(STORAGE_PATH . '/storage.json') ) {
+	$storage = [
+		'password'      => 'ae2d699aca20886f6bed96a0425c6168',
+		'sessionCookie' => '',
+		'library'       => "",
+		'syncToken'     => 0,
+	];
+} else {
+	$storage = json_decode(file_get_contents(STORAGE_PATH . '/storage.json'), true);
+}
+
+Router::add('GET',  '/',               $endpoints['renderList']);
+Router::add('GET',  '/list/([0-9]*)',  $endpoints['renderList']);
+Router::add('GET',  '/editor',         $endpoints['editor']);
+Router::add('GET',  '/login',          $endpoints['editor']);
+Router::add('GET',  '/csv/([0-9]*)',   $endpoints['exportCSV']);
+Router::add('POST', '/getLibrary',     $endpoints['getLibrary']);
+Router::add('POST', '/saveLibrary',    $endpoints['saveLibrary']);
+Router::add('POST', '/changePassword', $endpoints['changePassword']);
+
+Router::pathNotFound(function() {
+	header("Loctaion: /");
+});
+
+Router::run('/');
diff --git a/public/lib/LibraryRenderer.php b/public/lib/LibraryRenderer.php
@@ -0,0 +1,122 @@
+<?php
+require_once 'Parsedown.php';
+require_once 'helpers.php';
+
+class LibraryRenderer {
+	public  array       $show;
+	public  array       $lists;
+	public  array       $categories;
+	public  array       $units;
+
+	public  string|bool $chartData;
+	public  string      $currencySymbol;
+	public  string      $totalUnit;
+	public  string      $listName;
+	public  string      $listDescription;
+
+	public  int         $listTotalQty;
+	public  int         $listTotalWeight;
+	public  float       $listTotalWeightDisplay;
+
+	public function __construct (array $library, int $listId) {
+		$parsedown   = new Parsedown();
+
+		$categoryIds = array_column($library['categories'], 'id');
+		$itemIds     = array_column($library['items'], 'id');
+		$listIds     = array_column($library['lists'], 'id');
+
+		$listId      = array_search($listId, $listIds);
+
+		$show        = array_merge($library['optionalFields'], [
+			'weight' => ($library['lists'][$listId]['totalWeight'] !== 0),	
+		]);
+
+		$this->show                   = $show;
+		$this->units                  = renderUnits($library['totalUnit']);
+
+		$this->currencySymbol         = $library['currencySymbol'];
+		$this->totalUnit              = $library['totalUnit'];
+		$this->listName               = $library['lists'][$listId]['name'];
+		$this->listDescription        = $parsedown->text($library['lists'][$listId]['description']);
+
+		$this->listTotalQty           = $library['lists'][$listId]['totalQty'];
+		$this->listTotalWeight        = $library['lists'][$listId]['totalWeight'];
+		$this->listTotalWeightDisplay = mgToWeight($library['lists'][$listId]['totalWeight'], $library['totalUnit']);
+
+
+		foreach ( $library['lists'] as $id => $list ) {
+			$this->lists[] = [
+				'id'     => $list['id'],
+				'name'   => $list['name'],
+				'active' => ($id == $listId)
+			];
+		}
+
+
+		$list      = $library['lists'][$listId];
+		$chartData = [
+			'points' => [],
+			'total'  => 0,
+		];		
+
+		foreach ( $list['categoryIds'] as $id ) {
+			$categoryId  = array_search($id, $categoryIds);
+
+			$points      = [];
+
+			$category                          = $library['categories'][$categoryId];
+			$category['items']                 = [];
+			$category['subtotalWeightDisplay'] = mgToWeight($category['subtotalWeight'], $library['totalUnit']);
+
+			foreach ( $category['categoryItems'] as $item ) {
+				$itemId = array_search($item['itemId'], $itemIds);
+				$item   = array_merge($item, $library['items'][$itemId]);
+
+				$item['displayWeight'] = mgToWeight($item['weight'], $item['authorUnit']);
+				$item['units']         = renderUnits($item['authorUnit']);
+
+				$itemWeight = ($item['weight'] * $item['qty']);
+				$itemName   = $item['name'].': '.mgToWeight($item['weight'], $item['authorUnit']).' '.$item['authorUnit'];
+
+				$category['items'][] = $item;
+
+				// calculate chartData
+				if ( $itemWeight == 0 ) continue;
+				if ( $item['qty'] > 1 ) $itemName .= ' x '.$item['qty'];
+
+				$points[] = [
+					'id'      => $item['id'],
+					'value'   => $itemWeight,
+					'name'    => $itemName,
+					'percent' => ($itemWeight / $category['subtotalWeight']),
+				];
+			}
+
+			$this->categories[]    = $category;
+
+			//calculate chartData
+			if ( $list['totalWeight'] == 0 ) continue;
+			$chartData['total']   += $category['subtotalWeight'];
+			$chartData['points'][] = [
+				'id'            => $category['id'],
+				'name'          => $category['name'].': '.mgToWeight($category['subtotalWeight'], $library['totalUnit']).' '.$library['totalUnit'],
+				'color'         => $category['color'],
+				'total'         => $category['subtotalWeight'],
+				'points'        => $points,
+				'visiblePoints' => false,
+			];
+		}
+
+		//calculate chartData
+		if ( $chartData['total'] !== 0 ) {
+			foreach ( $chartData['points'] as $id => $point ) {
+				$chartData['points'][$id]['percent'] = ($point['total'] / $chartData['total']);
+			}
+
+			$this->chartData = rawurlencode(json_encode($chartData));
+		} else {
+			$this->chartData = false;
+		}
+	}
+
+}
diff --git a/public/lib/Mustache/Autoloader.php b/public/lib/Mustache/Autoloader.php
@@ -0,0 +1,88 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache class autoloader.
+ */
+class Mustache_Autoloader
+{
+    private $baseDir;
+
+    /**
+     * An array where the key is the baseDir and the key is an instance of this
+     * class.
+     *
+     * @var array
+     */
+    private static $instances;
+
+    /**
+     * Autoloader constructor.
+     *
+     * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..')
+     */
+    public function __construct($baseDir = null)
+    {
+        if ($baseDir === null) {
+            $baseDir = dirname(__FILE__) . '/..';
+        }
+
+        // realpath doesn't always work, for example, with stream URIs
+        $realDir = realpath($baseDir);
+        if (is_dir($realDir)) {
+            $this->baseDir = $realDir;
+        } else {
+            $this->baseDir = $baseDir;
+        }
+    }
+
+    /**
+     * Register a new instance as an SPL autoloader.
+     *
+     * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..')
+     *
+     * @return Mustache_Autoloader Registered Autoloader instance
+     */
+    public static function register($baseDir = null)
+    {
+        $key = $baseDir ? $baseDir : 0;
+
+        if (!isset(self::$instances[$key])) {
+            self::$instances[$key] = new self($baseDir);
+        }
+
+        $loader = self::$instances[$key];
+        spl_autoload_register(array($loader, 'autoload'));
+
+        return $loader;
+    }
+
+    /**
+     * Autoload Mustache classes.
+     *
+     * @param string $class
+     */
+    public function autoload($class)
+    {
+        if ($class[0] === '\\') {
+            $class = substr($class, 1);
+        }
+
+        if (strpos($class, 'Mustache') !== 0) {
+            return;
+        }
+
+        $file = sprintf('%s/%s.php', $this->baseDir, str_replace('_', '/', $class));
+        if (is_file($file)) {
+            require $file;
+        }
+    }
+}
diff --git a/public/lib/Mustache/Cache.php b/public/lib/Mustache/Cache.php
@@ -0,0 +1,43 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Cache interface.
+ *
+ * Interface for caching and loading Mustache_Template classes
+ * generated by the Mustache_Compiler.
+ */
+interface Mustache_Cache
+{
+    /**
+     * Load a compiled Mustache_Template class from cache.
+     *
+     * @param string $key
+     *
+     * @return bool indicates successfully class load
+     */
+    public function load($key);
+
+    /**
+     * Cache and load a compiled Mustache_Template class.
+     *
+     * @param string $key
+     * @param string $value
+     */
+    public function cache($key, $value);
+
+    /**
+     * Set a logger instance.
+     *
+     * @param Mustache_Logger|Psr\Log\LoggerInterface $logger
+     */
+    public function setLogger($logger = null);
+}
diff --git a/public/lib/Mustache/Cache/AbstractCache.php b/public/lib/Mustache/Cache/AbstractCache.php
@@ -0,0 +1,60 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Abstract Mustache Cache class.
+ *
+ * Provides logging support to child implementations.
+ *
+ * @abstract
+ */
+abstract class Mustache_Cache_AbstractCache implements Mustache_Cache
+{
+    private $logger = null;
+
+    /**
+     * Get the current logger instance.
+     *
+     * @return Mustache_Logger|Psr\Log\LoggerInterface
+     */
+    public function getLogger()
+    {
+        return $this->logger;
+    }
+
+    /**
+     * Set a logger instance.
+     *
+     * @param Mustache_Logger|Psr\Log\LoggerInterface $logger
+     */
+    public function setLogger($logger = null)
+    {
+        if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) {
+            throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.');
+        }
+
+        $this->logger = $logger;
+    }
+
+    /**
+     * Add a log record if logging is enabled.
+     *
+     * @param string $level   The logging level
+     * @param string $message The log message
+     * @param array  $context The log context
+     */
+    protected function log($level, $message, array $context = array())
+    {
+        if (isset($this->logger)) {
+            $this->logger->log($level, $message, $context);
+        }
+    }
+}
diff --git a/public/lib/Mustache/Cache/FilesystemCache.php b/public/lib/Mustache/Cache/FilesystemCache.php
@@ -0,0 +1,161 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Cache filesystem implementation.
+ *
+ * A FilesystemCache instance caches Mustache Template classes from the filesystem by name:
+ *
+ *     $cache = new Mustache_Cache_FilesystemCache(dirname(__FILE__).'/cache');
+ *     $cache->cache($className, $compiledSource);
+ *
+ * The FilesystemCache benefits from any opcode caching that may be setup in your environment. So do that, k?
+ */
+class Mustache_Cache_FilesystemCache extends Mustache_Cache_AbstractCache
+{
+    private $baseDir;
+    private $fileMode;
+
+    /**
+     * Filesystem cache constructor.
+     *
+     * @param string $baseDir  Directory for compiled templates
+     * @param int    $fileMode Override default permissions for cache files. Defaults to using the system-defined umask
+     */
+    public function __construct($baseDir, $fileMode = null)
+    {
+        $this->baseDir = $baseDir;
+        $this->fileMode = $fileMode;
+    }
+
+    /**
+     * Load the class from cache using `require_once`.
+     *
+     * @param string $key
+     *
+     * @return bool
+     */
+    public function load($key)
+    {
+        $fileName = $this->getCacheFilename($key);
+        if (!is_file($fileName)) {
+            return false;
+        }
+
+        require_once $fileName;
+
+        return true;
+    }
+
+    /**
+     * Cache and load the compiled class.
+     *
+     * @param string $key
+     * @param string $value
+     */
+    public function cache($key, $value)
+    {
+        $fileName = $this->getCacheFilename($key);
+
+        $this->log(
+            Mustache_Logger::DEBUG,
+            'Writing to template cache: "{fileName}"',
+            array('fileName' => $fileName)
+        );
+
+        $this->writeFile($fileName, $value);
+        $this->load($key);
+    }
+
+    /**
+     * Build the cache filename.
+     * Subclasses should override for custom cache directory structures.
+     *
+     * @param string $name
+     *
+     * @return string
+     */
+    protected function getCacheFilename($name)
+    {
+        return sprintf('%s/%s.php', $this->baseDir, $name);
+    }
+
+    /**
+     * Create cache directory.
+     *
+     * @throws Mustache_Exception_RuntimeException If unable to create directory
+     *
+     * @param string $fileName
+     *
+     * @return string
+     */
+    private function buildDirectoryForFilename($fileName)
+    {
+        $dirName = dirname($fileName);
+        if (!is_dir($dirName)) {
+            $this->log(
+                Mustache_Logger::INFO,
+                'Creating Mustache template cache directory: "{dirName}"',
+                array('dirName' => $dirName)
+            );
+
+            @mkdir($dirName, 0777, true);
+            // @codeCoverageIgnoreStart
+            if (!is_dir($dirName)) {
+                throw new Mustache_Exception_RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName));
+            }
+            // @codeCoverageIgnoreEnd
+        }
+
+        return $dirName;
+    }
+
+    /**
+     * Write cache file.
+     *
+     * @throws Mustache_Exception_RuntimeException If unable to write file
+     *
+     * @param string $fileName
+     * @param string $value
+     */
+    private function writeFile($fileName, $value)
+    {
+        $dirName = $this->buildDirectoryForFilename($fileName);
+
+        $this->log(
+            Mustache_Logger::DEBUG,
+            'Caching compiled template to "{fileName}"',
+            array('fileName' => $fileName)
+        );
+
+        $tempFile = tempnam($dirName, basename($fileName));
+        if (false !== @file_put_contents($tempFile, $value)) {
+            if (@rename($tempFile, $fileName)) {
+                $mode = isset($this->fileMode) ? $this->fileMode : (0666 & ~umask());
+                @chmod($fileName, $mode);
+
+                return;
+            }
+
+            // @codeCoverageIgnoreStart
+            $this->log(
+                Mustache_Logger::ERROR,
+                'Unable to rename Mustache temp cache file: "{tempName}" -> "{fileName}"',
+                array('tempName' => $tempFile, 'fileName' => $fileName)
+            );
+            // @codeCoverageIgnoreEnd
+        }
+
+        // @codeCoverageIgnoreStart
+        throw new Mustache_Exception_RuntimeException(sprintf('Failed to write cache file "%s".', $fileName));
+        // @codeCoverageIgnoreEnd
+    }
+}
diff --git a/public/lib/Mustache/Cache/NoopCache.php b/public/lib/Mustache/Cache/NoopCache.php
@@ -0,0 +1,47 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Cache in-memory implementation.
+ *
+ * The in-memory cache is used for uncached lambda section templates. It's also useful during development, but is not
+ * recommended for production use.
+ */
+class Mustache_Cache_NoopCache extends Mustache_Cache_AbstractCache
+{
+    /**
+     * Loads nothing. Move along.
+     *
+     * @param string $key
+     *
+     * @return bool
+     */
+    public function load($key)
+    {
+        return false;
+    }
+
+    /**
+     * Loads the compiled Mustache Template class without caching.
+     *
+     * @param string $key
+     * @param string $value
+     */
+    public function cache($key, $value)
+    {
+        $this->log(
+            Mustache_Logger::WARNING,
+            'Template cache disabled, evaluating "{className}" class at runtime',
+            array('className' => $key)
+        );
+        eval('?>' . $value);
+    }
+}
diff --git a/public/lib/Mustache/Compiler.php b/public/lib/Mustache/Compiler.php
@@ -0,0 +1,689 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Compiler class.
+ *
+ * This class is responsible for turning a Mustache token parse tree into normal PHP source code.
+ */
+class Mustache_Compiler
+{
+    private $pragmas;
+    private $defaultPragmas = array();
+    private $sections;
+    private $blocks;
+    private $source;
+    private $indentNextLine;
+    private $customEscape;
+    private $entityFlags;
+    private $charset;
+    private $strictCallables;
+
+    /**
+     * Compile a Mustache token parse tree into PHP source code.
+     *
+     * @param string $source          Mustache Template source code
+     * @param string $tree            Parse tree of Mustache tokens
+     * @param string $name            Mustache Template class name
+     * @param bool   $customEscape    (default: false)
+     * @param string $charset         (default: 'UTF-8')
+     * @param bool   $strictCallables (default: false)
+     * @param int    $entityFlags     (default: ENT_COMPAT)
+     *
+     * @return string Generated PHP source code
+     */
+    public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT)
+    {
+        $this->pragmas         = $this->defaultPragmas;
+        $this->sections        = array();
+        $this->blocks          = array();
+        $this->source          = $source;
+        $this->indentNextLine  = true;
+        $this->customEscape    = $customEscape;
+        $this->entityFlags     = $entityFlags;
+        $this->charset         = $charset;
+        $this->strictCallables = $strictCallables;
+
+        return $this->writeCode($tree, $name);
+    }
+
+    /**
+     * Enable pragmas across all templates, regardless of the presence of pragma
+     * tags in the individual templates.
+     *
+     * @internal Users should set global pragmas in Mustache_Engine, not here :)
+     *
+     * @param string[] $pragmas
+     */
+    public function setPragmas(array $pragmas)
+    {
+        $this->pragmas = array();
+        foreach ($pragmas as $pragma) {
+            $this->pragmas[$pragma] = true;
+        }
+        $this->defaultPragmas = $this->pragmas;
+    }
+
+    /**
+     * Helper function for walking the Mustache token parse tree.
+     *
+     * @throws Mustache_Exception_SyntaxException upon encountering unknown token types
+     *
+     * @param array $tree  Parse tree of Mustache tokens
+     * @param int   $level (default: 0)
+     *
+     * @return string Generated PHP source code
+     */
+    private function walk(array $tree, $level = 0)
+    {
+        $code = '';
+        $level++;
+        foreach ($tree as $node) {
+            switch ($node[Mustache_Tokenizer::TYPE]) {
+                case Mustache_Tokenizer::T_PRAGMA:
+                    $this->pragmas[$node[Mustache_Tokenizer::NAME]] = true;
+                    break;
+
+                case Mustache_Tokenizer::T_SECTION:
+                    $code .= $this->section(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
+                        $node[Mustache_Tokenizer::INDEX],
+                        $node[Mustache_Tokenizer::END],
+                        $node[Mustache_Tokenizer::OTAG],
+                        $node[Mustache_Tokenizer::CTAG],
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_INVERTED:
+                    $code .= $this->invertedSection(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_PARTIAL:
+                    $code .= $this->partial(
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_PARENT:
+                    $code .= $this->parent(
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
+                        $node[Mustache_Tokenizer::NODES],
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_BLOCK_ARG:
+                    $code .= $this->blockArg(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        $node[Mustache_Tokenizer::INDEX],
+                        $node[Mustache_Tokenizer::END],
+                        $node[Mustache_Tokenizer::OTAG],
+                        $node[Mustache_Tokenizer::CTAG],
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_BLOCK_VAR:
+                    $code .= $this->blockVar(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        $node[Mustache_Tokenizer::INDEX],
+                        $node[Mustache_Tokenizer::END],
+                        $node[Mustache_Tokenizer::OTAG],
+                        $node[Mustache_Tokenizer::CTAG],
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_COMMENT:
+                    break;
+
+                case Mustache_Tokenizer::T_ESCAPED:
+                case Mustache_Tokenizer::T_UNESCAPED:
+                case Mustache_Tokenizer::T_UNESCAPED_2:
+                    $code .= $this->variable(
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
+                        $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_ESCAPED,
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_TEXT:
+                    $code .= $this->text($node[Mustache_Tokenizer::VALUE], $level);
+                    break;
+
+                default:
+                    throw new Mustache_Exception_SyntaxException(sprintf('Unknown token type: %s', $node[Mustache_Tokenizer::TYPE]), $node);
+            }
+        }
+
+        return $code;
+    }
+
+    const KLASS = '<?php
+
+        class %s extends Mustache_Template
+        {
+            private $lambdaHelper;%s
+
+            public function renderInternal(Mustache_Context $context, $indent = \'\')
+            {
+                $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context);
+                $buffer = \'\';
+        %s
+
+                return $buffer;
+            }
+        %s
+        %s
+        }';
+
+    const KLASS_NO_LAMBDAS = '<?php
+
+        class %s extends Mustache_Template
+        {%s
+            public function renderInternal(Mustache_Context $context, $indent = \'\')
+            {
+                $buffer = \'\';
+        %s
+
+                return $buffer;
+            }
+        }';
+
+    const STRICT_CALLABLE = 'protected $strictCallables = true;';
+
+    /**
+     * Generate Mustache Template class PHP source.
+     *
+     * @param array  $tree Parse tree of Mustache tokens
+     * @param string $name Mustache Template class name
+     *
+     * @return string Generated PHP source code
+     */
+    private function writeCode($tree, $name)
+    {
+        $code     = $this->walk($tree);
+        $sections = implode("\n", $this->sections);
+        $blocks   = implode("\n", $this->blocks);
+        $klass    = empty($this->sections) && empty($this->blocks) ? self::KLASS_NO_LAMBDAS : self::KLASS;
+
+        $callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : '';
+
+        return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections, $blocks);
+    }
+
+    const BLOCK_VAR = '
+        $blockFunction = $context->findInBlock(%s);
+        if (is_callable($blockFunction)) {
+            $buffer .= call_user_func($blockFunction, $context);
+        %s}
+    ';
+
+    const BLOCK_VAR_ELSE = '} else {%s';
+
+    /**
+     * Generate Mustache Template inheritance block variable PHP source.
+     *
+     * @param array  $nodes Array of child tokens
+     * @param string $id    Section name
+     * @param int    $start Section start offset
+     * @param int    $end   Section end offset
+     * @param string $otag  Current Mustache opening tag
+     * @param string $ctag  Current Mustache closing tag
+     * @param int    $level
+     *
+     * @return string Generated PHP source code
+     */
+    private function blockVar($nodes, $id, $start, $end, $otag, $ctag, $level)
+    {
+        $id = var_export($id, true);
+
+        $else = $this->walk($nodes, $level);
+        if ($else !== '') {
+            $else = sprintf($this->prepare(self::BLOCK_VAR_ELSE, $level + 1, false, true), $else);
+        }
+
+        return sprintf($this->prepare(self::BLOCK_VAR, $level), $id, $else);
+    }
+
+    const BLOCK_ARG = '%s => array($this, \'block%s\'),';
+
+    /**
+     * Generate Mustache Template inheritance block argument PHP source.
+     *
+     * @param array  $nodes Array of child tokens
+     * @param string $id    Section name
+     * @param int    $start Section start offset
+     * @param int    $end   Section end offset
+     * @param string $otag  Current Mustache opening tag
+     * @param string $ctag  Current Mustache closing tag
+     * @param int    $level
+     *
+     * @return string Generated PHP source code
+     */
+    private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level)
+    {
+        $key = $this->block($nodes);
+        $id = var_export($id, true);
+
+        return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key);
+    }
+
+    const BLOCK_FUNCTION = '
+        public function block%s($context)
+        {
+            $indent = $buffer = \'\';%s
+
+            return $buffer;
+        }
+    ';
+
+    /**
+     * Generate Mustache Template inheritance block function PHP source.
+     *
+     * @param array $nodes Array of child tokens
+     *
+     * @return string key of new block function
+     */
+    private function block($nodes)
+    {
+        $code = $this->walk($nodes, 0);
+        $key = ucfirst(md5($code));
+
+        if (!isset($this->blocks[$key])) {
+            $this->blocks[$key] = sprintf($this->prepare(self::BLOCK_FUNCTION, 0), $key, $code);
+        }
+
+        return $key;
+    }
+
+    const SECTION_CALL = '
+        $value = $context->%s(%s);%s
+        $buffer .= $this->section%s($context, $indent, $value);
+    ';
+
+    const SECTION = '
+        private function section%s(Mustache_Context $context, $indent, $value)
+        {
+            $buffer = \'\';
+
+            if (%s) {
+                $source = %s;
+                $result = (string) call_user_func($value, $source, %s);
+                if (strpos($result, \'{{\') === false) {
+                    $buffer .= $result;
+                } else {
+                    $buffer .= $this->mustache
+                        ->loadLambda($result%s)
+                        ->renderInternal($context);
+                }
+            } elseif (!empty($value)) {
+                $values = $this->isIterable($value) ? $value : array($value);
+                foreach ($values as $value) {
+                    $context->push($value);
+                    %s
+                    $context->pop();
+                }
+            }
+
+            return $buffer;
+        }
+    ';
+
+    /**
+     * Generate Mustache Template section PHP source.
+     *
+     * @param array    $nodes   Array of child tokens
+     * @param string   $id      Section name
+     * @param string[] $filters Array of filters
+     * @param int      $start   Section start offset
+     * @param int      $end     Section end offset
+     * @param string   $otag    Current Mustache opening tag
+     * @param string   $ctag    Current Mustache closing tag
+     * @param int      $level
+     *
+     * @return string Generated section PHP source code
+     */
+    private function section($nodes, $id, $filters, $start, $end, $otag, $ctag, $level)
+    {
+        $source   = var_export(substr($this->source, $start, $end - $start), true);
+        $callable = $this->getCallable();
+
+        if ($otag !== '{{' || $ctag !== '}}') {
+            $delimTag = var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
+            $helper = sprintf('$this->lambdaHelper->withDelimiters(%s)', $delimTag);
+            $delims = ', ' . $delimTag;
+        } else {
+            $helper = '$this->lambdaHelper';
+            $delims = '';
+        }
+
+        $key = ucfirst(md5($delims . "\n" . $source));
+
+        if (!isset($this->sections[$key])) {
+            $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $delims, $this->walk($nodes, 2));
+        }
+
+        $method  = $this->getFindMethod($id);
+        $id      = var_export($id, true);
+        $filters = $this->getFilters($filters, $level);
+
+        return sprintf($this->prepare(self::SECTION_CALL, $level), $method, $id, $filters, $key);
+    }
+
+    const INVERTED_SECTION = '
+        $value = $context->%s(%s);%s
+        if (empty($value)) {
+            %s
+        }
+    ';
+
+    /**
+     * Generate Mustache Template inverted section PHP source.
+     *
+     * @param array    $nodes   Array of child tokens
+     * @param string   $id      Section name
+     * @param string[] $filters Array of filters
+     * @param int      $level
+     *
+     * @return string Generated inverted section PHP source code
+     */
+    private function invertedSection($nodes, $id, $filters, $level)
+    {
+        $method  = $this->getFindMethod($id);
+        $id      = var_export($id, true);
+        $filters = $this->getFilters($filters, $level);
+
+        return sprintf($this->prepare(self::INVERTED_SECTION, $level), $method, $id, $filters, $this->walk($nodes, $level));
+    }
+
+    const PARTIAL_INDENT = ', $indent . %s';
+    const PARTIAL = '
+        if ($partial = $this->mustache->loadPartial(%s)) {
+            $buffer .= $partial->renderInternal($context%s);
+        }
+    ';
+
+    /**
+     * Generate Mustache Template partial call PHP source.
+     *
+     * @param string $id     Partial name
+     * @param string $indent Whitespace indent to apply to partial
+     * @param int    $level
+     *
+     * @return string Generated partial call PHP source code
+     */
+    private function partial($id, $indent, $level)
+    {
+        if ($indent !== '') {
+            $indentParam = sprintf(self::PARTIAL_INDENT, var_export($indent, true));
+        } else {
+            $indentParam = '';
+        }
+
+        return sprintf(
+            $this->prepare(self::PARTIAL, $level),
+            var_export($id, true),
+            $indentParam
+        );
+    }
+
+    const PARENT = '
+        if ($parent = $this->mustache->loadPartial(%s)) {
+            $context->pushBlockContext(array(%s
+            ));
+            $buffer .= $parent->renderInternal($context, $indent);
+            $context->popBlockContext();
+        }
+    ';
+
+    const PARENT_NO_CONTEXT = '
+        if ($parent = $this->mustache->loadPartial(%s)) {
+            $buffer .= $parent->renderInternal($context, $indent);
+        }
+    ';
+
+    /**
+     * Generate Mustache Template inheritance parent call PHP source.
+     *
+     * @param string $id       Parent tag name
+     * @param string $indent   Whitespace indent to apply to parent
+     * @param array  $children Child nodes
+     * @param int    $level
+     *
+     * @return string Generated PHP source code
+     */
+    private function parent($id, $indent, array $children, $level)
+    {
+        $realChildren = array_filter($children, array(__CLASS__, 'onlyBlockArgs'));
+
+        if (empty($realChildren)) {
+            return sprintf($this->prepare(self::PARENT_NO_CONTEXT, $level), var_export($id, true));
+        }
+
+        return sprintf(
+            $this->prepare(self::PARENT, $level),
+            var_export($id, true),
+            $this->walk($realChildren, $level + 1)
+        );
+    }
+
+    /**
+     * Helper method for filtering out non-block-arg tokens.
+     *
+     * @param array $node
+     *
+     * @return bool True if $node is a block arg token
+     */
+    private static function onlyBlockArgs(array $node)
+    {
+        return $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_BLOCK_ARG;
+    }
+
+    const VARIABLE = '
+        $value = $this->resolveValue($context->%s(%s), $context);%s
+        $buffer .= %s($value === null ? \'\' : %s);
+    ';
+
+    /**
+     * Generate Mustache Template variable interpolation PHP source.
+     *
+     * @param string   $id      Variable name
+     * @param string[] $filters Array of filters
+     * @param bool     $escape  Escape the variable value for output?
+     * @param int      $level
+     *
+     * @return string Generated variable interpolation PHP source
+     */
+    private function variable($id, $filters, $escape, $level)
+    {
+        $method  = $this->getFindMethod($id);
+        $id      = ($method !== 'last') ? var_export($id, true) : '';
+        $filters = $this->getFilters($filters, $level);
+        $value   = $escape ? $this->getEscape() : '$value';
+
+        return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value);
+    }
+
+    const FILTER = '
+        $filter = $context->%s(%s);
+        if (!(%s)) {
+            throw new Mustache_Exception_UnknownFilterException(%s);
+        }
+        $value = call_user_func($filter, $value);%s
+    ';
+
+    /**
+     * Generate Mustache Template variable filtering PHP source.
+     *
+     * @param string[] $filters Array of filters
+     * @param int      $level
+     *
+     * @return string Generated filter PHP source
+     */
+    private function getFilters(array $filters, $level)
+    {
+        if (empty($filters)) {
+            return '';
+        }
+
+        $name     = array_shift($filters);
+        $method   = $this->getFindMethod($name);
+        $filter   = ($method !== 'last') ? var_export($name, true) : '';
+        $callable = $this->getCallable('$filter');
+        $msg      = var_export($name, true);
+
+        return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $callable, $msg, $this->getFilters($filters, $level));
+    }
+
+    const LINE = '$buffer .= "\n";';
+    const TEXT = '$buffer .= %s%s;';
+
+    /**
+     * Generate Mustache Template output Buffer call PHP source.
+     *
+     * @param string $text
+     * @param int    $level
+     *
+     * @return string Generated output Buffer call PHP source
+     */
+    private function text($text, $level)
+    {
+        $indentNextLine = (substr($text, -1) === "\n");
+        $code = sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true));
+        $this->indentNextLine = $indentNextLine;
+
+        return $code;
+    }
+
+    /**
+     * Prepare PHP source code snippet for output.
+     *
+     * @param string $text
+     * @param int    $bonus          Additional indent level (default: 0)
+     * @param bool   $prependNewline Prepend a newline to the snippet? (default: true)
+     * @param bool   $appendNewline  Append a newline to the snippet? (default: false)
+     *
+     * @return string PHP source code snippet
+     */
+    private function prepare($text, $bonus = 0, $prependNewline = true, $appendNewline = false)
+    {
+        $text = ($prependNewline ? "\n" : '') . trim($text);
+        if ($prependNewline) {
+            $bonus++;
+        }
+        if ($appendNewline) {
+            $text .= "\n";
+        }
+
+        return preg_replace("/\n( {8})?/", "\n" . str_repeat(' ', $bonus * 4), $text);
+    }
+
+    const DEFAULT_ESCAPE = 'htmlspecialchars(%s, %s, %s)';
+    const CUSTOM_ESCAPE  = 'call_user_func($this->mustache->getEscape(), %s)';
+
+    /**
+     * Get the current escaper.
+     *
+     * @param string $value (default: '$value')
+     *
+     * @return string Either a custom callback, or an inline call to `htmlspecialchars`
+     */
+    private function getEscape($value = '$value')
+    {
+        if ($this->customEscape) {
+            return sprintf(self::CUSTOM_ESCAPE, $value);
+        }
+
+        return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->entityFlags, true), var_export($this->charset, true));
+    }
+
+    /**
+     * Select the appropriate Context `find` method for a given $id.
+     *
+     * The return value will be one of `find`, `findDot`, `findAnchoredDot` or `last`.
+     *
+     * @see Mustache_Context::find
+     * @see Mustache_Context::findDot
+     * @see Mustache_Context::last
+     *
+     * @param string $id Variable name
+     *
+     * @return string `find` method name
+     */
+    private function getFindMethod($id)
+    {
+        if ($id === '.') {
+            return 'last';
+        }
+
+        if (isset($this->pragmas[Mustache_Engine::PRAGMA_ANCHORED_DOT]) && $this->pragmas[Mustache_Engine::PRAGMA_ANCHORED_DOT]) {
+            if (substr($id, 0, 1) === '.') {
+                return 'findAnchoredDot';
+            }
+        }
+
+        if (strpos($id, '.') === false) {
+            return 'find';
+        }
+
+        return 'findDot';
+    }
+
+    const IS_CALLABLE        = '!is_string(%s) && is_callable(%s)';
+    const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)';
+
+    /**
+     * Helper function to compile strict vs lax "is callable" logic.
+     *
+     * @param string $variable (default: '$value')
+     *
+     * @return string "is callable" logic
+     */
+    private function getCallable($variable = '$value')
+    {
+        $tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE;
+
+        return sprintf($tpl, $variable, $variable);
+    }
+
+    const LINE_INDENT = '$indent . ';
+
+    /**
+     * Get the current $indent prefix to write to the buffer.
+     *
+     * @return string "$indent . " or ""
+     */
+    private function flushIndent()
+    {
+        if (!$this->indentNextLine) {
+            return '';
+        }
+
+        $this->indentNextLine = false;
+
+        return self::LINE_INDENT;
+    }
+}
diff --git a/public/lib/Mustache/Context.php b/public/lib/Mustache/Context.php
@@ -0,0 +1,242 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template rendering Context.
+ */
+class Mustache_Context
+{
+    private $stack      = array();
+    private $blockStack = array();
+
+    /**
+     * Mustache rendering Context constructor.
+     *
+     * @param mixed $context Default rendering context (default: null)
+     */
+    public function __construct($context = null)
+    {
+        if ($context !== null) {
+            $this->stack = array($context);
+        }
+    }
+
+    /**
+     * Push a new Context frame onto the stack.
+     *
+     * @param mixed $value Object or array to use for context
+     */
+    public function push($value)
+    {
+        array_push($this->stack, $value);
+    }
+
+    /**
+     * Push a new Context frame onto the block context stack.
+     *
+     * @param mixed $value Object or array to use for block context
+     */
+    public function pushBlockContext($value)
+    {
+        array_push($this->blockStack, $value);
+    }
+
+    /**
+     * Pop the last Context frame from the stack.
+     *
+     * @return mixed Last Context frame (object or array)
+     */
+    public function pop()
+    {
+        return array_pop($this->stack);
+    }
+
+    /**
+     * Pop the last block Context frame from the stack.
+     *
+     * @return mixed Last block Context frame (object or array)
+     */
+    public function popBlockContext()
+    {
+        return array_pop($this->blockStack);
+    }
+
+    /**
+     * Get the last Context frame.
+     *
+     * @return mixed Last Context frame (object or array)
+     */
+    public function last()
+    {
+        return end($this->stack);
+    }
+
+    /**
+     * Find a variable in the Context stack.
+     *
+     * Starting with the last Context frame (the context of the innermost section), and working back to the top-level
+     * rendering context, look for a variable with the given name:
+     *
+     *  * If the Context frame is an associative array which contains the key $id, returns the value of that element.
+     *  * If the Context frame is an object, this will check first for a public method, then a public property named
+     *    $id. Failing both of these, it will try `__isset` and `__get` magic methods.
+     *  * If a value named $id is not found in any Context frame, returns an empty string.
+     *
+     * @param string $id Variable name
+     *
+     * @return mixed Variable value, or '' if not found
+     */
+    public function find($id)
+    {
+        return $this->findVariableInStack($id, $this->stack);
+    }
+
+    /**
+     * Find a 'dot notation' variable in the Context stack.
+     *
+     * Note that dot notation traversal bubbles through scope differently than the regular find method. After finding
+     * the initial chunk of the dotted name, each subsequent chunk is searched for only within the value of the previous
+     * result. For example, given the following context stack:
+     *
+     *     $data = array(
+     *         'name' => 'Fred',
+     *         'child' => array(
+     *             'name' => 'Bob'
+     *         ),
+     *     );
+     *
+     * ... and the Mustache following template:
+     *
+     *     {{ child.name }}
+     *
+     * ... the `name` value is only searched for within the `child` value of the global Context, not within parent
+     * Context frames.
+     *
+     * @param string $id Dotted variable selector
+     *
+     * @return mixed Variable value, or '' if not found
+     */
+    public function findDot($id)
+    {
+        $chunks = explode('.', $id);
+        $first  = array_shift($chunks);
+        $value  = $this->findVariableInStack($first, $this->stack);
+
+        foreach ($chunks as $chunk) {
+            if ($value === '') {
+                return $value;
+            }
+
+            $value = $this->findVariableInStack($chunk, array($value));
+        }
+
+        return $value;
+    }
+
+    /**
+     * Find an 'anchored dot notation' variable in the Context stack.
+     *
+     * This is the same as findDot(), except it looks in the top of the context
+     * stack for the first value, rather than searching the whole context stack
+     * and starting from there.
+     *
+     * @see Mustache_Context::findDot
+     *
+     * @throws Mustache_Exception_InvalidArgumentException if given an invalid anchored dot $id
+     *
+     * @param string $id Dotted variable selector
+     *
+     * @return mixed Variable value, or '' if not found
+     */
+    public function findAnchoredDot($id)
+    {
+        $chunks = explode('.', $id);
+        $first  = array_shift($chunks);
+        if ($first !== '') {
+            throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected id for findAnchoredDot: %s', $id));
+        }
+
+        $value  = $this->last();
+
+        foreach ($chunks as $chunk) {
+            if ($value === '') {
+                return $value;
+            }
+
+            $value = $this->findVariableInStack($chunk, array($value));
+        }
+
+        return $value;
+    }
+
+    /**
+     * Find an argument in the block context stack.
+     *
+     * @param string $id
+     *
+     * @return mixed Variable value, or '' if not found
+     */
+    public function findInBlock($id)
+    {
+        foreach ($this->blockStack as $context) {
+            if (array_key_exists($id, $context)) {
+                return $context[$id];
+            }
+        }
+
+        return '';
+    }
+
+    /**
+     * Helper function to find a variable in the Context stack.
+     *
+     * @see Mustache_Context::find
+     *
+     * @param string $id    Variable name
+     * @param array  $stack Context stack
+     *
+     * @return mixed Variable value, or '' if not found
+     */
+    private function findVariableInStack($id, array $stack)
+    {
+        for ($i = count($stack) - 1; $i >= 0; $i--) {
+            $frame = &$stack[$i];
+
+            switch (gettype($frame)) {
+                case 'object':
+                    if (!($frame instanceof Closure)) {
+                        // Note that is_callable() *will not work here*
+                        // See https://github.com/bobthecow/mustache.php/wiki/Magic-Methods
+                        if (method_exists($frame, $id)) {
+                            return $frame->$id();
+                        }
+
+                        if (isset($frame->$id)) {
+                            return $frame->$id;
+                        }
+
+                        if ($frame instanceof ArrayAccess && isset($frame[$id])) {
+                            return $frame[$id];
+                        }
+                    }
+                    break;
+
+                case 'array':
+                    if (array_key_exists($id, $frame)) {
+                        return $frame[$id];
+                    }
+                    break;
+            }
+        }
+
+        return '';
+    }
+}
diff --git a/public/lib/Mustache/Engine.php b/public/lib/Mustache/Engine.php
@@ -0,0 +1,829 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache implementation in PHP.
+ *
+ * {@link http://defunkt.github.com/mustache}
+ *
+ * Mustache is a framework-agnostic logic-less templating language. It enforces separation of view
+ * logic from template files. In fact, it is not even possible to embed logic in the template.
+ *
+ * This is very, very rad.
+ *
+ * @author Justin Hileman {@link http://justinhileman.com}
+ */
+class Mustache_Engine
+{
+    const VERSION        = '2.14.2';
+    const SPEC_VERSION   = '1.2.2';
+
+    const PRAGMA_FILTERS      = 'FILTERS';
+    const PRAGMA_BLOCKS       = 'BLOCKS';
+    const PRAGMA_ANCHORED_DOT = 'ANCHORED-DOT';
+
+    // Known pragmas
+    private static $knownPragmas = array(
+        self::PRAGMA_FILTERS      => true,
+        self::PRAGMA_BLOCKS       => true,
+        self::PRAGMA_ANCHORED_DOT => true,
+    );
+
+    // Template cache
+    private $templates = array();
+
+    // Environment
+    private $templateClassPrefix = '__Mustache_';
+    private $cache;
+    private $lambdaCache;
+    private $cacheLambdaTemplates = false;
+    private $loader;
+    private $partialsLoader;
+    private $helpers;
+    private $escape;
+    private $entityFlags = ENT_COMPAT;
+    private $charset = 'UTF-8';
+    private $logger;
+    private $strictCallables = false;
+    private $pragmas = array();
+    private $delimiters;
+
+    // Services
+    private $tokenizer;
+    private $parser;
+    private $compiler;
+
+    /**
+     * Mustache class constructor.
+     *
+     * Passing an $options array allows overriding certain Mustache options during instantiation:
+     *
+     *     $options = array(
+     *         // The class prefix for compiled templates. Defaults to '__Mustache_'.
+     *         'template_class_prefix' => '__MyTemplates_',
+     *
+     *         // A Mustache cache instance or a cache directory string for compiled templates.
+     *         // Mustache will not cache templates unless this is set.
+     *         'cache' => dirname(__FILE__).'/tmp/cache/mustache',
+     *
+     *         // Override default permissions for cache files. Defaults to using the system-defined umask. It is
+     *         // *strongly* recommended that you configure your umask properly rather than overriding permissions here.
+     *         'cache_file_mode' => 0666,
+     *
+     *         // Optionally, enable caching for lambda section templates. This is generally not recommended, as lambda
+     *         // sections are often too dynamic to benefit from caching.
+     *         'cache_lambda_templates' => true,
+     *
+     *         // Customize the tag delimiters used by this engine instance. Note that overriding here changes the
+     *         // delimiters used to parse all templates and partials loaded by this instance. To override just for a
+     *         // single template, use an inline "change delimiters" tag at the start of the template file:
+     *         //
+     *         //     {{=<% %>=}}
+     *         //
+     *         'delimiters' => '<% %>',
+     *
+     *         // A Mustache template loader instance. Uses a StringLoader if not specified.
+     *         'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
+     *
+     *         // A Mustache loader instance for partials.
+     *         'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
+     *
+     *         // An array of Mustache partials. Useful for quick-and-dirty string template loading, but not as
+     *         // efficient or lazy as a Filesystem (or database) loader.
+     *         'partials' => array('foo' => file_get_contents(dirname(__FILE__).'/views/partials/foo.mustache')),
+     *
+     *         // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order
+     *         // sections), or any other valid Mustache context value. They will be prepended to the context stack,
+     *         // so they will be available in any template loaded by this Mustache instance.
+     *         'helpers' => array('i18n' => function ($text) {
+     *             // do something translatey here...
+     *         }),
+     *
+     *         // An 'escape' callback, responsible for escaping double-mustache variables.
+     *         'escape' => function ($value) {
+     *             return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8');
+     *         },
+     *
+     *         // Type argument for `htmlspecialchars`.  Defaults to ENT_COMPAT.  You may prefer ENT_QUOTES.
+     *         'entity_flags' => ENT_QUOTES,
+     *
+     *         // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'.
+     *         'charset' => 'ISO-8859-1',
+     *
+     *         // A Mustache Logger instance. No logging will occur unless this is set. Using a PSR-3 compatible
+     *         // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is
+     *         // available as well:
+     *         'logger' => new Mustache_Logger_StreamLogger('php://stderr'),
+     *
+     *         // Only treat Closure instances and invokable classes as callable. If true, values like
+     *         // `array('ClassName', 'methodName')` and `array($classInstance, 'methodName')`, which are traditionally
+     *         // "callable" in PHP, are not called to resolve variables for interpolation or section contexts. This
+     *         // helps protect against arbitrary code execution when user input is passed directly into the template.
+     *         // This currently defaults to false, but will default to true in v3.0.
+     *         'strict_callables' => true,
+     *
+     *         // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual
+     *         // templates.
+     *         'pragmas' => [Mustache_Engine::PRAGMA_FILTERS],
+     *     );
+     *
+     * @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable
+     *
+     * @param array $options (default: array())
+     */
+    public function __construct(array $options = array())
+    {
+        if (isset($options['template_class_prefix'])) {
+            if ((string) $options['template_class_prefix'] === '') {
+                throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "template_class_prefix" must not be empty');
+            }
+
+            $this->templateClassPrefix = $options['template_class_prefix'];
+        }
+
+        if (isset($options['cache'])) {
+            $cache = $options['cache'];
+
+            if (is_string($cache)) {
+                $mode  = isset($options['cache_file_mode']) ? $options['cache_file_mode'] : null;
+                $cache = new Mustache_Cache_FilesystemCache($cache, $mode);
+            }
+
+            $this->setCache($cache);
+        }
+
+        if (isset($options['cache_lambda_templates'])) {
+            $this->cacheLambdaTemplates = (bool) $options['cache_lambda_templates'];
+        }
+
+        if (isset($options['loader'])) {
+            $this->setLoader($options['loader']);
+        }
+
+        if (isset($options['partials_loader'])) {
+            $this->setPartialsLoader($options['partials_loader']);
+        }
+
+        if (isset($options['partials'])) {
+            $this->setPartials($options['partials']);
+        }
+
+        if (isset($options['helpers'])) {
+            $this->setHelpers($options['helpers']);
+        }
+
+        if (isset($options['escape'])) {
+            if (!is_callable($options['escape'])) {
+                throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "escape" option must be callable');
+            }
+
+            $this->escape = $options['escape'];
+        }
+
+        if (isset($options['entity_flags'])) {
+            $this->entityFlags = $options['entity_flags'];
+        }
+
+        if (isset($options['charset'])) {
+            $this->charset = $options['charset'];
+        }
+
+        if (isset($options['logger'])) {
+            $this->setLogger($options['logger']);
+        }
+
+        if (isset($options['strict_callables'])) {
+            $this->strictCallables = $options['strict_callables'];
+        }
+
+        if (isset($options['delimiters'])) {
+            $this->delimiters = $options['delimiters'];
+        }
+
+        if (isset($options['pragmas'])) {
+            foreach ($options['pragmas'] as $pragma) {
+                if (!isset(self::$knownPragmas[$pragma])) {
+                    throw new Mustache_Exception_InvalidArgumentException(sprintf('Unknown pragma: "%s".', $pragma));
+                }
+                $this->pragmas[$pragma] = true;
+            }
+        }
+    }
+
+    /**
+     * Shortcut 'render' invocation.
+     *
+     * Equivalent to calling `$mustache->loadTemplate($template)->render($context);`
+     *
+     * @see Mustache_Engine::loadTemplate
+     * @see Mustache_Template::render
+     *
+     * @param string $template
+     * @param mixed  $context  (default: array())
+     *
+     * @return string Rendered template
+     */
+    public function render($template, $context = array())
+    {
+        return $this->loadTemplate($template)->render($context);
+    }
+
+    /**
+     * Get the current Mustache escape callback.
+     *
+     * @return callable|null
+     */
+    public function getEscape()
+    {
+        return $this->escape;
+    }
+
+    /**
+     * Get the current Mustache entitity type to escape.
+     *
+     * @return int
+     */
+    public function getEntityFlags()
+    {
+        return $this->entityFlags;
+    }
+
+    /**
+     * Get the current Mustache character set.
+     *
+     * @return string
+     */
+    public function getCharset()
+    {
+        return $this->charset;
+    }
+
+    /**
+     * Get the current globally enabled pragmas.
+     *
+     * @return array
+     */
+    public function getPragmas()
+    {
+        return array_keys($this->pragmas);
+    }
+
+    /**
+     * Set the Mustache template Loader instance.
+     *
+     * @param Mustache_Loader $loader
+     */
+    public function setLoader(Mustache_Loader $loader)
+    {
+        $this->loader = $loader;
+    }
+
+    /**
+     * Get the current Mustache template Loader instance.
+     *
+     * If no Loader instance has been explicitly specified, this method will instantiate and return
+     * a StringLoader instance.
+     *
+     * @return Mustache_Loader
+     */
+    public function getLoader()
+    {
+        if (!isset($this->loader)) {
+            $this->loader = new Mustache_Loader_StringLoader();
+        }
+
+        return $this->loader;
+    }
+
+    /**
+     * Set the Mustache partials Loader instance.
+     *
+     * @param Mustache_Loader $partialsLoader
+     */
+    public function setPartialsLoader(Mustache_Loader $partialsLoader)
+    {
+        $this->partialsLoader = $partialsLoader;
+    }
+
+    /**
+     * Get the current Mustache partials Loader instance.
+     *
+     * If no Loader instance has been explicitly specified, this method will instantiate and return
+     * an ArrayLoader instance.
+     *
+     * @return Mustache_Loader
+     */
+    public function getPartialsLoader()
+    {
+        if (!isset($this->partialsLoader)) {
+            $this->partialsLoader = new Mustache_Loader_ArrayLoader();
+        }
+
+        return $this->partialsLoader;
+    }
+
+    /**
+     * Set partials for the current partials Loader instance.
+     *
+     * @throws Mustache_Exception_RuntimeException If the current Loader instance is immutable
+     *
+     * @param array $partials (default: array())
+     */
+    public function setPartials(array $partials = array())
+    {
+        if (!isset($this->partialsLoader)) {
+            $this->partialsLoader = new Mustache_Loader_ArrayLoader();
+        }
+
+        if (!$this->partialsLoader instanceof Mustache_Loader_MutableLoader) {
+            throw new Mustache_Exception_RuntimeException('Unable to set partials on an immutable Mustache Loader instance');
+        }
+
+        $this->partialsLoader->setTemplates($partials);
+    }
+
+    /**
+     * Set an array of Mustache helpers.
+     *
+     * An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or
+     * any other valid Mustache context value. They will be prepended to the context stack, so they will be available in
+     * any template loaded by this Mustache instance.
+     *
+     * @throws Mustache_Exception_InvalidArgumentException if $helpers is not an array or Traversable
+     *
+     * @param array|Traversable $helpers
+     */
+    public function setHelpers($helpers)
+    {
+        if (!is_array($helpers) && !$helpers instanceof Traversable) {
+            throw new Mustache_Exception_InvalidArgumentException('setHelpers expects an array of helpers');
+        }
+
+        $this->getHelpers()->clear();
+
+        foreach ($helpers as $name => $helper) {
+            $this->addHelper($name, $helper);
+        }
+    }
+
+    /**
+     * Get the current set of Mustache helpers.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @return Mustache_HelperCollection
+     */
+    public function getHelpers()
+    {
+        if (!isset($this->helpers)) {
+            $this->helpers = new Mustache_HelperCollection();
+        }
+
+        return $this->helpers;
+    }
+
+    /**
+     * Add a new Mustache helper.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @param string $name
+     * @param mixed  $helper
+     */
+    public function addHelper($name, $helper)
+    {
+        $this->getHelpers()->add($name, $helper);
+    }
+
+    /**
+     * Get a Mustache helper by name.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @param string $name
+     *
+     * @return mixed Helper
+     */
+    public function getHelper($name)
+    {
+        return $this->getHelpers()->get($name);
+    }
+
+    /**
+     * Check whether this Mustache instance has a helper.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @param string $name
+     *
+     * @return bool True if the helper is present
+     */
+    public function hasHelper($name)
+    {
+        return $this->getHelpers()->has($name);
+    }
+
+    /**
+     * Remove a helper by name.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @param string $name
+     */
+    public function removeHelper($name)
+    {
+        $this->getHelpers()->remove($name);
+    }
+
+    /**
+     * Set the Mustache Logger instance.
+     *
+     * @throws Mustache_Exception_InvalidArgumentException If logger is not an instance of Mustache_Logger or Psr\Log\LoggerInterface
+     *
+     * @param Mustache_Logger|Psr\Log\LoggerInterface $logger
+     */
+    public function setLogger($logger = null)
+    {
+        if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) {
+            throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.');
+        }
+
+        if ($this->getCache()->getLogger() === null) {
+            $this->getCache()->setLogger($logger);
+        }
+
+        $this->logger = $logger;
+    }
+
+    /**
+     * Get the current Mustache Logger instance.
+     *
+     * @return Mustache_Logger|Psr\Log\LoggerInterface
+     */
+    public function getLogger()
+    {
+        return $this->logger;
+    }
+
+    /**
+     * Set the Mustache Tokenizer instance.
+     *
+     * @param Mustache_Tokenizer $tokenizer
+     */
+    public function setTokenizer(Mustache_Tokenizer $tokenizer)
+    {
+        $this->tokenizer = $tokenizer;
+    }
+
+    /**
+     * Get the current Mustache Tokenizer instance.
+     *
+     * If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one.
+     *
+     * @return Mustache_Tokenizer
+     */
+    public function getTokenizer()
+    {
+        if (!isset($this->tokenizer)) {
+            $this->tokenizer = new Mustache_Tokenizer();
+        }
+
+        return $this->tokenizer;
+    }
+
+    /**
+     * Set the Mustache Parser instance.
+     *
+     * @param Mustache_Parser $parser
+     */
+    public function setParser(Mustache_Parser $parser)
+    {
+        $this->parser = $parser;
+    }
+
+    /**
+     * Get the current Mustache Parser instance.
+     *
+     * If no Parser instance has been explicitly specified, this method will instantiate and return a new one.
+     *
+     * @return Mustache_Parser
+     */
+    public function getParser()
+    {
+        if (!isset($this->parser)) {
+            $this->parser = new Mustache_Parser();
+        }
+
+        return $this->parser;
+    }
+
+    /**
+     * Set the Mustache Compiler instance.
+     *
+     * @param Mustache_Compiler $compiler
+     */
+    public function setCompiler(Mustache_Compiler $compiler)
+    {
+        $this->compiler = $compiler;
+    }
+
+    /**
+     * Get the current Mustache Compiler instance.
+     *
+     * If no Compiler instance has been explicitly specified, this method will instantiate and return a new one.
+     *
+     * @return Mustache_Compiler
+     */
+    public function getCompiler()
+    {
+        if (!isset($this->compiler)) {
+            $this->compiler = new Mustache_Compiler();
+        }
+
+        return $this->compiler;
+    }
+
+    /**
+     * Set the Mustache Cache instance.
+     *
+     * @param Mustache_Cache $cache
+     */
+    public function setCache(Mustache_Cache $cache)
+    {
+        if (isset($this->logger) && $cache->getLogger() === null) {
+            $cache->setLogger($this->getLogger());
+        }
+
+        $this->cache = $cache;
+    }
+
+    /**
+     * Get the current Mustache Cache instance.
+     *
+     * If no Cache instance has been explicitly specified, this method will instantiate and return a new one.
+     *
+     * @return Mustache_Cache
+     */
+    public function getCache()
+    {
+        if (!isset($this->cache)) {
+            $this->setCache(new Mustache_Cache_NoopCache());
+        }
+
+        return $this->cache;
+    }
+
+    /**
+     * Get the current Lambda Cache instance.
+     *
+     * If 'cache_lambda_templates' is enabled, this is the default cache instance. Otherwise, it is a NoopCache.
+     *
+     * @see Mustache_Engine::getCache
+     *
+     * @return Mustache_Cache
+     */
+    protected function getLambdaCache()
+    {
+        if ($this->cacheLambdaTemplates) {
+            return $this->getCache();
+        }
+
+        if (!isset($this->lambdaCache)) {
+            $this->lambdaCache = new Mustache_Cache_NoopCache();
+        }
+
+        return $this->lambdaCache;
+    }
+
+    /**
+     * Helper method to generate a Mustache template class.
+     *
+     * This method must be updated any time options are added which make it so
+     * the same template could be parsed and compiled multiple different ways.
+     *
+     * @param string|Mustache_Source $source
+     *
+     * @return string Mustache Template class name
+     */
+    public function getTemplateClassName($source)
+    {
+        // For the most part, adding a new option here should do the trick.
+        //
+        // Pick a value here which is unique for each possible way the template
+        // could be compiled... but not necessarily unique per option value. See
+        // escape below, which only needs to differentiate between 'custom' and
+        // 'default' escapes.
+        //
+        // Keep this list in alphabetical order :)
+        $chunks = array(
+            'charset'         => $this->charset,
+            'delimiters'      => $this->delimiters ? $this->delimiters : '{{ }}',
+            'entityFlags'     => $this->entityFlags,
+            'escape'          => isset($this->escape) ? 'custom' : 'default',
+            'key'             => ($source instanceof Mustache_Source) ? $source->getKey() : 'source',
+            'pragmas'         => $this->getPragmas(),
+            'strictCallables' => $this->strictCallables,
+            'version'         => self::VERSION,
+        );
+
+        $key = json_encode($chunks);
+
+        // Template Source instances have already provided their own source key. For strings, just include the whole
+        // source string in the md5 hash.
+        if (!$source instanceof Mustache_Source) {
+            $key .= "\n" . $source;
+        }
+
+        return $this->templateClassPrefix . md5($key);
+    }
+
+    /**
+     * Load a Mustache Template by name.
+     *
+     * @param string $name
+     *
+     * @return Mustache_Template
+     */
+    public function loadTemplate($name)
+    {
+        return $this->loadSource($this->getLoader()->load($name));
+    }
+
+    /**
+     * Load a Mustache partial Template by name.
+     *
+     * This is a helper method used internally by Template instances for loading partial templates. You can most likely
+     * ignore it completely.
+     *
+     * @param string $name
+     *
+     * @return Mustache_Template
+     */
+    public function loadPartial($name)
+    {
+        try {
+            if (isset($this->partialsLoader)) {
+                $loader = $this->partialsLoader;
+            } elseif (isset($this->loader) && !$this->loader instanceof Mustache_Loader_StringLoader) {
+                $loader = $this->loader;
+            } else {
+                throw new Mustache_Exception_UnknownTemplateException($name);
+            }
+
+            return $this->loadSource($loader->load($name));
+        } catch (Mustache_Exception_UnknownTemplateException $e) {
+            // If the named partial cannot be found, log then return null.
+            $this->log(
+                Mustache_Logger::WARNING,
+                'Partial not found: "{name}"',
+                array('name' => $e->getTemplateName())
+            );
+        }
+    }
+
+    /**
+     * Load a Mustache lambda Template by source.
+     *
+     * This is a helper method used by Template instances to generate subtemplates for Lambda sections. You can most
+     * likely ignore it completely.
+     *
+     * @param string $source
+     * @param string $delims (default: null)
+     *
+     * @return Mustache_Template
+     */
+    public function loadLambda($source, $delims = null)
+    {
+        if ($delims !== null) {
+            $source = $delims . "\n" . $source;
+        }
+
+        return $this->loadSource($source, $this->getLambdaCache());
+    }
+
+    /**
+     * Instantiate and return a Mustache Template instance by source.
+     *
+     * Optionally provide a Mustache_Cache instance. This is used internally by Mustache_Engine::loadLambda to respect
+     * the 'cache_lambda_templates' configuration option.
+     *
+     * @see Mustache_Engine::loadTemplate
+     * @see Mustache_Engine::loadPartial
+     * @see Mustache_Engine::loadLambda
+     *
+     * @param string|Mustache_Source $source
+     * @param Mustache_Cache         $cache  (default: null)
+     *
+     * @return Mustache_Template
+     */
+    private function loadSource($source, Mustache_Cache $cache = null)
+    {
+        $className = $this->getTemplateClassName($source);
+
+        if (!isset($this->templates[$className])) {
+            if ($cache === null) {
+                $cache = $this->getCache();
+            }
+
+            if (!class_exists($className, false)) {
+                if (!$cache->load($className)) {
+                    $compiled = $this->compile($source);
+                    $cache->cache($className, $compiled);
+                }
+            }
+
+            $this->log(
+                Mustache_Logger::DEBUG,
+                'Instantiating template: "{className}"',
+                array('className' => $className)
+            );
+
+            $this->templates[$className] = new $className($this);
+        }
+
+        return $this->templates[$className];
+    }
+
+    /**
+     * Helper method to tokenize a Mustache template.
+     *
+     * @see Mustache_Tokenizer::scan
+     *
+     * @param string $source
+     *
+     * @return array Tokens
+     */
+    private function tokenize($source)
+    {
+        return $this->getTokenizer()->scan($source, $this->delimiters);
+    }
+
+    /**
+     * Helper method to parse a Mustache template.
+     *
+     * @see Mustache_Parser::parse
+     *
+     * @param string $source
+     *
+     * @return array Token tree
+     */
+    private function parse($source)
+    {
+        $parser = $this->getParser();
+        $parser->setPragmas($this->getPragmas());
+
+        return $parser->parse($this->tokenize($source));
+    }
+
+    /**
+     * Helper method to compile a Mustache template.
+     *
+     * @see Mustache_Compiler::compile
+     *
+     * @param string|Mustache_Source $source
+     *
+     * @return string generated Mustache template class code
+     */
+    private function compile($source)
+    {
+        $name = $this->getTemplateClassName($source);
+
+        $this->log(
+            Mustache_Logger::INFO,
+            'Compiling template to "{className}" class',
+            array('className' => $name)
+        );
+
+        if ($source instanceof Mustache_Source) {
+            $source = $source->getSource();
+        }
+        $tree = $this->parse($source);
+
+        $compiler = $this->getCompiler();
+        $compiler->setPragmas($this->getPragmas());
+
+        return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
+    }
+
+    /**
+     * Add a log record if logging is enabled.
+     *
+     * @param int    $level   The logging level
+     * @param string $message The log message
+     * @param array  $context The log context
+     */
+    private function log($level, $message, array $context = array())
+    {
+        if (isset($this->logger)) {
+            $this->logger->log($level, $message, $context);
+        }
+    }
+}
diff --git a/public/lib/Mustache/Exception.php b/public/lib/Mustache/Exception.php
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Exception interface.
+ */
+interface Mustache_Exception
+{
+    // This space intentionally left blank.
+}
diff --git a/public/lib/Mustache/Exception/InvalidArgumentException.php b/public/lib/Mustache/Exception/InvalidArgumentException.php
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Invalid argument exception.
+ */
+class Mustache_Exception_InvalidArgumentException extends InvalidArgumentException implements Mustache_Exception
+{
+    // This space intentionally left blank.
+}
diff --git a/public/lib/Mustache/Exception/LogicException.php b/public/lib/Mustache/Exception/LogicException.php
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Logic exception.
+ */
+class Mustache_Exception_LogicException extends LogicException implements Mustache_Exception
+{
+    // This space intentionally left blank.
+}
diff --git a/public/lib/Mustache/Exception/RuntimeException.php b/public/lib/Mustache/Exception/RuntimeException.php
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Runtime exception.
+ */
+class Mustache_Exception_RuntimeException extends RuntimeException implements Mustache_Exception
+{
+    // This space intentionally left blank.
+}
diff --git a/public/lib/Mustache/Exception/SyntaxException.php b/public/lib/Mustache/Exception/SyntaxException.php
@@ -0,0 +1,41 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache syntax exception.
+ */
+class Mustache_Exception_SyntaxException extends LogicException implements Mustache_Exception
+{
+    protected $token;
+
+    /**
+     * @param string    $msg
+     * @param array     $token
+     * @param Exception $previous
+     */
+    public function __construct($msg, array $token, Exception $previous = null)
+    {
+        $this->token = $token;
+        if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
+            parent::__construct($msg, 0, $previous);
+        } else {
+            parent::__construct($msg); // @codeCoverageIgnore
+        }
+    }
+
+    /**
+     * @return array
+     */
+    public function getToken()
+    {
+        return $this->token;
+    }
+}
diff --git a/public/lib/Mustache/Exception/UnknownFilterException.php b/public/lib/Mustache/Exception/UnknownFilterException.php
@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown filter exception.
+ */
+class Mustache_Exception_UnknownFilterException extends UnexpectedValueException implements Mustache_Exception
+{
+    protected $filterName;
+
+    /**
+     * @param string    $filterName
+     * @param Exception $previous
+     */
+    public function __construct($filterName, Exception $previous = null)
+    {
+        $this->filterName = $filterName;
+        $message = sprintf('Unknown filter: %s', $filterName);
+        if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
+            parent::__construct($message, 0, $previous);
+        } else {
+            parent::__construct($message); // @codeCoverageIgnore
+        }
+    }
+
+    public function getFilterName()
+    {
+        return $this->filterName;
+    }
+}
diff --git a/public/lib/Mustache/Exception/UnknownHelperException.php b/public/lib/Mustache/Exception/UnknownHelperException.php
@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown helper exception.
+ */
+class Mustache_Exception_UnknownHelperException extends InvalidArgumentException implements Mustache_Exception
+{
+    protected $helperName;
+
+    /**
+     * @param string    $helperName
+     * @param Exception $previous
+     */
+    public function __construct($helperName, Exception $previous = null)
+    {
+        $this->helperName = $helperName;
+        $message = sprintf('Unknown helper: %s', $helperName);
+        if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
+            parent::__construct($message, 0, $previous);
+        } else {
+            parent::__construct($message); // @codeCoverageIgnore
+        }
+    }
+
+    public function getHelperName()
+    {
+        return $this->helperName;
+    }
+}
diff --git a/public/lib/Mustache/Exception/UnknownTemplateException.php b/public/lib/Mustache/Exception/UnknownTemplateException.php
@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown template exception.
+ */
+class Mustache_Exception_UnknownTemplateException extends InvalidArgumentException implements Mustache_Exception
+{
+    protected $templateName;
+
+    /**
+     * @param string    $templateName
+     * @param Exception $previous
+     */
+    public function __construct($templateName, Exception $previous = null)
+    {
+        $this->templateName = $templateName;
+        $message = sprintf('Unknown template: %s', $templateName);
+        if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
+            parent::__construct($message, 0, $previous);
+        } else {
+            parent::__construct($message); // @codeCoverageIgnore
+        }
+    }
+
+    public function getTemplateName()
+    {
+        return $this->templateName;
+    }
+}
diff --git a/public/lib/Mustache/HelperCollection.php b/public/lib/Mustache/HelperCollection.php
@@ -0,0 +1,172 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A collection of helpers for a Mustache instance.
+ */
+class Mustache_HelperCollection
+{
+    private $helpers = array();
+
+    /**
+     * Helper Collection constructor.
+     *
+     * Optionally accepts an array (or Traversable) of `$name => $helper` pairs.
+     *
+     * @throws Mustache_Exception_InvalidArgumentException if the $helpers argument isn't an array or Traversable
+     *
+     * @param array|Traversable $helpers (default: null)
+     */
+    public function __construct($helpers = null)
+    {
+        if ($helpers === null) {
+            return;
+        }
+
+        if (!is_array($helpers) && !$helpers instanceof Traversable) {
+            throw new Mustache_Exception_InvalidArgumentException('HelperCollection constructor expects an array of helpers');
+        }
+
+        foreach ($helpers as $name => $helper) {
+            $this->add($name, $helper);
+        }
+    }
+
+    /**
+     * Magic mutator.
+     *
+     * @see Mustache_HelperCollection::add
+     *
+     * @param string $name
+     * @param mixed  $helper
+     */
+    public function __set($name, $helper)
+    {
+        $this->add($name, $helper);
+    }
+
+    /**
+     * Add a helper to this collection.
+     *
+     * @param string $name
+     * @param mixed  $helper
+     */
+    public function add($name, $helper)
+    {
+        $this->helpers[$name] = $helper;
+    }
+
+    /**
+     * Magic accessor.
+     *
+     * @see Mustache_HelperCollection::get
+     *
+     * @param string $name
+     *
+     * @return mixed Helper
+     */
+    public function __get($name)
+    {
+        return $this->get($name);
+    }
+
+    /**
+     * Get a helper by name.
+     *
+     * @throws Mustache_Exception_UnknownHelperException If helper does not exist
+     *
+     * @param string $name
+     *
+     * @return mixed Helper
+     */
+    public function get($name)
+    {
+        if (!$this->has($name)) {
+            throw new Mustache_Exception_UnknownHelperException($name);
+        }
+
+        return $this->helpers[$name];
+    }
+
+    /**
+     * Magic isset().
+     *
+     * @see Mustache_HelperCollection::has
+     *
+     * @param string $name
+     *
+     * @return bool True if helper is present
+     */
+    public function __isset($name)
+    {
+        return $this->has($name);
+    }
+
+    /**
+     * Check whether a given helper is present in the collection.
+     *
+     * @param string $name
+     *
+     * @return bool True if helper is present
+     */
+    public function has($name)
+    {
+        return array_key_exists($name, $this->helpers);
+    }
+
+    /**
+     * Magic unset().
+     *
+     * @see Mustache_HelperCollection::remove
+     *
+     * @param string $name
+     */
+    public function __unset($name)
+    {
+        $this->remove($name);
+    }
+
+    /**
+     * Check whether a given helper is present in the collection.
+     *
+     * @throws Mustache_Exception_UnknownHelperException if the requested helper is not present
+     *
+     * @param string $name
+     */
+    public function remove($name)
+    {
+        if (!$this->has($name)) {
+            throw new Mustache_Exception_UnknownHelperException($name);
+        }
+
+        unset($this->helpers[$name]);
+    }
+
+    /**
+     * Clear the helper collection.
+     *
+     * Removes all helpers from this collection
+     */
+    public function clear()
+    {
+        $this->helpers = array();
+    }
+
+    /**
+     * Check whether the helper collection is empty.
+     *
+     * @return bool True if the collection is empty
+     */
+    public function isEmpty()
+    {
+        return empty($this->helpers);
+    }
+}
diff --git a/public/lib/Mustache/LambdaHelper.php b/public/lib/Mustache/LambdaHelper.php
@@ -0,0 +1,76 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Lambda Helper.
+ *
+ * Passed as the second argument to section lambdas (higher order sections),
+ * giving them access to a `render` method for rendering a string with the
+ * current context.
+ */
+class Mustache_LambdaHelper
+{
+    private $mustache;
+    private $context;
+    private $delims;
+
+    /**
+     * Mustache Lambda Helper constructor.
+     *
+     * @param Mustache_Engine  $mustache Mustache engine instance
+     * @param Mustache_Context $context  Rendering context
+     * @param string           $delims   Optional custom delimiters, in the format `{{= <% %> =}}`. (default: null)
+     */
+    public function __construct(Mustache_Engine $mustache, Mustache_Context $context, $delims = null)
+    {
+        $this->mustache = $mustache;
+        $this->context  = $context;
+        $this->delims   = $delims;
+    }
+
+    /**
+     * Render a string as a Mustache template with the current rendering context.
+     *
+     * @param string $string
+     *
+     * @return string Rendered template
+     */
+    public function render($string)
+    {
+        return $this->mustache
+            ->loadLambda((string) $string, $this->delims)
+            ->renderInternal($this->context);
+    }
+
+    /**
+     * Render a string as a Mustache template with the current rendering context.
+     *
+     * @param string $string
+     *
+     * @return string Rendered template
+     */
+    public function __invoke($string)
+    {
+        return $this->render($string);
+    }
+
+    /**
+     * Get a Lambda Helper with custom delimiters.
+     *
+     * @param string $delims Custom delimiters, in the format `{{= <% %> =}}`
+     *
+     * @return Mustache_LambdaHelper
+     */
+    public function withDelimiters($delims)
+    {
+        return new self($this->mustache, $this->context, $delims);
+    }
+}
diff --git a/public/lib/Mustache/Loader.php b/public/lib/Mustache/Loader.php
@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template Loader interface.
+ */
+interface Mustache_Loader
+{
+    /**
+     * Load a Template by name.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found
+     *
+     * @param string $name
+     *
+     * @return string|Mustache_Source Mustache Template source
+     */
+    public function load($name);
+}
diff --git a/public/lib/Mustache/Loader/ArrayLoader.php b/public/lib/Mustache/Loader/ArrayLoader.php
@@ -0,0 +1,79 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template array Loader implementation.
+ *
+ * An ArrayLoader instance loads Mustache Template source by name from an initial array:
+ *
+ *     $loader = new ArrayLoader(
+ *         'foo' => '{{ bar }}',
+ *         'baz' => 'Hey {{ qux }}!'
+ *     );
+ *
+ *     $tpl = $loader->load('foo'); // '{{ bar }}'
+ *
+ * The ArrayLoader is used internally as a partials loader by Mustache_Engine instance when an array of partials
+ * is set. It can also be used as a quick-and-dirty Template loader.
+ */
+class Mustache_Loader_ArrayLoader implements Mustache_Loader, Mustache_Loader_MutableLoader
+{
+    private $templates;
+
+    /**
+     * ArrayLoader constructor.
+     *
+     * @param array $templates Associative array of Template source (default: array())
+     */
+    public function __construct(array $templates = array())
+    {
+        $this->templates = $templates;
+    }
+
+    /**
+     * Load a Template.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        if (!isset($this->templates[$name])) {
+            throw new Mustache_Exception_UnknownTemplateException($name);
+        }
+
+        return $this->templates[$name];
+    }
+
+    /**
+     * Set an associative array of Template sources for this loader.
+     *
+     * @param array $templates
+     */
+    public function setTemplates(array $templates)
+    {
+        $this->templates = $templates;
+    }
+
+    /**
+     * Set a Template source by name.
+     *
+     * @param string $name
+     * @param string $template Mustache Template source
+     */
+    public function setTemplate($name, $template)
+    {
+        $this->templates[$name] = $template;
+    }
+}
diff --git a/public/lib/Mustache/Loader/CascadingLoader.php b/public/lib/Mustache/Loader/CascadingLoader.php
@@ -0,0 +1,69 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Template cascading loader implementation, which delegates to other
+ * Loader instances.
+ */
+class Mustache_Loader_CascadingLoader implements Mustache_Loader
+{
+    private $loaders;
+
+    /**
+     * Construct a CascadingLoader with an array of loaders.
+     *
+     *     $loader = new Mustache_Loader_CascadingLoader(array(
+     *         new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__),
+     *         new Mustache_Loader_FilesystemLoader(__DIR__.'/templates')
+     *     ));
+     *
+     * @param Mustache_Loader[] $loaders
+     */
+    public function __construct(array $loaders = array())
+    {
+        $this->loaders = array();
+        foreach ($loaders as $loader) {
+            $this->addLoader($loader);
+        }
+    }
+
+    /**
+     * Add a Loader instance.
+     *
+     * @param Mustache_Loader $loader
+     */
+    public function addLoader(Mustache_Loader $loader)
+    {
+        $this->loaders[] = $loader;
+    }
+
+    /**
+     * Load a Template by name.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        foreach ($this->loaders as $loader) {
+            try {
+                return $loader->load($name);
+            } catch (Mustache_Exception_UnknownTemplateException $e) {
+                // do nothing, check the next loader.
+            }
+        }
+
+        throw new Mustache_Exception_UnknownTemplateException($name);
+    }
+}
diff --git a/public/lib/Mustache/Loader/FilesystemLoader.php b/public/lib/Mustache/Loader/FilesystemLoader.php
@@ -0,0 +1,135 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template filesystem Loader implementation.
+ *
+ * A FilesystemLoader instance loads Mustache Template source from the filesystem by name:
+ *
+ *     $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views');
+ *     $tpl = $loader->load('foo'); // equivalent to `file_get_contents(dirname(__FILE__).'/views/foo.mustache');
+ *
+ * This is probably the most useful Mustache Loader implementation. It can be used for partials and normal Templates:
+ *
+ *     $m = new Mustache(array(
+ *          'loader'          => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
+ *          'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
+ *     ));
+ */
+class Mustache_Loader_FilesystemLoader implements Mustache_Loader
+{
+    private $baseDir;
+    private $extension = '.mustache';
+    private $templates = array();
+
+    /**
+     * Mustache filesystem Loader constructor.
+     *
+     * Passing an $options array allows overriding certain Loader options during instantiation:
+     *
+     *     $options = array(
+     *         // The filename extension used for Mustache templates. Defaults to '.mustache'
+     *         'extension' => '.ms',
+     *     );
+     *
+     * @throws Mustache_Exception_RuntimeException if $baseDir does not exist
+     *
+     * @param string $baseDir Base directory containing Mustache template files
+     * @param array  $options Array of Loader options (default: array())
+     */
+    public function __construct($baseDir, array $options = array())
+    {
+        $this->baseDir = $baseDir;
+
+        if (strpos($this->baseDir, '://') === false) {
+            $this->baseDir = realpath($this->baseDir);
+        }
+
+        if ($this->shouldCheckPath() && !is_dir($this->baseDir)) {
+            throw new Mustache_Exception_RuntimeException(sprintf('FilesystemLoader baseDir must be a directory: %s', $baseDir));
+        }
+
+        if (array_key_exists('extension', $options)) {
+            if (empty($options['extension'])) {
+                $this->extension = '';
+            } else {
+                $this->extension = '.' . ltrim($options['extension'], '.');
+            }
+        }
+    }
+
+    /**
+     * Load a Template by name.
+     *
+     *     $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views');
+     *     $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache";
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        if (!isset($this->templates[$name])) {
+            $this->templates[$name] = $this->loadFile($name);
+        }
+
+        return $this->templates[$name];
+    }
+
+    /**
+     * Helper function for loading a Mustache file by name.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    protected function loadFile($name)
+    {
+        $fileName = $this->getFileName($name);
+
+        if ($this->shouldCheckPath() && !file_exists($fileName)) {
+            throw new Mustache_Exception_UnknownTemplateException($name);
+        }
+
+        return file_get_contents($fileName);
+    }
+
+    /**
+     * Helper function for getting a Mustache template file name.
+     *
+     * @param string $name
+     *
+     * @return string Template file name
+     */
+    protected function getFileName($name)
+    {
+        $fileName = $this->baseDir . '/' . $name;
+        if (substr($fileName, 0 - strlen($this->extension)) !== $this->extension) {
+            $fileName .= $this->extension;
+        }
+
+        return $fileName;
+    }
+
+    /**
+     * Only check if baseDir is a directory and requested templates are files if
+     * baseDir is using the filesystem stream wrapper.
+     *
+     * @return bool Whether to check `is_dir` and `file_exists`
+     */
+    protected function shouldCheckPath()
+    {
+        return strpos($this->baseDir, '://') === false || strpos($this->baseDir, 'file://') === 0;
+    }
+}
diff --git a/public/lib/Mustache/Loader/InlineLoader.php b/public/lib/Mustache/Loader/InlineLoader.php
@@ -0,0 +1,123 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Template loader for inline templates.
+ *
+ * With the InlineLoader, templates can be defined at the end of any PHP source
+ * file:
+ *
+ *     $loader  = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
+ *     $hello   = $loader->load('hello');
+ *     $goodbye = $loader->load('goodbye');
+ *
+ *     __halt_compiler();
+ *
+ *     @@ hello
+ *     Hello, {{ planet }}!
+ *
+ *     @@ goodbye
+ *     Goodbye, cruel {{ planet }}
+ *
+ * Templates are deliniated by lines containing only `@@ name`.
+ *
+ * The InlineLoader is well-suited to micro-frameworks such as Silex:
+ *
+ *     $app->register(new MustacheServiceProvider, array(
+ *         'mustache.loader' => new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__)
+ *     ));
+ *
+ *     $app->get('/{name}', function ($name) use ($app) {
+ *         return $app['mustache']->render('hello', compact('name'));
+ *     })
+ *     ->value('name', 'world');
+ *
+ *     // ...
+ *
+ *     __halt_compiler();
+ *
+ *     @@ hello
+ *     Hello, {{ name }}!
+ */
+class Mustache_Loader_InlineLoader implements Mustache_Loader
+{
+    protected $fileName;
+    protected $offset;
+    protected $templates;
+
+    /**
+     * The InlineLoader requires a filename and offset to process templates.
+     *
+     * The magic constants `__FILE__` and `__COMPILER_HALT_OFFSET__` are usually
+     * perfectly suited to the job:
+     *
+     *     $loader = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
+     *
+     * Note that this only works if the loader is instantiated inside the same
+     * file as the inline templates. If the templates are located in another
+     * file, it would be necessary to manually specify the filename and offset.
+     *
+     * @param string $fileName The file to parse for inline templates
+     * @param int    $offset   A string offset for the start of the templates.
+     *                         This usually coincides with the `__halt_compiler`
+     *                         call, and the `__COMPILER_HALT_OFFSET__`
+     */
+    public function __construct($fileName, $offset)
+    {
+        if (!is_file($fileName)) {
+            throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid filename.');
+        }
+
+        if (!is_int($offset) || $offset < 0) {
+            throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid file offset.');
+        }
+
+        $this->fileName = $fileName;
+        $this->offset   = $offset;
+    }
+
+    /**
+     * Load a Template by name.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        $this->loadTemplates();
+
+        if (!array_key_exists($name, $this->templates)) {
+            throw new Mustache_Exception_UnknownTemplateException($name);
+        }
+
+        return $this->templates[$name];
+    }
+
+    /**
+     * Parse and load templates from the end of a source file.
+     */
+    protected function loadTemplates()
+    {
+        if ($this->templates === null) {
+            $this->templates = array();
+            $data = file_get_contents($this->fileName, false, null, $this->offset);
+            foreach (preg_split("/^@@(?= [\w\d\.]+$)/m", $data, -1) as $chunk) {
+                if (trim($chunk)) {
+                    list($name, $content)         = explode("\n", $chunk, 2);
+                    $this->templates[trim($name)] = trim($content);
+                }
+            }
+        }
+    }
+}
diff --git a/public/lib/Mustache/Loader/MutableLoader.php b/public/lib/Mustache/Loader/MutableLoader.php
@@ -0,0 +1,31 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template mutable Loader interface.
+ */
+interface Mustache_Loader_MutableLoader
+{
+    /**
+     * Set an associative array of Template sources for this loader.
+     *
+     * @param array $templates
+     */
+    public function setTemplates(array $templates);
+
+    /**
+     * Set a Template source by name.
+     *
+     * @param string $name
+     * @param string $template Mustache Template source
+     */
+    public function setTemplate($name, $template);
+}
diff --git a/public/lib/Mustache/Loader/ProductionFilesystemLoader.php b/public/lib/Mustache/Loader/ProductionFilesystemLoader.php
@@ -0,0 +1,86 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template production filesystem Loader implementation.
+ *
+ * A production-ready FilesystemLoader, which doesn't require reading a file if it already exists in the template cache.
+ *
+ * {@inheritdoc}
+ */
+class Mustache_Loader_ProductionFilesystemLoader extends Mustache_Loader_FilesystemLoader
+{
+    private $statProps;
+
+    /**
+     * Mustache production filesystem Loader constructor.
+     *
+     * Passing an $options array allows overriding certain Loader options during instantiation:
+     *
+     *     $options = array(
+     *         // The filename extension used for Mustache templates. Defaults to '.mustache'
+     *         'extension' => '.ms',
+     *         'stat_props' => array('size', 'mtime'),
+     *     );
+     *
+     * Specifying 'stat_props' overrides the stat properties used to invalidate the template cache. By default, this
+     * uses 'mtime' and 'size', but this can be set to any of the properties supported by stat():
+     *
+     *     http://php.net/manual/en/function.stat.php
+     *
+     * You can also disable filesystem stat entirely:
+     *
+     *     $options = array('stat_props' => null);
+     *
+     * But with great power comes great responsibility. Namely, if you disable stat-based cache invalidation,
+     * YOU MUST CLEAR THE TEMPLATE CACHE YOURSELF when your templates change. Make it part of your build or deploy
+     * process so you don't forget!
+     *
+     * @throws Mustache_Exception_RuntimeException if $baseDir does not exist.
+     *
+     * @param string $baseDir Base directory containing Mustache template files.
+     * @param array  $options Array of Loader options (default: array())
+     */
+    public function __construct($baseDir, array $options = array())
+    {
+        parent::__construct($baseDir, $options);
+
+        if (array_key_exists('stat_props', $options)) {
+            if (empty($options['stat_props'])) {
+                $this->statProps = array();
+            } else {
+                $this->statProps = $options['stat_props'];
+            }
+        } else {
+            $this->statProps = array('size', 'mtime');
+        }
+    }
+
+    /**
+     * Helper function for loading a Mustache file by name.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
+     * @param string $name
+     *
+     * @return Mustache_Source Mustache Template source
+     */
+    protected function loadFile($name)
+    {
+        $fileName = $this->getFileName($name);
+
+        if (!file_exists($fileName)) {
+            throw new Mustache_Exception_UnknownTemplateException($name);
+        }
+
+        return new Mustache_Source_FilesystemSource($fileName, $this->statProps);
+    }
+}
diff --git a/public/lib/Mustache/Loader/StringLoader.php b/public/lib/Mustache/Loader/StringLoader.php
@@ -0,0 +1,39 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template string Loader implementation.
+ *
+ * A StringLoader instance is essentially a noop. It simply passes the 'name' argument straight through:
+ *
+ *     $loader = new StringLoader;
+ *     $tpl = $loader->load('{{ foo }}'); // '{{ foo }}'
+ *
+ * This is the default Template Loader instance used by Mustache:
+ *
+ *     $m = new Mustache;
+ *     $tpl = $m->loadTemplate('{{ foo }}');
+ *     echo $tpl->render(array('foo' => 'bar')); // "bar"
+ */
+class Mustache_Loader_StringLoader implements Mustache_Loader
+{
+    /**
+     * Load a Template by source.
+     *
+     * @param string $name Mustache Template source
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        return $name;
+    }
+}
diff --git a/public/lib/Mustache/Logger.php b/public/lib/Mustache/Logger.php
@@ -0,0 +1,126 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Describes a Mustache logger instance.
+ *
+ * This is identical to the Psr\Log\LoggerInterface.
+ *
+ * The message MUST be a string or object implementing __toString().
+ *
+ * The message MAY contain placeholders in the form: {foo} where foo
+ * will be replaced by the context data in key "foo".
+ *
+ * The context array can contain arbitrary data, the only assumption that
+ * can be made by implementors is that if an Exception instance is given
+ * to produce a stack trace, it MUST be in a key named "exception".
+ *
+ * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
+ * for the full interface specification.
+ */
+interface Mustache_Logger
+{
+    /**
+     * Psr\Log compatible log levels.
+     */
+    const EMERGENCY = 'emergency';
+    const ALERT     = 'alert';
+    const CRITICAL  = 'critical';
+    const ERROR     = 'error';
+    const WARNING   = 'warning';
+    const NOTICE    = 'notice';
+    const INFO      = 'info';
+    const DEBUG     = 'debug';
+
+    /**
+     * System is unusable.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function emergency($message, array $context = array());
+
+    /**
+     * Action must be taken immediately.
+     *
+     * Example: Entire website down, database unavailable, etc. This should
+     * trigger the SMS alerts and wake you up.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function alert($message, array $context = array());
+
+    /**
+     * Critical conditions.
+     *
+     * Example: Application component unavailable, unexpected exception.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function critical($message, array $context = array());
+
+    /**
+     * Runtime errors that do not require immediate action but should typically
+     * be logged and monitored.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function error($message, array $context = array());
+
+    /**
+     * Exceptional occurrences that are not errors.
+     *
+     * Example: Use of deprecated APIs, poor use of an API, undesirable things
+     * that are not necessarily wrong.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function warning($message, array $context = array());
+
+    /**
+     * Normal but significant events.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function notice($message, array $context = array());
+
+    /**
+     * Interesting events.
+     *
+     * Example: User logs in, SQL logs.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function info($message, array $context = array());
+
+    /**
+     * Detailed debug information.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function debug($message, array $context = array());
+
+    /**
+     * Logs with an arbitrary level.
+     *
+     * @param mixed  $level
+     * @param string $message
+     * @param array  $context
+     */
+    public function log($level, $message, array $context = array());
+}
diff --git a/public/lib/Mustache/Logger/AbstractLogger.php b/public/lib/Mustache/Logger/AbstractLogger.php
@@ -0,0 +1,121 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * This is a simple Logger implementation that other Loggers can inherit from.
+ *
+ * This is identical to the Psr\Log\AbstractLogger.
+ *
+ * It simply delegates all log-level-specific methods to the `log` method to
+ * reduce boilerplate code that a simple Logger that does the same thing with
+ * messages regardless of the error level has to implement.
+ */
+abstract class Mustache_Logger_AbstractLogger implements Mustache_Logger
+{
+    /**
+     * System is unusable.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function emergency($message, array $context = array())
+    {
+        $this->log(Mustache_Logger::EMERGENCY, $message, $context);
+    }
+
+    /**
+     * Action must be taken immediately.
+     *
+     * Example: Entire website down, database unavailable, etc. This should
+     * trigger the SMS alerts and wake you up.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function alert($message, array $context = array())
+    {
+        $this->log(Mustache_Logger::ALERT, $message, $context);
+    }
+
+    /**
+     * Critical conditions.
+     *
+     * Example: Application component unavailable, unexpected exception.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function critical($message, array $context = array())
+    {
+        $this->log(Mustache_Logger::CRITICAL, $message, $context);
+    }
+
+    /**
+     * Runtime errors that do not require immediate action but should typically
+     * be logged and monitored.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function error($message, array $context = array())
+    {
+        $this->log(Mustache_Logger::ERROR, $message, $context);
+    }
+
+    /**
+     * Exceptional occurrences that are not errors.
+     *
+     * Example: Use of deprecated APIs, poor use of an API, undesirable things
+     * that are not necessarily wrong.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function warning($message, array $context = array())
+    {
+        $this->log(Mustache_Logger::WARNING, $message, $context);
+    }
+
+    /**
+     * Normal but significant events.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function notice($message, array $context = array())
+    {
+        $this->log(Mustache_Logger::NOTICE, $message, $context);
+    }
+
+    /**
+     * Interesting events.
+     *
+     * Example: User logs in, SQL logs.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function info($message, array $context = array())
+    {
+        $this->log(Mustache_Logger::INFO, $message, $context);
+    }
+
+    /**
+     * Detailed debug information.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function debug($message, array $context = array())
+    {
+        $this->log(Mustache_Logger::DEBUG, $message, $context);
+    }
+}
diff --git a/public/lib/Mustache/Logger/StreamLogger.php b/public/lib/Mustache/Logger/StreamLogger.php
@@ -0,0 +1,194 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Stream Logger.
+ *
+ * The Stream Logger wraps a file resource instance (such as a stream) or a
+ * stream URL. All log messages over the threshold level will be appended to
+ * this stream.
+ *
+ * Hint: Try `php://stderr` for your stream URL.
+ */
+class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
+{
+    protected static $levels = array(
+        self::DEBUG     => 100,
+        self::INFO      => 200,
+        self::NOTICE    => 250,
+        self::WARNING   => 300,
+        self::ERROR     => 400,
+        self::CRITICAL  => 500,
+        self::ALERT     => 550,
+        self::EMERGENCY => 600,
+    );
+
+    protected $level;
+    protected $stream = null;
+    protected $url    = null;
+
+    /**
+     * @throws InvalidArgumentException if the logging level is unknown
+     *
+     * @param resource|string $stream Resource instance or URL
+     * @param int             $level  The minimum logging level at which this handler will be triggered
+     */
+    public function __construct($stream, $level = Mustache_Logger::ERROR)
+    {
+        $this->setLevel($level);
+
+        if (is_resource($stream)) {
+            $this->stream = $stream;
+        } else {
+            $this->url = $stream;
+        }
+    }
+
+    /**
+     * Close stream resources.
+     */
+    public function __destruct()
+    {
+        if (is_resource($this->stream)) {
+            fclose($this->stream);
+        }
+    }
+
+    /**
+     * Set the minimum logging level.
+     *
+     * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown
+     *
+     * @param int $level The minimum logging level which will be written
+     */
+    public function setLevel($level)
+    {
+        if (!array_key_exists($level, self::$levels)) {
+            throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
+        }
+
+        $this->level = $level;
+    }
+
+    /**
+     * Get the current minimum logging level.
+     *
+     * @return int
+     */
+    public function getLevel()
+    {
+        return $this->level;
+    }
+
+    /**
+     * Logs with an arbitrary level.
+     *
+     * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown
+     *
+     * @param mixed  $level
+     * @param string $message
+     * @param array  $context
+     */
+    public function log($level, $message, array $context = array())
+    {
+        if (!array_key_exists($level, self::$levels)) {
+            throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
+        }
+
+        if (self::$levels[$level] >= self::$levels[$this->level]) {
+            $this->writeLog($level, $message, $context);
+        }
+    }
+
+    /**
+     * Write a record to the log.
+     *
+     * @throws Mustache_Exception_LogicException   If neither a stream resource nor url is present
+     * @throws Mustache_Exception_RuntimeException If the stream url cannot be opened
+     *
+     * @param int    $level   The logging level
+     * @param string $message The log message
+     * @param array  $context The log context
+     */
+    protected function writeLog($level, $message, array $context = array())
+    {
+        if (!is_resource($this->stream)) {
+            if (!isset($this->url)) {
+                throw new Mustache_Exception_LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().');
+            }
+
+            $this->stream = fopen($this->url, 'a');
+            if (!is_resource($this->stream)) {
+                // @codeCoverageIgnoreStart
+                throw new Mustache_Exception_RuntimeException(sprintf('The stream or file "%s" could not be opened.', $this->url));
+                // @codeCoverageIgnoreEnd
+            }
+        }
+
+        fwrite($this->stream, self::formatLine($level, $message, $context));
+    }
+
+    /**
+     * Gets the name of the logging level.
+     *
+     * @throws InvalidArgumentException if the logging level is unknown
+     *
+     * @param int $level
+     *
+     * @return string
+     */
+    protected static function getLevelName($level)
+    {
+        return strtoupper($level);
+    }
+
+    /**
+     * Format a log line for output.
+     *
+     * @param int    $level   The logging level
+     * @param string $message The log message
+     * @param array  $context The log context
+     *
+     * @return string
+     */
+    protected static function formatLine($level, $message, array $context = array())
+    {
+        return sprintf(
+            "%s: %s\n",
+            self::getLevelName($level),
+            self::interpolateMessage($message, $context)
+        );
+    }
+
+    /**
+     * Interpolate context values into the message placeholders.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return string
+     */
+    protected static function interpolateMessage($message, array $context = array())
+    {
+        if (strpos($message, '{') === false) {
+            return $message;
+        }
+
+        // build a replacement array with braces around the context keys
+        $replace = array();
+        foreach ($context as $key => $val) {
+            $replace['{' . $key . '}'] = $val;
+        }
+
+        // interpolate replacement values into the the message and return
+        return strtr($message, $replace);
+    }
+}
diff --git a/public/lib/Mustache/Parser.php b/public/lib/Mustache/Parser.php
@@ -0,0 +1,317 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Parser class.
+ *
+ * This class is responsible for turning a set of Mustache tokens into a parse tree.
+ */
+class Mustache_Parser
+{
+    private $lineNum;
+    private $lineTokens;
+    private $pragmas;
+    private $defaultPragmas = array();
+
+    private $pragmaFilters;
+    private $pragmaBlocks;
+
+    /**
+     * Process an array of Mustache tokens and convert them into a parse tree.
+     *
+     * @param array $tokens Set of Mustache tokens
+     *
+     * @return array Mustache token parse tree
+     */
+    public function parse(array $tokens = array())
+    {
+        $this->lineNum    = -1;
+        $this->lineTokens = 0;
+        $this->pragmas    = $this->defaultPragmas;
+
+        $this->pragmaFilters = isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS]);
+        $this->pragmaBlocks  = isset($this->pragmas[Mustache_Engine::PRAGMA_BLOCKS]);
+
+        return $this->buildTree($tokens);
+    }
+
+    /**
+     * Enable pragmas across all templates, regardless of the presence of pragma
+     * tags in the individual templates.
+     *
+     * @internal Users should set global pragmas in Mustache_Engine, not here :)
+     *
+     * @param string[] $pragmas
+     */
+    public function setPragmas(array $pragmas)
+    {
+        $this->pragmas = array();
+        foreach ($pragmas as $pragma) {
+            $this->enablePragma($pragma);
+        }
+        $this->defaultPragmas = $this->pragmas;
+    }
+
+    /**
+     * Helper method for recursively building a parse tree.
+     *
+     * @throws Mustache_Exception_SyntaxException when nesting errors or mismatched section tags are encountered
+     *
+     * @param array &$tokens Set of Mustache tokens
+     * @param array $parent  Parent token (default: null)
+     *
+     * @return array Mustache Token parse tree
+     */
+    private function buildTree(array &$tokens, array $parent = null)
+    {
+        $nodes = array();
+
+        while (!empty($tokens)) {
+            $token = array_shift($tokens);
+
+            if ($token[Mustache_Tokenizer::LINE] === $this->lineNum) {
+                $this->lineTokens++;
+            } else {
+                $this->lineNum    = $token[Mustache_Tokenizer::LINE];
+                $this->lineTokens = 0;
+            }
+
+            if ($this->pragmaFilters && isset($token[Mustache_Tokenizer::NAME])) {
+                list($name, $filters) = $this->getNameAndFilters($token[Mustache_Tokenizer::NAME]);
+                if (!empty($filters)) {
+                    $token[Mustache_Tokenizer::NAME]    = $name;
+                    $token[Mustache_Tokenizer::FILTERS] = $filters;
+                }
+            }
+
+            switch ($token[Mustache_Tokenizer::TYPE]) {
+                case Mustache_Tokenizer::T_DELIM_CHANGE:
+                    $this->checkIfTokenIsAllowedInParent($parent, $token);
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    break;
+
+                case Mustache_Tokenizer::T_SECTION:
+                case Mustache_Tokenizer::T_INVERTED:
+                    $this->checkIfTokenIsAllowedInParent($parent, $token);
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    $nodes[] = $this->buildTree($tokens, $token);
+                    break;
+
+                case Mustache_Tokenizer::T_END_SECTION:
+                    if (!isset($parent)) {
+                        $msg = sprintf(
+                            'Unexpected closing tag: /%s on line %d',
+                            $token[Mustache_Tokenizer::NAME],
+                            $token[Mustache_Tokenizer::LINE]
+                        );
+                        throw new Mustache_Exception_SyntaxException($msg, $token);
+                    }
+
+                    if ($token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME]) {
+                        $msg = sprintf(
+                            'Nesting error: %s (on line %d) vs. %s (on line %d)',
+                            $parent[Mustache_Tokenizer::NAME],
+                            $parent[Mustache_Tokenizer::LINE],
+                            $token[Mustache_Tokenizer::NAME],
+                            $token[Mustache_Tokenizer::LINE]
+                        );
+                        throw new Mustache_Exception_SyntaxException($msg, $token);
+                    }
+
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    $parent[Mustache_Tokenizer::END]   = $token[Mustache_Tokenizer::INDEX];
+                    $parent[Mustache_Tokenizer::NODES] = $nodes;
+
+                    return $parent;
+
+                case Mustache_Tokenizer::T_PARTIAL:
+                    $this->checkIfTokenIsAllowedInParent($parent, $token);
+                    //store the whitespace prefix for laters!
+                    if ($indent = $this->clearStandaloneLines($nodes, $tokens)) {
+                        $token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE];
+                    }
+                    $nodes[] = $token;
+                    break;
+
+                case Mustache_Tokenizer::T_PARENT:
+                    $this->checkIfTokenIsAllowedInParent($parent, $token);
+                    $nodes[] = $this->buildTree($tokens, $token);
+                    break;
+
+                case Mustache_Tokenizer::T_BLOCK_VAR:
+                    if ($this->pragmaBlocks) {
+                        // BLOCKS pragma is enabled, let's do this!
+                        if (isset($parent) && $parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) {
+                            $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_BLOCK_ARG;
+                        }
+                        $this->clearStandaloneLines($nodes, $tokens);
+                        $nodes[] = $this->buildTree($tokens, $token);
+                    } else {
+                        // pretend this was just a normal "escaped" token...
+                        $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_ESCAPED;
+                        // TODO: figure out how to figure out if there was a space after this dollar:
+                        $token[Mustache_Tokenizer::NAME] = '$' . $token[Mustache_Tokenizer::NAME];
+                        $nodes[] = $token;
+                    }
+                    break;
+
+                case Mustache_Tokenizer::T_PRAGMA:
+                    $this->enablePragma($token[Mustache_Tokenizer::NAME]);
+                    // no break
+
+                case Mustache_Tokenizer::T_COMMENT:
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    $nodes[] = $token;
+                    break;
+
+                default:
+                    $nodes[] = $token;
+                    break;
+            }
+        }
+
+        if (isset($parent)) {
+            $msg = sprintf(
+                'Missing closing tag: %s opened on line %d',
+                $parent[Mustache_Tokenizer::NAME],
+                $parent[Mustache_Tokenizer::LINE]
+            );
+            throw new Mustache_Exception_SyntaxException($msg, $parent);
+        }
+
+        return $nodes;
+    }
+
+    /**
+     * Clear standalone line tokens.
+     *
+     * Returns a whitespace token for indenting partials, if applicable.
+     *
+     * @param array $nodes  Parsed nodes
+     * @param array $tokens Tokens to be parsed
+     *
+     * @return array|null Resulting indent token, if any
+     */
+    private function clearStandaloneLines(array &$nodes, array &$tokens)
+    {
+        if ($this->lineTokens > 1) {
+            // this is the third or later node on this line, so it can't be standalone
+            return;
+        }
+
+        $prev = null;
+        if ($this->lineTokens === 1) {
+            // this is the second node on this line, so it can't be standalone
+            // unless the previous node is whitespace.
+            if ($prev = end($nodes)) {
+                if (!$this->tokenIsWhitespace($prev)) {
+                    return;
+                }
+            }
+        }
+
+        if ($next = reset($tokens)) {
+            // If we're on a new line, bail.
+            if ($next[Mustache_Tokenizer::LINE] !== $this->lineNum) {
+                return;
+            }
+
+            // If the next token isn't whitespace, bail.
+            if (!$this->tokenIsWhitespace($next)) {
+                return;
+            }
+
+            if (count($tokens) !== 1) {
+                // Unless it's the last token in the template, the next token
+                // must end in newline for this to be standalone.
+                if (substr($next[Mustache_Tokenizer::VALUE], -1) !== "\n") {
+                    return;
+                }
+            }
+
+            // Discard the whitespace suffix
+            array_shift($tokens);
+        }
+
+        if ($prev) {
+            // Return the whitespace prefix, if any
+            return array_pop($nodes);
+        }
+    }
+
+    /**
+     * Check whether token is a whitespace token.
+     *
+     * True if token type is T_TEXT and value is all whitespace characters.
+     *
+     * @param array $token
+     *
+     * @return bool True if token is a whitespace token
+     */
+    private function tokenIsWhitespace(array $token)
+    {
+        if ($token[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_TEXT) {
+            return preg_match('/^\s*$/', $token[Mustache_Tokenizer::VALUE]);
+        }
+
+        return false;
+    }
+
+    /**
+     * Check whether a token is allowed inside a parent tag.
+     *
+     * @throws Mustache_Exception_SyntaxException if an invalid token is found inside a parent tag
+     *
+     * @param array|null $parent
+     * @param array      $token
+     */
+    private function checkIfTokenIsAllowedInParent($parent, array $token)
+    {
+        if (isset($parent) && $parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) {
+            throw new Mustache_Exception_SyntaxException('Illegal content in < parent tag', $token);
+        }
+    }
+
+    /**
+     * Split a tag name into name and filters.
+     *
+     * @param string $name
+     *
+     * @return array [Tag name, Array of filters]
+     */
+    private function getNameAndFilters($name)
+    {
+        $filters = array_map('trim', explode('|', $name));
+        $name    = array_shift($filters);
+
+        return array($name, $filters);
+    }
+
+    /**
+     * Enable a pragma.
+     *
+     * @param string $name
+     */
+    private function enablePragma($name)
+    {
+        $this->pragmas[$name] = true;
+
+        switch ($name) {
+            case Mustache_Engine::PRAGMA_BLOCKS:
+                $this->pragmaBlocks = true;
+                break;
+
+            case Mustache_Engine::PRAGMA_FILTERS:
+                $this->pragmaFilters = true;
+                break;
+        }
+    }
+}
diff --git a/public/lib/Mustache/Source.php b/public/lib/Mustache/Source.php
@@ -0,0 +1,40 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache template Source interface.
+ */
+interface Mustache_Source
+{
+    /**
+     * Get the Source key (used to generate the compiled class name).
+     *
+     * This must return a distinct key for each template source. For example, an
+     * MD5 hash of the template contents would probably do the trick. The
+     * ProductionFilesystemLoader uses mtime and file path. If your production
+     * source directory is under version control, you could use the current Git
+     * rev and the file path...
+     *
+     * @throws RuntimeException when a source file cannot be read
+     *
+     * @return string
+     */
+    public function getKey();
+
+    /**
+     * Get the template Source.
+     *
+     * @throws RuntimeException when a source file cannot be read
+     *
+     * @return string
+     */
+    public function getSource();
+}
diff --git a/public/lib/Mustache/Source/FilesystemSource.php b/public/lib/Mustache/Source/FilesystemSource.php
@@ -0,0 +1,77 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache template Filesystem Source.
+ *
+ * This template Source uses stat() to generate the Source key, so that using
+ * pre-compiled templates doesn't require hitting the disk to read the source.
+ * It is more suitable for production use, and is used by default in the
+ * ProductionFilesystemLoader.
+ */
+class Mustache_Source_FilesystemSource implements Mustache_Source
+{
+    private $fileName;
+    private $statProps;
+    private $stat;
+
+    /**
+     * Filesystem Source constructor.
+     *
+     * @param string $fileName
+     * @param array  $statProps
+     */
+    public function __construct($fileName, array $statProps)
+    {
+        $this->fileName = $fileName;
+        $this->statProps = $statProps;
+    }
+
+    /**
+     * Get the Source key (used to generate the compiled class name).
+     *
+     * @throws Mustache_Exception_RuntimeException when a source file cannot be read
+     *
+     * @return string
+     */
+    public function getKey()
+    {
+        $chunks = array(
+            'fileName' => $this->fileName,
+        );
+
+        if (!empty($this->statProps)) {
+            if (!isset($this->stat)) {
+                $this->stat = @stat($this->fileName);
+            }
+
+            if ($this->stat === false) {
+                throw new Mustache_Exception_RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName));
+            }
+
+            foreach ($this->statProps as $prop) {
+                $chunks[$prop] = $this->stat[$prop];
+            }
+        }
+
+        return json_encode($chunks);
+    }
+
+    /**
+     * Get the template Source.
+     *
+     * @return string
+     */
+    public function getSource()
+    {
+        return file_get_contents($this->fileName);
+    }
+}
diff --git a/public/lib/Mustache/Template.php b/public/lib/Mustache/Template.php
@@ -0,0 +1,180 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Abstract Mustache Template class.
+ *
+ * @abstract
+ */
+abstract class Mustache_Template
+{
+    /**
+     * @var Mustache_Engine
+     */
+    protected $mustache;
+
+    /**
+     * @var bool
+     */
+    protected $strictCallables = false;
+
+    /**
+     * Mustache Template constructor.
+     *
+     * @param Mustache_Engine $mustache
+     */
+    public function __construct(Mustache_Engine $mustache)
+    {
+        $this->mustache = $mustache;
+    }
+
+    /**
+     * Mustache Template instances can be treated as a function and rendered by simply calling them.
+     *
+     *     $m = new Mustache_Engine;
+     *     $tpl = $m->loadTemplate('Hello, {{ name }}!');
+     *     echo $tpl(array('name' => 'World')); // "Hello, World!"
+     *
+     * @see Mustache_Template::render
+     *
+     * @param mixed $context Array or object rendering context (default: array())
+     *
+     * @return string Rendered template
+     */
+    public function __invoke($context = array())
+    {
+        return $this->render($context);
+    }
+
+    /**
+     * Render this template given the rendering context.
+     *
+     * @param mixed $context Array or object rendering context (default: array())
+     *
+     * @return string Rendered template
+     */
+    public function render($context = array())
+    {
+        return $this->renderInternal(
+            $this->prepareContextStack($context)
+        );
+    }
+
+    /**
+     * Internal rendering method implemented by Mustache Template concrete subclasses.
+     *
+     * This is where the magic happens :)
+     *
+     * NOTE: This method is not part of the Mustache.php public API.
+     *
+     * @param Mustache_Context $context
+     * @param string           $indent  (default: '')
+     *
+     * @return string Rendered template
+     */
+    abstract public function renderInternal(Mustache_Context $context, $indent = '');
+
+    /**
+     * Tests whether a value should be iterated over (e.g. in a section context).
+     *
+     * In most languages there are two distinct array types: list and hash (or whatever you want to call them). Lists
+     * should be iterated, hashes should be treated as objects. Mustache follows this paradigm for Ruby, Javascript,
+     * Java, Python, etc.
+     *
+     * PHP, however, treats lists and hashes as one primitive type: array. So Mustache.php needs a way to distinguish
+     * between between a list of things (numeric, normalized array) and a set of variables to be used as section context
+     * (associative array). In other words, this will be iterated over:
+     *
+     *     $items = array(
+     *         array('name' => 'foo'),
+     *         array('name' => 'bar'),
+     *         array('name' => 'baz'),
+     *     );
+     *
+     * ... but this will be used as a section context block:
+     *
+     *     $items = array(
+     *         1        => array('name' => 'foo'),
+     *         'banana' => array('name' => 'bar'),
+     *         42       => array('name' => 'baz'),
+     *     );
+     *
+     * @param mixed $value
+     *
+     * @return bool True if the value is 'iterable'
+     */
+    protected function isIterable($value)
+    {
+        switch (gettype($value)) {
+            case 'object':
+                return $value instanceof Traversable;
+
+            case 'array':
+                $i = 0;
+                foreach ($value as $k => $v) {
+                    if ($k !== $i++) {
+                        return false;
+                    }
+                }
+
+                return true;
+
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Helper method to prepare the Context stack.
+     *
+     * Adds the Mustache HelperCollection to the stack's top context frame if helpers are present.
+     *
+     * @param mixed $context Optional first context frame (default: null)
+     *
+     * @return Mustache_Context
+     */
+    protected function prepareContextStack($context = null)
+    {
+        $stack = new Mustache_Context();
+
+        $helpers = $this->mustache->getHelpers();
+        if (!$helpers->isEmpty()) {
+            $stack->push($helpers);
+        }
+
+        if (!empty($context)) {
+            $stack->push($context);
+        }
+
+        return $stack;
+    }
+
+    /**
+     * Resolve a context value.
+     *
+     * Invoke the value if it is callable, otherwise return the value.
+     *
+     * @param mixed            $value
+     * @param Mustache_Context $context
+     *
+     * @return string
+     */
+    protected function resolveValue($value, Mustache_Context $context)
+    {
+        if (($this->strictCallables ? is_object($value) : !is_string($value)) && is_callable($value)) {
+            return $this->mustache
+                ->loadLambda((string) call_user_func($value))
+                ->renderInternal($context);
+        }
+
+        return $value;
+    }
+}
diff --git a/public/lib/Mustache/Tokenizer.php b/public/lib/Mustache/Tokenizer.php
@@ -0,0 +1,378 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Tokenizer class.
+ *
+ * This class is responsible for turning raw template source into a set of Mustache tokens.
+ */
+class Mustache_Tokenizer
+{
+    // Finite state machine states
+    const IN_TEXT     = 0;
+    const IN_TAG_TYPE = 1;
+    const IN_TAG      = 2;
+
+    // Token types
+    const T_SECTION      = '#';
+    const T_INVERTED     = '^';
+    const T_END_SECTION  = '/';
+    const T_COMMENT      = '!';
+    const T_PARTIAL      = '>';
+    const T_PARENT       = '<';
+    const T_DELIM_CHANGE = '=';
+    const T_ESCAPED      = '_v';
+    const T_UNESCAPED    = '{';
+    const T_UNESCAPED_2  = '&';
+    const T_TEXT         = '_t';
+    const T_PRAGMA       = '%';
+    const T_BLOCK_VAR    = '$';
+    const T_BLOCK_ARG    = '$arg';
+
+    // Valid token types
+    private static $tagTypes = array(
+        self::T_SECTION      => true,
+        self::T_INVERTED     => true,
+        self::T_END_SECTION  => true,
+        self::T_COMMENT      => true,
+        self::T_PARTIAL      => true,
+        self::T_PARENT       => true,
+        self::T_DELIM_CHANGE => true,
+        self::T_ESCAPED      => true,
+        self::T_UNESCAPED    => true,
+        self::T_UNESCAPED_2  => true,
+        self::T_PRAGMA       => true,
+        self::T_BLOCK_VAR    => true,
+    );
+
+    // Token properties
+    const TYPE    = 'type';
+    const NAME    = 'name';
+    const OTAG    = 'otag';
+    const CTAG    = 'ctag';
+    const LINE    = 'line';
+    const INDEX   = 'index';
+    const END     = 'end';
+    const INDENT  = 'indent';
+    const NODES   = 'nodes';
+    const VALUE   = 'value';
+    const FILTERS = 'filters';
+
+    private $state;
+    private $tagType;
+    private $buffer;
+    private $tokens;
+    private $seenTag;
+    private $line;
+
+    private $otag;
+    private $otagChar;
+    private $otagLen;
+
+    private $ctag;
+    private $ctagChar;
+    private $ctagLen;
+
+    /**
+     * Scan and tokenize template source.
+     *
+     * @throws Mustache_Exception_SyntaxException when mismatched section tags are encountered
+     * @throws Mustache_Exception_InvalidArgumentException when $delimiters string is invalid
+     *
+     * @param string $text       Mustache template source to tokenize
+     * @param string $delimiters Optionally, pass initial opening and closing delimiters (default: empty string)
+     *
+     * @return array Set of Mustache tokens
+     */
+    public function scan($text, $delimiters = '')
+    {
+        // Setting mbstring.func_overload makes things *really* slow.
+        // Let's do everyone a favor and scan this string as ASCII instead.
+        //
+        // The INI directive was removed in PHP 8.0 so we don't need to check there (and can drop it
+        // when we remove support for older versions of PHP).
+        //
+        // @codeCoverageIgnoreStart
+        $encoding = null;
+        if (version_compare(PHP_VERSION, '8.0.0', '<')) {
+            if (function_exists('mb_internal_encoding') && ini_get('mbstring.func_overload') & 2) {
+                $encoding = mb_internal_encoding();
+                mb_internal_encoding('ASCII');
+            }
+        }
+        // @codeCoverageIgnoreEnd
+
+        $this->reset();
+
+        if (is_string($delimiters) && $delimiters = trim($delimiters)) {
+            $this->setDelimiters($delimiters);
+        }
+
+        $len = strlen($text);
+        for ($i = 0; $i < $len; $i++) {
+            switch ($this->state) {
+                case self::IN_TEXT:
+                    $char = $text[$i];
+                    // Test whether it's time to change tags.
+                    if ($char === $this->otagChar && substr($text, $i, $this->otagLen) === $this->otag) {
+                        $i--;
+                        $this->flushBuffer();
+                        $this->state = self::IN_TAG_TYPE;
+                    } else {
+                        $this->buffer .= $char;
+                        if ($char === "\n") {
+                            $this->flushBuffer();
+                            $this->line++;
+                        }
+                    }
+                    break;
+
+                case self::IN_TAG_TYPE:
+                    $i += $this->otagLen - 1;
+                    $char = $text[$i + 1];
+                    if (isset(self::$tagTypes[$char])) {
+                        $tag = $char;
+                        $this->tagType = $tag;
+                    } else {
+                        $tag = null;
+                        $this->tagType = self::T_ESCAPED;
+                    }
+
+                    if ($this->tagType === self::T_DELIM_CHANGE) {
+                        $i = $this->changeDelimiters($text, $i);
+                        $this->state = self::IN_TEXT;
+                    } elseif ($this->tagType === self::T_PRAGMA) {
+                        $i = $this->addPragma($text, $i);
+                        $this->state = self::IN_TEXT;
+                    } else {
+                        if ($tag !== null) {
+                            $i++;
+                        }
+                        $this->state = self::IN_TAG;
+                    }
+                    $this->seenTag = $i;
+                    break;
+
+                default:
+                    $char = $text[$i];
+                    // Test whether it's time to change tags.
+                    if ($char === $this->ctagChar && substr($text, $i, $this->ctagLen) === $this->ctag) {
+                        $token = array(
+                            self::TYPE  => $this->tagType,
+                            self::NAME  => trim($this->buffer),
+                            self::OTAG  => $this->otag,
+                            self::CTAG  => $this->ctag,
+                            self::LINE  => $this->line,
+                            self::INDEX => ($this->tagType === self::T_END_SECTION) ? $this->seenTag - $this->otagLen : $i + $this->ctagLen,
+                        );
+
+                        if ($this->tagType === self::T_UNESCAPED) {
+                            // Clean up `{{{ tripleStache }}}` style tokens.
+                            if ($this->ctag === '}}') {
+                                if (($i + 2 < $len) && $text[$i + 2] === '}') {
+                                    $i++;
+                                } else {
+                                    $msg = sprintf(
+                                        'Mismatched tag delimiters: %s on line %d',
+                                        $token[self::NAME],
+                                        $token[self::LINE]
+                                    );
+
+                                    throw new Mustache_Exception_SyntaxException($msg, $token);
+                                }
+                            } else {
+                                $lastName = $token[self::NAME];
+                                if (substr($lastName, -1) === '}') {
+                                    $token[self::NAME] = trim(substr($lastName, 0, -1));
+                                } else {
+                                    $msg = sprintf(
+                                        'Mismatched tag delimiters: %s on line %d',
+                                        $token[self::NAME],
+                                        $token[self::LINE]
+                                    );
+
+                                    throw new Mustache_Exception_SyntaxException($msg, $token);
+                                }
+                            }
+                        }
+
+                        $this->buffer = '';
+                        $i += $this->ctagLen - 1;
+                        $this->state = self::IN_TEXT;
+                        $this->tokens[] = $token;
+                    } else {
+                        $this->buffer .= $char;
+                    }
+                    break;
+            }
+        }
+
+        if ($this->state !== self::IN_TEXT) {
+            $this->throwUnclosedTagException();
+        }
+
+        $this->flushBuffer();
+
+        // Restore the user's encoding...
+        // @codeCoverageIgnoreStart
+        if ($encoding) {
+            mb_internal_encoding($encoding);
+        }
+        // @codeCoverageIgnoreEnd
+
+        return $this->tokens;
+    }
+
+    /**
+     * Helper function to reset tokenizer internal state.
+     */
+    private function reset()
+    {
+        $this->state    = self::IN_TEXT;
+        $this->tagType  = null;
+        $this->buffer   = '';
+        $this->tokens   = array();
+        $this->seenTag  = false;
+        $this->line     = 0;
+
+        $this->otag     = '{{';
+        $this->otagChar = '{';
+        $this->otagLen  = 2;
+
+        $this->ctag     = '}}';
+        $this->ctagChar = '}';
+        $this->ctagLen  = 2;
+    }
+
+    /**
+     * Flush the current buffer to a token.
+     */
+    private function flushBuffer()
+    {
+        if (strlen($this->buffer) > 0) {
+            $this->tokens[] = array(
+                self::TYPE  => self::T_TEXT,
+                self::LINE  => $this->line,
+                self::VALUE => $this->buffer,
+            );
+            $this->buffer   = '';
+        }
+    }
+
+    /**
+     * Change the current Mustache delimiters. Set new `otag` and `ctag` values.
+     *
+     * @throws Mustache_Exception_SyntaxException when delimiter string is invalid
+     *
+     * @param string $text  Mustache template source
+     * @param int    $index Current tokenizer index
+     *
+     * @return int New index value
+     */
+    private function changeDelimiters($text, $index)
+    {
+        $startIndex = strpos($text, '=', $index) + 1;
+        $close      = '=' . $this->ctag;
+        $closeIndex = strpos($text, $close, $index);
+
+        if ($closeIndex === false) {
+            $this->throwUnclosedTagException();
+        }
+
+        $token = array(
+            self::TYPE => self::T_DELIM_CHANGE,
+            self::LINE => $this->line,
+        );
+
+        try {
+            $this->setDelimiters(trim(substr($text, $startIndex, $closeIndex - $startIndex)));
+        } catch (Mustache_Exception_InvalidArgumentException $e) {
+            throw new Mustache_Exception_SyntaxException($e->getMessage(), $token);
+        }
+
+        $this->tokens[] = $token;
+
+        return $closeIndex + strlen($close) - 1;
+    }
+
+    /**
+     * Set the current Mustache `otag` and `ctag` delimiters.
+     *
+     * @throws Mustache_Exception_InvalidArgumentException when delimiter string is invalid
+     *
+     * @param string $delimiters
+     */
+    private function setDelimiters($delimiters)
+    {
+        if (!preg_match('/^\s*(\S+)\s+(\S+)\s*$/', $delimiters, $matches)) {
+            throw new Mustache_Exception_InvalidArgumentException(sprintf('Invalid delimiters: %s', $delimiters));
+        }
+
+        list($_, $otag, $ctag) = $matches;
+
+        $this->otag     = $otag;
+        $this->otagChar = $otag[0];
+        $this->otagLen  = strlen($otag);
+
+        $this->ctag     = $ctag;
+        $this->ctagChar = $ctag[0];
+        $this->ctagLen  = strlen($ctag);
+    }
+
+    /**
+     * Add pragma token.
+     *
+     * Pragmas are hoisted to the front of the template, so all pragma tokens
+     * will appear at the front of the token list.
+     *
+     * @param string $text
+     * @param int    $index
+     *
+     * @return int New index value
+     */
+    private function addPragma($text, $index)
+    {
+        $end    = strpos($text, $this->ctag, $index);
+        if ($end === false) {
+            $this->throwUnclosedTagException();
+        }
+
+        $pragma = trim(substr($text, $index + 2, $end - $index - 2));
+
+        // Pragmas are hoisted to the front of the template.
+        array_unshift($this->tokens, array(
+            self::TYPE => self::T_PRAGMA,
+            self::NAME => $pragma,
+            self::LINE => 0,
+        ));
+
+        return $end + $this->ctagLen - 1;
+    }
+
+    private function throwUnclosedTagException()
+    {
+        $name = trim($this->buffer);
+        if ($name !== '') {
+            $msg = sprintf('Unclosed tag: %s on line %d', $name, $this->line);
+        } else {
+            $msg = sprintf('Unclosed tag on line %d', $this->line);
+        }
+
+        throw new Mustache_Exception_SyntaxException($msg, array(
+            self::TYPE  => $this->tagType,
+            self::NAME  => $name,
+            self::OTAG  => $this->otag,
+            self::CTAG  => $this->ctag,
+            self::LINE  => $this->line,
+            self::INDEX => $this->seenTag - $this->otagLen,
+        ));
+    }
+}
diff --git a/public/lib/Parsedown.php b/public/lib/Parsedown.php
@@ -0,0 +1,1994 @@
+<?php
+
+#
+#
+# Parsedown
+# http://parsedown.org
+#
+# (c) Emanuil Rusev
+# http://erusev.com
+#
+# For the full license information, view the LICENSE file that was distributed
+# with this source code.
+#
+#
+
+class Parsedown
+{
+    # ~
+
+    const version = '1.8.0-beta-7';
+
+    # ~
+
+    function text($text)
+    {
+        $Elements = $this->textElements($text);
+
+        # convert to markup
+        $markup = $this->elements($Elements);
+
+        # trim line breaks
+        $markup = trim($markup, "\n");
+
+        return $markup;
+    }
+
+    protected function textElements($text)
+    {
+        # make sure no definitions are set
+        $this->DefinitionData = array();
+
+        # standardize line breaks
+        $text = str_replace(array("\r\n", "\r"), "\n", $text);
+
+        # remove surrounding line breaks
+        $text = trim($text, "\n");
+
+        # split text into lines
+        $lines = explode("\n", $text);
+
+        # iterate through lines to identify blocks
+        return $this->linesElements($lines);
+    }
+
+    #
+    # Setters
+    #
+
+    function setBreaksEnabled($breaksEnabled)
+    {
+        $this->breaksEnabled = $breaksEnabled;
+
+        return $this;
+    }
+
+    protected $breaksEnabled;
+
+    function setMarkupEscaped($markupEscaped)
+    {
+        $this->markupEscaped = $markupEscaped;
+
+        return $this;
+    }
+
+    protected $markupEscaped;
+
+    function setUrlsLinked($urlsLinked)
+    {
+        $this->urlsLinked = $urlsLinked;
+
+        return $this;
+    }
+
+    protected $urlsLinked = true;
+
+    function setSafeMode($safeMode)
+    {
+        $this->safeMode = (bool) $safeMode;
+
+        return $this;
+    }
+
+    protected $safeMode;
+
+    function setStrictMode($strictMode)
+    {
+        $this->strictMode = (bool) $strictMode;
+
+        return $this;
+    }
+
+    protected $strictMode;
+
+    protected $safeLinksWhitelist = array(
+        'http://',
+        'https://',
+        'ftp://',
+        'ftps://',
+        'mailto:',
+        'tel:',
+        'data:image/png;base64,',
+        'data:image/gif;base64,',
+        'data:image/jpeg;base64,',
+        'irc:',
+        'ircs:',
+        'git:',
+        'ssh:',
+        'news:',
+        'steam:',
+    );
+
+    #
+    # Lines
+    #
+
+    protected $BlockTypes = array(
+        '#' => array('Header'),
+        '*' => array('Rule', 'List'),
+        '+' => array('List'),
+        '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
+        '0' => array('List'),
+        '1' => array('List'),
+        '2' => array('List'),
+        '3' => array('List'),
+        '4' => array('List'),
+        '5' => array('List'),
+        '6' => array('List'),
+        '7' => array('List'),
+        '8' => array('List'),
+        '9' => array('List'),
+        ':' => array('Table'),
+        '<' => array('Comment', 'Markup'),
+        '=' => array('SetextHeader'),
+        '>' => array('Quote'),
+        '[' => array('Reference'),
+        '_' => array('Rule'),
+        '`' => array('FencedCode'),
+        '|' => array('Table'),
+        '~' => array('FencedCode'),
+    );
+
+    # ~
+
+    protected $unmarkedBlockTypes = array(
+        'Code',
+    );
+
+    #
+    # Blocks
+    #
+
+    protected function lines(array $lines)
+    {
+        return $this->elements($this->linesElements($lines));
+    }
+
+    protected function linesElements(array $lines)
+    {
+        $Elements = array();
+        $CurrentBlock = null;
+
+        foreach ($lines as $line)
+        {
+            if (chop($line) === '')
+            {
+                if (isset($CurrentBlock))
+                {
+                    $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted'])
+                        ? $CurrentBlock['interrupted'] + 1 : 1
+                    );
+                }
+
+                continue;
+            }
+
+            while (($beforeTab = strstr($line, "\t", true)) !== false)
+            {
+                $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4;
+
+                $line = $beforeTab
+                    . str_repeat(' ', $shortage)
+                    . substr($line, strlen($beforeTab) + 1)
+                ;
+            }
+
+            $indent = strspn($line, ' ');
+
+            $text = $indent > 0 ? substr($line, $indent) : $line;
+
+            # ~
+
+            $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
+
+            # ~
+
+            if (isset($CurrentBlock['continuable']))
+            {
+                $methodName = 'block' . $CurrentBlock['type'] . 'Continue';
+                $Block = $this->$methodName($Line, $CurrentBlock);
+
+                if (isset($Block))
+                {
+                    $CurrentBlock = $Block;
+
+                    continue;
+                }
+                else
+                {
+                    if ($this->isBlockCompletable($CurrentBlock['type']))
+                    {
+                        $methodName = 'block' . $CurrentBlock['type'] . 'Complete';
+                        $CurrentBlock = $this->$methodName($CurrentBlock);
+                    }
+                }
+            }
+
+            # ~
+
+            $marker = $text[0];
+
+            # ~
+
+            $blockTypes = $this->unmarkedBlockTypes;
+
+            if (isset($this->BlockTypes[$marker]))
+            {
+                foreach ($this->BlockTypes[$marker] as $blockType)
+                {
+                    $blockTypes []= $blockType;
+                }
+            }
+
+            #
+            # ~
+
+            foreach ($blockTypes as $blockType)
+            {
+                $Block = $this->{"block$blockType"}($Line, $CurrentBlock);
+
+                if (isset($Block))
+                {
+                    $Block['type'] = $blockType;
+
+                    if ( ! isset($Block['identified']))
+                    {
+                        if (isset($CurrentBlock))
+                        {
+                            $Elements[] = $this->extractElement($CurrentBlock);
+                        }
+
+                        $Block['identified'] = true;
+                    }
+
+                    if ($this->isBlockContinuable($blockType))
+                    {
+                        $Block['continuable'] = true;
+                    }
+
+                    $CurrentBlock = $Block;
+
+                    continue 2;
+                }
+            }
+
+            # ~
+
+            if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph')
+            {
+                $Block = $this->paragraphContinue($Line, $CurrentBlock);
+            }
+
+            if (isset($Block))
+            {
+                $CurrentBlock = $Block;
+            }
+            else
+            {
+                if (isset($CurrentBlock))
+                {
+                    $Elements[] = $this->extractElement($CurrentBlock);
+                }
+
+                $CurrentBlock = $this->paragraph($Line);
+
+                $CurrentBlock['identified'] = true;
+            }
+        }
+
+        # ~
+
+        if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
+        {
+            $methodName = 'block' . $CurrentBlock['type'] . 'Complete';
+            $CurrentBlock = $this->$methodName($CurrentBlock);
+        }
+
+        # ~
+
+        if (isset($CurrentBlock))
+        {
+            $Elements[] = $this->extractElement($CurrentBlock);
+        }
+
+        # ~
+
+        return $Elements;
+    }
+
+    protected function extractElement(array $Component)
+    {
+        if ( ! isset($Component['element']))
+        {
+            if (isset($Component['markup']))
+            {
+                $Component['element'] = array('rawHtml' => $Component['markup']);
+            }
+            elseif (isset($Component['hidden']))
+            {
+                $Component['element'] = array();
+            }
+        }
+
+        return $Component['element'];
+    }
+
+    protected function isBlockContinuable($Type)
+    {
+        return method_exists($this, 'block' . $Type . 'Continue');
+    }
+
+    protected function isBlockCompletable($Type)
+    {
+        return method_exists($this, 'block' . $Type . 'Complete');
+    }
+
+    #
+    # Code
+
+    protected function blockCode($Line, $Block = null)
+    {
+        if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted']))
+        {
+            return;
+        }
+
+        if ($Line['indent'] >= 4)
+        {
+            $text = substr($Line['body'], 4);
+
+            $Block = array(
+                'element' => array(
+                    'name' => 'pre',
+                    'element' => array(
+                        'name' => 'code',
+                        'text' => $text,
+                    ),
+                ),
+            );
+
+            return $Block;
+        }
+    }
+
+    protected function blockCodeContinue($Line, $Block)
+    {
+        if ($Line['indent'] >= 4)
+        {
+            if (isset($Block['interrupted']))
+            {
+                $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']);
+
+                unset($Block['interrupted']);
+            }
+
+            $Block['element']['element']['text'] .= "\n";
+
+            $text = substr($Line['body'], 4);
+
+            $Block['element']['element']['text'] .= $text;
+
+            return $Block;
+        }
+    }
+
+    protected function blockCodeComplete($Block)
+    {
+        return $Block;
+    }
+
+    #
+    # Comment
+
+    protected function blockComment($Line)
+    {
+        if ($this->markupEscaped or $this->safeMode)
+        {
+            return;
+        }
+
+        if (strpos($Line['text'], '<!--') === 0)
+        {
+            $Block = array(
+                'element' => array(
+                    'rawHtml' => $Line['body'],
+                    'autobreak' => true,
+                ),
+            );
+
+            if (strpos($Line['text'], '-->') !== false)
+            {
+                $Block['closed'] = true;
+            }
+
+            return $Block;
+        }
+    }
+
+    protected function blockCommentContinue($Line, array $Block)
+    {
+        if (isset($Block['closed']))
+        {
+            return;
+        }
+
+        $Block['element']['rawHtml'] .= "\n" . $Line['body'];
+
+        if (strpos($Line['text'], '-->') !== false)
+        {
+            $Block['closed'] = true;
+        }
+
+        return $Block;
+    }
+
+    #
+    # Fenced Code
+
+    protected function blockFencedCode($Line)
+    {
+        $marker = $Line['text'][0];
+
+        $openerLength = strspn($Line['text'], $marker);
+
+        if ($openerLength < 3)
+        {
+            return;
+        }
+
+        $infostring = trim(substr($Line['text'], $openerLength), "\t ");
+
+        if (strpos($infostring, '`') !== false)
+        {
+            return;
+        }
+
+        $Element = array(
+            'name' => 'code',
+            'text' => '',
+        );
+
+        if ($infostring !== '')
+        {
+            /**
+             * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
+             * Every HTML element may have a class attribute specified.
+             * The attribute, if specified, must have a value that is a set
+             * of space-separated tokens representing the various classes
+             * that the element belongs to.
+             * [...]
+             * The space characters, for the purposes of this specification,
+             * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
+             * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
+             * U+000D CARRIAGE RETURN (CR).
+             */
+            $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r"));
+
+            $Element['attributes'] = array('class' => "language-$language");
+        }
+
+        $Block = array(
+            'char' => $marker,
+            'openerLength' => $openerLength,
+            'element' => array(
+                'name' => 'pre',
+                'element' => $Element,
+            ),
+        );
+
+        return $Block;
+    }
+
+    protected function blockFencedCodeContinue($Line, $Block)
+    {
+        if (isset($Block['complete']))
+        {
+            return;
+        }
+
+        if (isset($Block['interrupted']))
+        {
+            $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']);
+
+            unset($Block['interrupted']);
+        }
+
+        if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength']
+            and chop(substr($Line['text'], $len), ' ') === ''
+        ) {
+            $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1);
+
+            $Block['complete'] = true;
+
+            return $Block;
+        }
+
+        $Block['element']['element']['text'] .= "\n" . $Line['body'];
+
+        return $Block;
+    }
+
+    protected function blockFencedCodeComplete($Block)
+    {
+        return $Block;
+    }
+
+    #
+    # Header
+
+    protected function blockHeader($Line)
+    {
+        $level = strspn($Line['text'], '#');
+
+        if ($level > 6)
+        {
+            return;
+        }
+
+        $text = trim($Line['text'], '#');
+
+        if ($this->strictMode and isset($text[0]) and $text[0] !== ' ')
+        {
+            return;
+        }
+
+        $text = trim($text, ' ');
+
+        $Block = array(
+            'element' => array(
+                'name' => 'h' . $level,
+                'handler' => array(
+                    'function' => 'lineElements',
+                    'argument' => $text,
+                    'destination' => 'elements',
+                )
+            ),
+        );
+
+        return $Block;
+    }
+
+    #
+    # List
+
+    protected function blockList($Line, array $CurrentBlock = null)
+    {
+        list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]');
+
+        if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches))
+        {
+            $contentIndent = strlen($matches[2]);
+
+            if ($contentIndent >= 5)
+            {
+                $contentIndent -= 1;
+                $matches[1] = substr($matches[1], 0, -$contentIndent);
+                $matches[3] = str_repeat(' ', $contentIndent) . $matches[3];
+            }
+            elseif ($contentIndent === 0)
+            {
+                $matches[1] .= ' ';
+            }
+
+            $markerWithoutWhitespace = strstr($matches[1], ' ', true);
+
+            $Block = array(
+                'indent' => $Line['indent'],
+                'pattern' => $pattern,
+                'data' => array(
+                    'type' => $name,
+                    'marker' => $matches[1],
+                    'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)),
+                ),
+                'element' => array(
+                    'name' => $name,
+                    'elements' => array(),
+                ),
+            );
+            $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/');
+
+            if ($name === 'ol')
+            {
+                $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0';
+
+                if ($listStart !== '1')
+                {
+                    if (
+                        isset($CurrentBlock)
+                        and $CurrentBlock['type'] === 'Paragraph'
+                        and ! isset($CurrentBlock['interrupted'])
+                    ) {
+                        return;
+                    }
+
+                    $Block['element']['attributes'] = array('start' => $listStart);
+                }
+            }
+
+            $Block['li'] = array(
+                'name' => 'li',
+                'handler' => array(
+                    'function' => 'li',
+                    'argument' => !empty($matches[3]) ? array($matches[3]) : array(),
+                    'destination' => 'elements'
+                )
+            );
+
+            $Block['element']['elements'] []= & $Block['li'];
+
+            return $Block;
+        }
+    }
+
+    protected function blockListContinue($Line, array $Block)
+    {
+        if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument']))
+        {
+            return null;
+        }
+
+        $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker']));
+
+        if ($Line['indent'] < $requiredIndent
+            and (
+                (
+                    $Block['data']['type'] === 'ol'
+                    and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)
+                ) or (
+                    $Block['data']['type'] === 'ul'
+                    and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)
+                )
+            )
+        ) {
+            if (isset($Block['interrupted']))
+            {
+                $Block['li']['handler']['argument'] []= '';
+
+                $Block['loose'] = true;
+
+                unset($Block['interrupted']);
+            }
+
+            unset($Block['li']);
+
+            $text = isset($matches[1]) ? $matches[1] : '';
+
+            $Block['indent'] = $Line['indent'];
+
+            $Block['li'] = array(
+                'name' => 'li',
+                'handler' => array(
+                    'function' => 'li',
+                    'argument' => array($text),
+                    'destination' => 'elements'
+                )
+            );
+
+            $Block['element']['elements'] []= & $Block['li'];
+
+            return $Block;
+        }
+        elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line))
+        {
+            return null;
+        }
+
+        if ($Line['text'][0] === '[' and $this->blockReference($Line))
+        {
+            return $Block;
+        }
+
+        if ($Line['indent'] >= $requiredIndent)
+        {
+            if (isset($Block['interrupted']))
+            {
+                $Block['li']['handler']['argument'] []= '';
+
+                $Block['loose'] = true;
+
+                unset($Block['interrupted']);
+            }
+
+            $text = substr($Line['body'], $requiredIndent);
+
+            $Block['li']['handler']['argument'] []= $text;
+
+            return $Block;
+        }
+
+        if ( ! isset($Block['interrupted']))
+        {
+            $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']);
+
+            $Block['li']['handler']['argument'] []= $text;
+
+            return $Block;
+        }
+    }
+
+    protected function blockListComplete(array $Block)
+    {
+        if (isset($Block['loose']))
+        {
+            foreach ($Block['element']['elements'] as &$li)
+            {
+                if (end($li['handler']['argument']) !== '')
+                {
+                    $li['handler']['argument'] []= '';
+                }
+            }
+        }
+
+        return $Block;
+    }
+
+    #
+    # Quote
+
+    protected function blockQuote($Line)
+    {
+        if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches))
+        {
+            $Block = array(
+                'element' => array(
+                    'name' => 'blockquote',
+                    'handler' => array(
+                        'function' => 'linesElements',
+                        'argument' => (array) $matches[1],
+                        'destination' => 'elements',
+                    )
+                ),
+            );
+
+            return $Block;
+        }
+    }
+
+    protected function blockQuoteContinue($Line, array $Block)
+    {
+        if (isset($Block['interrupted']))
+        {
+            return;
+        }
+
+        if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches))
+        {
+            $Block['element']['handler']['argument'] []= $matches[1];
+
+            return $Block;
+        }
+
+        if ( ! isset($Block['interrupted']))
+        {
+            $Block['element']['handler']['argument'] []= $Line['text'];
+
+            return $Block;
+        }
+    }
+
+    #
+    # Rule
+
+    protected function blockRule($Line)
+    {
+        $marker = $Line['text'][0];
+
+        if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '')
+        {
+            $Block = array(
+                'element' => array(
+                    'name' => 'hr',
+                ),
+            );
+
+            return $Block;
+        }
+    }
+
+    #
+    # Setext
+
+    protected function blockSetextHeader($Line, array $Block = null)
+    {
+        if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted']))
+        {
+            return;
+        }
+
+        if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '')
+        {
+            $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
+
+            return $Block;
+        }
+    }
+
+    #
+    # Markup
+
+    protected function blockMarkup($Line)
+    {
+        if ($this->markupEscaped or $this->safeMode)
+        {
+            return;
+        }
+
+        if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches))
+        {
+            $element = strtolower($matches[1]);
+
+            if (in_array($element, $this->textLevelElements))
+            {
+                return;
+            }
+
+            $Block = array(
+                'name' => $matches[1],
+                'element' => array(
+                    'rawHtml' => $Line['text'],
+                    'autobreak' => true,
+                ),
+            );
+
+            return $Block;
+        }
+    }
+
+    protected function blockMarkupContinue($Line, array $Block)
+    {
+        if (isset($Block['closed']) or isset($Block['interrupted']))
+        {
+            return;
+        }
+
+        $Block['element']['rawHtml'] .= "\n" . $Line['body'];
+
+        return $Block;
+    }
+
+    #
+    # Reference
+
+    protected function blockReference($Line)
+    {
+        if (strpos($Line['text'], ']') !== false
+            and preg_match('/^\[(.+?)\]:[ ]*+<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches)
+        ) {
+            $id = strtolower($matches[1]);
+
+            $Data = array(
+                'url' => $matches[2],
+                'title' => isset($matches[3]) ? $matches[3] : null,
+            );
+
+            $this->DefinitionData['Reference'][$id] = $Data;
+
+            $Block = array(
+                'element' => array(),
+            );
+
+            return $Block;
+        }
+    }
+
+    #
+    # Table
+
+    protected function blockTable($Line, array $Block = null)
+    {
+        if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted']))
+        {
+            return;
+        }
+
+        if (
+            strpos($Block['element']['handler']['argument'], '|') === false
+            and strpos($Line['text'], '|') === false
+            and strpos($Line['text'], ':') === false
+            or strpos($Block['element']['handler']['argument'], "\n") !== false
+        ) {
+            return;
+        }
+
+        if (chop($Line['text'], ' -:|') !== '')
+        {
+            return;
+        }
+
+        $alignments = array();
+
+        $divider = $Line['text'];
+
+        $divider = trim($divider);
+        $divider = trim($divider, '|');
+
+        $dividerCells = explode('|', $divider);
+
+        foreach ($dividerCells as $dividerCell)
+        {
+            $dividerCell = trim($dividerCell);
+
+            if ($dividerCell === '')
+            {
+                return;
+            }
+
+            $alignment = null;
+
+            if ($dividerCell[0] === ':')
+            {
+                $alignment = 'left';
+            }
+
+            if (substr($dividerCell, - 1) === ':')
+            {
+                $alignment = $alignment === 'left' ? 'center' : 'right';
+            }
+
+            $alignments []= $alignment;
+        }
+
+        # ~
+
+        $HeaderElements = array();
+
+        $header = $Block['element']['handler']['argument'];
+
+        $header = trim($header);
+        $header = trim($header, '|');
+
+        $headerCells = explode('|', $header);
+
+        if (count($headerCells) !== count($alignments))
+        {
+            return;
+        }
+
+        foreach ($headerCells as $index => $headerCell)
+        {
+            $headerCell = trim($headerCell);
+
+            $HeaderElement = array(
+                'name' => 'th',
+                'handler' => array(
+                    'function' => 'lineElements',
+                    'argument' => $headerCell,
+                    'destination' => 'elements',
+                )
+            );
+
+            if (isset($alignments[$index]))
+            {
+                $alignment = $alignments[$index];
+
+                $HeaderElement['attributes'] = array(
+                    'style' => "text-align: $alignment;",
+                );
+            }
+
+            $HeaderElements []= $HeaderElement;
+        }
+
+        # ~
+
+        $Block = array(
+            'alignments' => $alignments,
+            'identified' => true,
+            'element' => array(
+                'name' => 'table',
+                'elements' => array(),
+            ),
+        );
+
+        $Block['element']['elements'] []= array(
+            'name' => 'thead',
+        );
+
+        $Block['element']['elements'] []= array(
+            'name' => 'tbody',
+            'elements' => array(),
+        );
+
+        $Block['element']['elements'][0]['elements'] []= array(
+            'name' => 'tr',
+            'elements' => $HeaderElements,
+        );
+
+        return $Block;
+    }
+
+    protected function blockTableContinue($Line, array $Block)
+    {
+        if (isset($Block['interrupted']))
+        {
+            return;
+        }
+
+        if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|'))
+        {
+            $Elements = array();
+
+            $row = $Line['text'];
+
+            $row = trim($row);
+            $row = trim($row, '|');
+
+            preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches);
+
+            $cells = array_slice($matches[0], 0, count($Block['alignments']));
+
+            foreach ($cells as $index => $cell)
+            {
+                $cell = trim($cell);
+
+                $Element = array(
+                    'name' => 'td',
+                    'handler' => array(
+                        'function' => 'lineElements',
+                        'argument' => $cell,
+                        'destination' => 'elements',
+                    )
+                );
+
+                if (isset($Block['alignments'][$index]))
+                {
+                    $Element['attributes'] = array(
+                        'style' => 'text-align: ' . $Block['alignments'][$index] . ';',
+                    );
+                }
+
+                $Elements []= $Element;
+            }
+
+            $Element = array(
+                'name' => 'tr',
+                'elements' => $Elements,
+            );
+
+            $Block['element']['elements'][1]['elements'] []= $Element;
+
+            return $Block;
+        }
+    }
+
+    #
+    # ~
+    #
+
+    protected function paragraph($Line)
+    {
+        return array(
+            'type' => 'Paragraph',
+            'element' => array(
+                'name' => 'p',
+                'handler' => array(
+                    'function' => 'lineElements',
+                    'argument' => $Line['text'],
+                    'destination' => 'elements',
+                ),
+            ),
+        );
+    }
+
+    protected function paragraphContinue($Line, array $Block)
+    {
+        if (isset($Block['interrupted']))
+        {
+            return;
+        }
+
+        $Block['element']['handler']['argument'] .= "\n".$Line['text'];
+
+        return $Block;
+    }
+
+    #
+    # Inline Elements
+    #
+
+    protected $InlineTypes = array(
+        '!' => array('Image'),
+        '&' => array('SpecialCharacter'),
+        '*' => array('Emphasis'),
+        ':' => array('Url'),
+        '<' => array('UrlTag', 'EmailTag', 'Markup'),
+        '[' => array('Link'),
+        '_' => array('Emphasis'),
+        '`' => array('Code'),
+        '~' => array('Strikethrough'),
+        '\\' => array('EscapeSequence'),
+    );
+
+    # ~
+
+    protected $inlineMarkerList = '!*_&[:<`~\\';
+
+    #
+    # ~
+    #
+
+    public function line($text, $nonNestables = array())
+    {
+        return $this->elements($this->lineElements($text, $nonNestables));
+    }
+
+    protected function lineElements($text, $nonNestables = array())
+    {
+        # standardize line breaks
+        $text = str_replace(array("\r\n", "\r"), "\n", $text);
+
+        $Elements = array();
+
+        $nonNestables = (empty($nonNestables)
+            ? array()
+            : array_combine($nonNestables, $nonNestables)
+        );
+
+        # $excerpt is based on the first occurrence of a marker
+
+        while ($excerpt = strpbrk($text, $this->inlineMarkerList))
+        {
+            $marker = $excerpt[0];
+
+            $markerPosition = strlen($text) - strlen($excerpt);
+
+            $Excerpt = array('text' => $excerpt, 'context' => $text);
+
+            foreach ($this->InlineTypes[$marker] as $inlineType)
+            {
+                # check to see if the current inline type is nestable in the current context
+
+                if (isset($nonNestables[$inlineType]))
+                {
+                    continue;
+                }
+
+                $Inline = $this->{"inline$inlineType"}($Excerpt);
+
+                if ( ! isset($Inline))
+                {
+                    continue;
+                }
+
+                # makes sure that the inline belongs to "our" marker
+
+                if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
+                {
+                    continue;
+                }
+
+                # sets a default inline position
+
+                if ( ! isset($Inline['position']))
+                {
+                    $Inline['position'] = $markerPosition;
+                }
+
+                # cause the new element to 'inherit' our non nestables
+
+
+                $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables'])
+                    ? array_merge($Inline['element']['nonNestables'], $nonNestables)
+                    : $nonNestables
+                ;
+
+                # the text that comes before the inline
+                $unmarkedText = substr($text, 0, $Inline['position']);
+
+                # compile the unmarked text
+                $InlineText = $this->inlineText($unmarkedText);
+                $Elements[] = $InlineText['element'];
+
+                # compile the inline
+                $Elements[] = $this->extractElement($Inline);
+
+                # remove the examined text
+                $text = substr($text, $Inline['position'] + $Inline['extent']);
+
+                continue 2;
+            }
+
+            # the marker does not belong to an inline
+
+            $unmarkedText = substr($text, 0, $markerPosition + 1);
+
+            $InlineText = $this->inlineText($unmarkedText);
+            $Elements[] = $InlineText['element'];
+
+            $text = substr($text, $markerPosition + 1);
+        }
+
+        $InlineText = $this->inlineText($text);
+        $Elements[] = $InlineText['element'];
+
+        foreach ($Elements as &$Element)
+        {
+            if ( ! isset($Element['autobreak']))
+            {
+                $Element['autobreak'] = false;
+            }
+        }
+
+        return $Elements;
+    }
+
+    #
+    # ~
+    #
+
+    protected function inlineText($text)
+    {
+        $Inline = array(
+            'extent' => strlen($text),
+            'element' => array(),
+        );
+
+        $Inline['element']['elements'] = self::pregReplaceElements(
+            $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/',
+            array(
+                array('name' => 'br'),
+                array('text' => "\n"),
+            ),
+            $text
+        );
+
+        return $Inline;
+    }
+
+    protected function inlineCode($Excerpt)
+    {
+        $marker = $Excerpt['text'][0];
+
+        if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(?<!['.$marker.'])\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
+        {
+            $text = $matches[2];
+            $text = preg_replace('/[ ]*+\n/', ' ', $text);
+
+            return array(
+                'extent' => strlen($matches[0]),
+                'element' => array(
+                    'name' => 'code',
+                    'text' => $text,
+                ),
+            );
+        }
+    }
+
+    protected function inlineEmailTag($Excerpt)
+    {
+        $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?';
+
+        $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@'
+            . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*';
+
+        if (strpos($Excerpt['text'], '>') !== false
+            and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches)
+        ){
+            $url = $matches[1];
+
+            if ( ! isset($matches[2]))
+            {
+                $url = "mailto:$url";
+            }
+
+            return array(
+                'extent' => strlen($matches[0]),
+                'element' => array(
+                    'name' => 'a',
+                    'text' => $matches[1],
+                    'attributes' => array(
+                        'href' => $url,
+                    ),
+                ),
+            );
+        }
+    }
+
+    protected function inlineEmphasis($Excerpt)
+    {
+        if ( ! isset($Excerpt['text'][1]))
+        {
+            return;
+        }
+
+        $marker = $Excerpt['text'][0];
+
+        if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
+        {
+            $emphasis = 'strong';
+        }
+        elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
+        {
+            $emphasis = 'em';
+        }
+        else
+        {
+            return;
+        }
+
+        return array(
+            'extent' => strlen($matches[0]),
+            'element' => array(
+                'name' => $emphasis,
+                'handler' => array(
+                    'function' => 'lineElements',
+                    'argument' => $matches[1],
+                    'destination' => 'elements',
+                )
+            ),
+        );
+    }
+
+    protected function inlineEscapeSequence($Excerpt)
+    {
+        if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
+        {
+            return array(
+                'element' => array('rawHtml' => $Excerpt['text'][1]),
+                'extent' => 2,
+            );
+        }
+    }
+
+    protected function inlineImage($Excerpt)
+    {
+        if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
+        {
+            return;
+        }
+
+        $Excerpt['text']= substr($Excerpt['text'], 1);
+
+        $Link = $this->inlineLink($Excerpt);
+
+        if ($Link === null)
+        {
+            return;
+        }
+
+        $Inline = array(
+            'extent' => $Link['extent'] + 1,
+            'element' => array(
+                'name' => 'img',
+                'attributes' => array(
+                    'src' => $Link['element']['attributes']['href'],
+                    'alt' => $Link['element']['handler']['argument'],
+                ),
+                'autobreak' => true,
+            ),
+        );
+
+        $Inline['element']['attributes'] += $Link['element']['attributes'];
+
+        unset($Inline['element']['attributes']['href']);
+
+        return $Inline;
+    }
+
+    protected function inlineLink($Excerpt)
+    {
+        $Element = array(
+            'name' => 'a',
+            'handler' => array(
+                'function' => 'lineElements',
+                'argument' => null,
+                'destination' => 'elements',
+            ),
+            'nonNestables' => array('Url', 'Link'),
+            'attributes' => array(
+                'href' => null,
+                'title' => null,
+            ),
+        );
+
+        $extent = 0;
+
+        $remainder = $Excerpt['text'];
+
+        if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
+        {
+            $Element['handler']['argument'] = $matches[1];
+
+            $extent += strlen($matches[0]);
+
+            $remainder = substr($remainder, $extent);
+        }
+        else
+        {
+            return;
+        }
+
+        if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches))
+        {
+            $Element['attributes']['href'] = $matches[1];
+
+            if (isset($matches[2]))
+            {
+                $Element['attributes']['title'] = substr($matches[2], 1, - 1);
+            }
+
+            $extent += strlen($matches[0]);
+        }
+        else
+        {
+            if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
+            {
+                $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument'];
+                $definition = strtolower($definition);
+
+                $extent += strlen($matches[0]);
+            }
+            else
+            {
+                $definition = strtolower($Element['handler']['argument']);
+            }
+
+            if ( ! isset($this->DefinitionData['Reference'][$definition]))
+            {
+                return;
+            }
+
+            $Definition = $this->DefinitionData['Reference'][$definition];
+
+            $Element['attributes']['href'] = $Definition['url'];
+            $Element['attributes']['title'] = $Definition['title'];
+        }
+
+        return array(
+            'extent' => $extent,
+            'element' => $Element,
+        );
+    }
+
+    protected function inlineMarkup($Excerpt)
+    {
+        if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false)
+        {
+            return;
+        }
+
+        if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches))
+        {
+            return array(
+                'element' => array('rawHtml' => $matches[0]),
+                'extent' => strlen($matches[0]),
+            );
+        }
+
+        if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?+[^-])*-->/s', $Excerpt['text'], $matches))
+        {
+            return array(
+                'element' => array('rawHtml' => $matches[0]),
+                'extent' => strlen($matches[0]),
+            );
+        }
+
+        if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches))
+        {
+            return array(
+                'element' => array('rawHtml' => $matches[0]),
+                'extent' => strlen($matches[0]),
+            );
+        }
+    }
+
+    protected function inlineSpecialCharacter($Excerpt)
+    {
+        if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false
+            and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches)
+        ) {
+            return array(
+                'element' => array('rawHtml' => '&' . $matches[1] . ';'),
+                'extent' => strlen($matches[0]),
+            );
+        }
+
+        return;
+    }
+
+    protected function inlineStrikethrough($Excerpt)
+    {
+        if ( ! isset($Excerpt['text'][1]))
+        {
+            return;
+        }
+
+        if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
+        {
+            return array(
+                'extent' => strlen($matches[0]),
+                'element' => array(
+                    'name' => 'del',
+                    'handler' => array(
+                        'function' => 'lineElements',
+                        'argument' => $matches[1],
+                        'destination' => 'elements',
+                    )
+                ),
+            );
+        }
+    }
+
+    protected function inlineUrl($Excerpt)
+    {
+        if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
+        {
+            return;
+        }
+
+        if (strpos($Excerpt['context'], 'http') !== false
+            and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)
+        ) {
+            $url = $matches[0][0];
+
+            $Inline = array(
+                'extent' => strlen($matches[0][0]),
+                'position' => $matches[0][1],
+                'element' => array(
+                    'name' => 'a',
+                    'text' => $url,
+                    'attributes' => array(
+                        'href' => $url,
+                    ),
+                ),
+            );
+
+            return $Inline;
+        }
+    }
+
+    protected function inlineUrlTag($Excerpt)
+    {
+        if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches))
+        {
+            $url = $matches[1];
+
+            return array(
+                'extent' => strlen($matches[0]),
+                'element' => array(
+                    'name' => 'a',
+                    'text' => $url,
+                    'attributes' => array(
+                        'href' => $url,
+                    ),
+                ),
+            );
+        }
+    }
+
+    # ~
+
+    protected function unmarkedText($text)
+    {
+        $Inline = $this->inlineText($text);
+        return $this->element($Inline['element']);
+    }
+
+    #
+    # Handlers
+    #
+
+    protected function handle(array $Element)
+    {
+        if (isset($Element['handler']))
+        {
+            if (!isset($Element['nonNestables']))
+            {
+                $Element['nonNestables'] = array();
+            }
+
+            if (is_string($Element['handler']))
+            {
+                $function = $Element['handler'];
+                $argument = $Element['text'];
+                unset($Element['text']);
+                $destination = 'rawHtml';
+            }
+            else
+            {
+                $function = $Element['handler']['function'];
+                $argument = $Element['handler']['argument'];
+                $destination = $Element['handler']['destination'];
+            }
+
+            $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']);
+
+            if ($destination === 'handler')
+            {
+                $Element = $this->handle($Element);
+            }
+
+            unset($Element['handler']);
+        }
+
+        return $Element;
+    }
+
+    protected function handleElementRecursive(array $Element)
+    {
+        return $this->elementApplyRecursive(array($this, 'handle'), $Element);
+    }
+
+    protected function handleElementsRecursive(array $Elements)
+    {
+        return $this->elementsApplyRecursive(array($this, 'handle'), $Elements);
+    }
+
+    protected function elementApplyRecursive($closure, array $Element)
+    {
+        $Element = call_user_func($closure, $Element);
+
+        if (isset($Element['elements']))
+        {
+            $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']);
+        }
+        elseif (isset($Element['element']))
+        {
+            $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']);
+        }
+
+        return $Element;
+    }
+
+    protected function elementApplyRecursiveDepthFirst($closure, array $Element)
+    {
+        if (isset($Element['elements']))
+        {
+            $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']);
+        }
+        elseif (isset($Element['element']))
+        {
+            $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']);
+        }
+
+        $Element = call_user_func($closure, $Element);
+
+        return $Element;
+    }
+
+    protected function elementsApplyRecursive($closure, array $Elements)
+    {
+        foreach ($Elements as &$Element)
+        {
+            $Element = $this->elementApplyRecursive($closure, $Element);
+        }
+
+        return $Elements;
+    }
+
+    protected function elementsApplyRecursiveDepthFirst($closure, array $Elements)
+    {
+        foreach ($Elements as &$Element)
+        {
+            $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element);
+        }
+
+        return $Elements;
+    }
+
+    protected function element(array $Element)
+    {
+        if ($this->safeMode)
+        {
+            $Element = $this->sanitiseElement($Element);
+        }
+
+        # identity map if element has no handler
+        $Element = $this->handle($Element);
+
+        $hasName = isset($Element['name']);
+
+        $markup = '';
+
+        if ($hasName)
+        {
+            $markup .= '<' . $Element['name'];
+
+            if (isset($Element['attributes']))
+            {
+                foreach ($Element['attributes'] as $name => $value)
+                {
+                    if ($value === null)
+                    {
+                        continue;
+                    }
+
+                    $markup .= " $name=\"".self::escape($value).'"';
+                }
+            }
+        }
+
+        $permitRawHtml = false;
+
+        if (isset($Element['text']))
+        {
+            $text = $Element['text'];
+        }
+        // very strongly consider an alternative if you're writing an
+        // extension
+        elseif (isset($Element['rawHtml']))
+        {
+            $text = $Element['rawHtml'];
+
+            $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
+            $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
+        }
+
+        $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']);
+
+        if ($hasContent)
+        {
+            $markup .= $hasName ? '>' : '';
+
+            if (isset($Element['elements']))
+            {
+                $markup .= $this->elements($Element['elements']);
+            }
+            elseif (isset($Element['element']))
+            {
+                $markup .= $this->element($Element['element']);
+            }
+            else
+            {
+                if (!$permitRawHtml)
+                {
+                    $markup .= self::escape($text, true);
+                }
+                else
+                {
+                    $markup .= $text;
+                }
+            }
+
+            $markup .= $hasName ? '</' . $Element['name'] . '>' : '';
+        }
+        elseif ($hasName)
+        {
+            $markup .= ' />';
+        }
+
+        return $markup;
+    }
+
+    protected function elements(array $Elements)
+    {
+        $markup = '';
+
+        $autoBreak = true;
+
+        foreach ($Elements as $Element)
+        {
+            if (empty($Element))
+            {
+                continue;
+            }
+
+            $autoBreakNext = (isset($Element['autobreak'])
+                ? $Element['autobreak'] : isset($Element['name'])
+            );
+            // (autobreak === false) covers both sides of an element
+            $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext;
+
+            $markup .= ($autoBreak ? "\n" : '') . $this->element($Element);
+            $autoBreak = $autoBreakNext;
+        }
+
+        $markup .= $autoBreak ? "\n" : '';
+
+        return $markup;
+    }
+
+    # ~
+
+    protected function li($lines)
+    {
+        $Elements = $this->linesElements($lines);
+
+        if ( ! in_array('', $lines)
+            and isset($Elements[0]) and isset($Elements[0]['name'])
+            and $Elements[0]['name'] === 'p'
+        ) {
+            unset($Elements[0]['name']);
+        }
+
+        return $Elements;
+    }
+
+    #
+    # AST Convenience
+    #
+
+    /**
+     * Replace occurrences $regexp with $Elements in $text. Return an array of
+     * elements representing the replacement.
+     */
+    protected static function pregReplaceElements($regexp, $Elements, $text)
+    {
+        $newElements = array();
+
+        while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE))
+        {
+            $offset = $matches[0][1];
+            $before = substr($text, 0, $offset);
+            $after = substr($text, $offset + strlen($matches[0][0]));
+
+            $newElements[] = array('text' => $before);
+
+            foreach ($Elements as $Element)
+            {
+                $newElements[] = $Element;
+            }
+
+            $text = $after;
+        }
+
+        $newElements[] = array('text' => $text);
+
+        return $newElements;
+    }
+
+    #
+    # Deprecated Methods
+    #
+
+    function parse($text)
+    {
+        $markup = $this->text($text);
+
+        return $markup;
+    }
+
+    protected function sanitiseElement(array $Element)
+    {
+        static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
+        static $safeUrlNameToAtt  = array(
+            'a'   => 'href',
+            'img' => 'src',
+        );
+
+        if ( ! isset($Element['name']))
+        {
+            unset($Element['attributes']);
+            return $Element;
+        }
+
+        if (isset($safeUrlNameToAtt[$Element['name']]))
+        {
+            $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
+        }
+
+        if ( ! empty($Element['attributes']))
+        {
+            foreach ($Element['attributes'] as $att => $val)
+            {
+                # filter out badly parsed attribute
+                if ( ! preg_match($goodAttribute, $att))
+                {
+                    unset($Element['attributes'][$att]);
+                }
+                # dump onevent attribute
+                elseif (self::striAtStart($att, 'on'))
+                {
+                    unset($Element['attributes'][$att]);
+                }
+            }
+        }
+
+        return $Element;
+    }
+
+    protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
+    {
+        foreach ($this->safeLinksWhitelist as $scheme)
+        {
+            if (self::striAtStart($Element['attributes'][$attribute], $scheme))
+            {
+                return $Element;
+            }
+        }
+
+        $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);
+
+        return $Element;
+    }
+
+    #
+    # Static Methods
+    #
+
+    protected static function escape($text, $allowQuotes = false)
+    {
+        return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
+    }
+
+    protected static function striAtStart($string, $needle)
+    {
+        $len = strlen($needle);
+
+        if ($len > strlen($string))
+        {
+            return false;
+        }
+        else
+        {
+            return strtolower(substr($string, 0, $len)) === strtolower($needle);
+        }
+    }
+
+    static function instance($name = 'default')
+    {
+        if (isset(self::$instances[$name]))
+        {
+            return self::$instances[$name];
+        }
+
+        $instance = new static();
+
+        self::$instances[$name] = $instance;
+
+        return $instance;
+    }
+
+    private static $instances = array();
+
+    #
+    # Fields
+    #
+
+    protected $DefinitionData;
+
+    #
+    # Read-Only
+
+    protected $specialCharacters = array(
+        '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~'
+    );
+
+    protected $StrongRegex = array(
+        '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s',
+        '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us',
+    );
+
+    protected $EmRegex = array(
+        '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
+        '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
+    );
+
+    protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
+
+    protected $voidElements = array(
+        'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
+    );
+
+    protected $textLevelElements = array(
+        'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
+        'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
+        'i', 'rp', 'del', 'code',          'strike', 'marquee',
+        'q', 'rt', 'ins', 'font',          'strong',
+        's', 'tt', 'kbd', 'mark',
+        'u', 'xm', 'sub', 'nobr',
+                   'sup', 'ruby',
+                   'var', 'span',
+                   'wbr', 'time',
+    );
+}
diff --git a/public/lib/Router.php b/public/lib/Router.php
@@ -0,0 +1,104 @@
+<?php
+
+class Router {
+
+  private static $routes = [];
+  private static $pathNotFound = null;
+  private static $methodNotAllowed = null;
+
+  public static function add($method, $expression, $function){
+    array_push(self::$routes, [
+      'expression' => $expression,
+      'function' => $function,
+      'method' => $method
+    ]);
+  }
+
+  public static function pathNotFound($function){
+    self::$pathNotFound = $function;
+  }
+
+  public static function methodNotAllowed($function){
+    self::$methodNotAllowed = $function;
+  }
+
+  public static function run($basepath = '/'){
+
+    // Parse current url
+    $parsed_url = parse_url($_SERVER['REQUEST_URI']);//Parse Uri
+
+    if(isset($parsed_url['path'])){
+      $path = $parsed_url['path'];
+    }else{
+      $path = '/';
+    }
+
+    // Get current request method
+    $method = $_SERVER['REQUEST_METHOD'];
+
+    $path_match_found = false;
+
+    $route_match_found = false;
+
+    foreach(self::$routes as $route){
+
+      // If the method matches check the path
+
+      // Add basepath to matching string
+      if($basepath!=''&&$basepath!='/'){
+        $route['expression'] = '('.$basepath.')'.$route['expression'];
+      }
+
+      // Add 'find string start' automatically
+      $route['expression'] = '^'.$route['expression'];
+
+      // Add 'find string end' automatically
+      $route['expression'] = $route['expression'].'$';
+
+      // echo $route['expression'].'<br/>';
+
+      // Check path match	
+      if(preg_match('#'.$route['expression'].'#',$path,$matches)){
+
+        $path_match_found = true;
+
+        // Check method match
+        if(strtolower($method) == strtolower($route['method'])){
+
+          array_shift($matches);// Always remove first element. This contains the whole string
+
+          if($basepath!=''&&$basepath!='/'){
+            array_shift($matches);// Remove basepath
+          }
+
+          call_user_func_array($route['function'], $matches);
+
+          $route_match_found = true;
+
+          // Do not check other routes
+          break;
+        }
+      }
+    }
+
+    // No matching route was found
+    if(!$route_match_found){
+
+      // But a matching path exists
+      if($path_match_found){
+        header("HTTP/1.0 405 Method Not Allowed");
+        if(self::$methodNotAllowed){
+          call_user_func_array(self::$methodNotAllowed, Array($path,$method));
+        }
+      }else{
+        header("HTTP/1.0 404 Not Found");
+        if(self::$pathNotFound){
+          call_user_func_array(self::$pathNotFound, Array($path));
+        }
+      }
+
+    }
+
+  }
+
+}
diff --git a/public/lib/TinyHtmlMinifier.php b/public/lib/TinyHtmlMinifier.php
@@ -0,0 +1,284 @@
+<?php
+//Source: https://github.com/jenstornell/tiny-html-minifier
+
+class TinyHtmlMinifier
+{
+    private $options;
+    private $output;
+    private $build;
+    private $skip;
+    private $skipName;
+    private $head;
+    private $elements;
+
+    public function __construct(array $options)
+    {
+        $this->options = $options;
+        $this->output = '';
+        $this->build = [];
+        $this->skip = 0;
+        $this->skipName = '';
+        $this->head = false;
+        $this->elements = [
+            'skip' => [
+                'code',
+                'pre',
+                'script',
+                'textarea',
+            ],
+            'inline' => [
+                'a',
+                'abbr',
+                'acronym',
+                'b',
+                'bdo',
+                'big',
+                'br',
+                'cite',
+                'code',
+                'dfn',
+                'em',
+                'i',
+                'img',
+                'kbd',
+                'map',
+                'object',
+                'samp',
+                'small',
+                'span',
+                'strong',
+                'sub',
+                'sup',
+                'tt',
+                'var',
+                'q',
+            ],
+            'hard' => [
+                '!doctype',
+                'body',
+                'html',
+            ]
+        ];
+    }
+
+    // Run minifier
+    public function minify(string $html) : string
+    {
+        if (!isset($this->options['disable_comments']) ||
+            !$this->options['disable_comments']) {
+            $html = $this->removeComments($html);
+        }
+
+        $rest = $html;
+
+        while (!empty($rest)) {
+            $parts = explode('<', $rest, 2);
+            $this->walk($parts[0]);
+            $rest = (isset($parts[1])) ? $parts[1] : '';
+        }
+
+        return $this->output;
+    }
+
+    // Walk trough html
+    private function walk(&$part)
+    {
+        $tag_parts = explode('>', $part);
+        $tag_content = $tag_parts[0];
+
+        if (!empty($tag_content)) {
+            $name = $this->findName($tag_content);
+            $element = $this->toElement($tag_content, $part, $name);
+            $type = $this->toType($element);
+
+            if ($name == 'head') {
+                $this->head = $type === 'open';
+            }
+
+            $this->build[] = [
+                'name' => $name,
+                'content' => $element,
+                'type' => $type
+            ];
+
+            $this->setSkip($name, $type);
+
+            if (!empty($tag_content)) {
+                $content = (isset($tag_parts[1])) ? $tag_parts[1] : '';
+                if ($content !== '') {
+                    $this->build[] = [
+                        'content' => $this->compact($content, $name, $element),
+                        'type' => 'content'
+                    ];
+                }
+            }
+
+            $this->buildHtml();
+        }
+    }
+
+    // Remove comments
+    private function removeComments($content = '')
+    {
+        return preg_replace('/(?=<!--)([\s\S]*?)-->/', '', $content);
+    }
+
+    // Check if string contains string
+    private function contains($needle, $haystack)
+    {
+        return strpos($haystack, $needle) !== false;
+    }
+
+    // Return type of element
+    private function toType($element)
+    {
+        return (substr($element, 1, 1) == '/') ? 'close' : 'open';
+    }
+
+    // Create element
+    private function toElement($element, $noll, $name)
+    {
+        $element = $this->stripWhitespace($element);
+        $element = $this->addChevrons($element, $noll);
+        $element = $this->removeSelfSlash($element);
+        $element = $this->removeMeta($element, $name);
+        return $element;
+    }
+
+    // Remove unneeded element meta
+    private function removeMeta($element, $name)
+    {
+        if ($name == 'style') {
+            $element = str_replace(
+                [
+                    ' type="text/css"',
+                    "' type='text/css'"
+                ],
+                ['', ''],
+                $element
+            );
+        } elseif ($name == 'script') {
+            $element = str_replace(
+                [
+                    ' type="text/javascript"',
+                    " type='text/javascript'"
+                ],
+                ['', ''],
+                $element
+            );
+        }
+        return $element;
+    }
+
+    // Strip whitespace from element
+    private function stripWhitespace($element)
+    {
+        if ($this->skip == 0) {
+            $element = preg_replace('/\s+/', ' ', $element);
+        }
+        return trim($element);
+    }
+
+    // Add chevrons around element
+    private function addChevrons($element, $noll)
+    {
+        if (empty($element)) {
+            return $element;
+        }
+        $char = ($this->contains('>', $noll)) ? '>' : '';
+        $element = '<' . $element . $char;
+        return $element;
+    }
+
+    // Remove unneeded self slash
+    private function removeSelfSlash($element)
+    {
+        if (substr($element, -3) == ' />') {
+            $element = substr($element, 0, -3) . '>';
+        }
+        return $element;
+    }
+
+    // Compact content
+    private function compact($content, $name, $element)
+    {
+        if ($this->skip != 0) {
+            $name = $this->skipName;
+        } else {
+            $content = preg_replace('/\s+/', ' ', $content);
+        }
+
+        if (in_array($name, $this->elements['skip'])) {
+            return $content;
+        } elseif (in_array($name, $this->elements['hard']) ||
+            $this->head) {
+            return $this->minifyHard($content);
+        } else {
+            return $this->minifyKeepSpaces($content);
+        }
+    }
+
+    // Build html
+    private function buildHtml()
+    {
+        foreach ($this->build as $build) {
+
+            if (!empty($this->options['collapse_whitespace'])) {
+
+                if (strlen(trim($build['content'])) == 0)
+                    continue;
+
+                elseif ($build['type'] != 'content' && !in_array($build['name'], $this->elements['inline']))
+                    trim($build['content']);
+
+            }
+
+            $this->output .= $build['content'];
+        }
+
+        $this->build = [];
+    }
+
+    // Find name by part
+    private function findName($part)
+    {
+        $name_cut = explode(" ", $part, 2)[0];
+        $name_cut = explode(">", $name_cut, 2)[0];
+        $name_cut = explode("\n", $name_cut, 2)[0];
+        $name_cut = preg_replace('/\s+/', '', $name_cut);
+        $name_cut = strtolower(str_replace('/', '', $name_cut));
+        return $name_cut;
+    }
+
+    // Set skip if elements are blocked from minification
+    private function setSkip($name, $type)
+    {
+        foreach ($this->elements['skip'] as $element) {
+            if ($element == $name && $this->skip == 0) {
+                $this->skipName = $name;
+            }
+        }
+        if (in_array($name, $this->elements['skip'])) {
+            if ($type == 'open') {
+                $this->skip++;
+            }
+            if ($type == 'close') {
+                $this->skip--;
+            }
+        }
+    }
+
+    // Minify all, even spaces between elements
+    private function minifyHard($element)
+    {
+        $element = preg_replace('!\s+!', ' ', $element);
+        $element = trim($element);
+        return trim($element);
+    }
+
+    // Strip but keep one space
+    private function minifyKeepSpaces($element)
+    {
+        return preg_replace('!\s+!', ' ', $element);
+    }
+}
diff --git a/public/lib/helpers.php b/public/lib/helpers.php
@@ -0,0 +1,33 @@
+<?php
+
+function mgToWeight(int $value, string $unit) {
+	if ( $unit === 'g' ) {
+		return number_format(($value / 1000), 0);
+	} elseif ( $unit === 'kg' ) {
+		return number_format(($value / 1000000), 2);
+    }
+}
+
+function renderUnits (string $unit) {
+	return [
+		[
+			'unit'     => 'g',
+			'selected' => ($unit == 'g')
+		],
+		[
+			'unit'     => 'kg',
+			'selected' => ($unit == 'kg')
+		]
+	];
+}
+
+function response (int $responseCode, null|string $message, null|array $extraData = NULL) {
+	$response = [];
+
+	if ( $message   !== NULL ) $response['message'] = $message;
+	if ( $extraData !== NULL ) $response            = array_merge($response, $extraData);
+
+
+	http_response_code($responseCode);
+	exit(json_encode($response));	
+}
diff --git a/public/templates/categories.mustache b/public/templates/categories.mustache
@@ -0,0 +1,31 @@
+{{#categories}}
+<li class="lpCategory" id="{{id}}">
+	<ul class="lpItems lpDataTable">
+		<li class="lpHeader lpItemsHeader">
+			<h2 class="lpCategoryName">{{name}}</h2>
+
+			{{#show.price}}
+			<span class="lpPriceCell">Price</span>
+			{{/show.price}}
+
+			{{#show.weight}}
+			<span class="lpWeightCell">Weight</span>
+			{{/show.weight}}
+
+			<span class="lpQtyCell">qty</span>
+		</li>
+		{{>item}}
+		<li class="lpFooter lpItemsFooter">
+			{{#show.price}}
+			<span class="lpPriceCell lpNumber"><div class="lpPriceSubtotal">{{subtotalPrice}} {{currencySymbol}}</div></span>
+			{{/show.price}}
+
+			{{#show.weight}}
+			<span class="lpWeightCell lpNumber"><div class="lpSubtotal"><span class="lpDisplaySubtotal" mg="{{subtotalWeight}}">{{subtotalWeightDisplay}}</span> <span class="lpSubtotalUnit">{{totalUnit}}</span></div></span>
+			{{/show.weight}}
+
+			<span class="lpQtyCell lpNumber"><div class="lpSubtotal"><span class="lpQtySubtotal">{{subtotalQty}}</span></div></span>
+		</li>
+	</ul>
+</li>
+{{/categories}}
diff --git a/public/templates/item.mustache b/public/templates/item.mustache
@@ -0,0 +1,46 @@
+{{#items}}
+<li class="lpItem {{classes}} {{#imageUrl}}lpItemHasImage{{/imageUrl}} {{#price}}lpItemHasPrice{{/price}}" id="{{id}}">
+	{{#show.images}}
+		<span class="lpImageCell">
+			{{#imageUrl}}
+				<img class="lpItemImage" src="{{imageUrl}}" href="{{imageUrl}}" />
+			{{/imageUrl}}
+		</span>
+	{{/show.images}}
+
+	<span class="lpName">
+		{{#url}}
+			<a href="{{url}}" target="_blank" class="lpHref">
+		{{/url}}
+
+		{{name}}
+
+		{{#url}}
+			</a>
+		{{/url}}
+	</span>
+
+	<span class="lpDescription">{{description}}</span>
+
+	<span class="lpActionsCell">
+		<i class="lpSprite lpWorn {{#worn}}lpActive{{/worn}} {{wornClass}}" title="This item is worn and not counted in pack weight."></i>
+		<i class="lpSprite lpConsumable {{#consumable}}lpActive{{/consumable}} {{consumableClass}}" title="This item is a consumable and not counted in pack weight."></i>
+		<i class="lpSprite lpStar {{#star}}lpStar{{star}}{{/star}} {{^star}}lpHidden{{/star}}" title="This item is starred"></i>
+	</span>
+
+	{{#show.price}}
+		<span class="lpPriceCell lpNumber">
+			{{price}} {{currencySymbol}}
+		</span>
+	{{/show.price}}
+	
+	{{#show.weight}}
+	<span class="lpWeightCell lpNumber">
+		<span class="lpWeight">{{displayWeight}}</span>
+		{{>unitSelect}}
+	</span>
+	{{/show.weight}}
+
+	<span class="lpQtyCell lpNumber">{{qty}}</span>
+</li>
+{{/items}}
diff --git a/public/templates/total.mustache b/public/templates/total.mustache
@@ -0,0 +1,97 @@
+<ul class="lpTotals lpTable lpDataTable">
+	<li class="lpRow lpHeader">
+		<span class="lpCell">&nbsp;</span>
+		<span class="lpCell">
+			Category
+		</span>
+		{{#show.price}}
+		<span class="lpCell">
+			Price
+		</span>
+		{{/show.price}}
+		<span class="lpCell">
+			Weight
+		</span>
+	</li>
+
+	{{#categories}}
+		<li class="lpTotalCategory lpRow" id="total_{{id}}" category="{{id}}">
+			<span class="lpCell lpLegendCell">
+				<span class="lpLegend" style="background-color: {{displayColor}}"></span>
+			</span>
+			<span class="lpCell">
+				{{name}}
+			</span>
+			{{#show.price}}
+			<span class="lpCell lpNumber">
+				{{currencySymbol}}{{subtotalPriceDisplay}}
+			</span>
+			{{/show.price}}
+			<span class="lpCell lpNumber">
+				<div class="lpSubtotal"><span class="lpDisplaySubtotal"  mg="{{subtotalWeight}}">{{subtotalWeightDisplay}}</span> <span class="lpSubtotalUnit">{{totalUnit}}</span></div>
+			</span>
+		</li>
+	{{/categories}}
+
+	<li class="lpRow lpFooter lpTotal">
+		<span class="lpCell"></span>
+		<span class="lpCell lpSubtotal">
+			Total
+		</span>
+		{{#show.price}}
+		<span class="lpCell lpNumber lpSubtotal items">{{currencySymbol}}{{totalPriceDisplay}}</span>
+		{{/show.price}}
+		<span class="lpCell lpNumber lpSubtotal">
+			<span class="lpTotalValue" title="{{listTotalQty}} items">{{listTotalWeightDisplay}}</span>
+			<span class="lpTotalUnit">{{>unitSelect}}</span>
+		</span>
+	</li>
+
+	{{#totalConsumableWeight}}
+		<li data-weight-type="consumable" class="lpRow lpFooter lpBreakdown lpConsumableWeight">
+			<span class="lpCell"></span>
+			<span class="lpCell lpSubtotal">
+				Consumable
+			</span>
+			{{#show.price}}
+			<span class="lpCell"></span>
+			{{/show.price}}
+			<span class="lpCell lpNumber lpSubtotal">
+				<span class="lpDisplaySubtotal" mg="{{totalConsumableWeight}}">{{totalConsumableWeightDisplay}}</span>
+				<span class="lpSubtotalUnit">{{totalUnit}}</span>
+			</span>
+		</li>
+	{{/totalConsumableWeight}}
+
+	{{#totalWornWeight}}
+		<li data-weight-type="worn" class="lpRow lpFooter lpBreakdown lpWornWeight">
+			<span class="lpCell"></span>
+			<span class="lpCell lpSubtotal">
+				Worn
+			</span>
+			{{#show.price}}
+			<span class="lpCell"></span>
+			{{/show.price}}
+			<span class="lpCell lpNumber lpSubtotal">
+				<span class="lpDisplaySubtotal" mg="{{totalWornWeight}}">{{totalWornWeightDisplay}}</span>
+				<span class="lpSubtotalUnit">{{totalUnit}}</span>
+			</span>
+		</li>
+	{{/totalWornWeight}}
+
+	{{#shouldDisplayPackWeight}}
+		<li data-weight-type="base" class="lpRow lpFooter lpBreakdown lpPackWeight">
+			<span class="lpCell"></span>
+			<span class="lpCell lpSubtotal">
+				Base Weight
+			</span>
+			{{#show.price}}
+			<span class="lpCell"></span>
+			{{/show.price}}
+			<span class="lpCell lpNumber lpSubtotal">
+				<span class="lpDisplaySubtotal" mg="{{totalPackWeight}}">{{totalPackWeightDisplay}}</span>
+				<span class="lpSubtotalUnit">{{talUnit}}</span>
+			</span>
+		</li>
+	{{/shouldDisplayPackWeight}}
+</ul>
diff --git a/public/templates/unitSelect.mustache b/public/templates/unitSelect.mustache
@@ -0,0 +1,30 @@
+<div class="lpUnitSelect">
+	<select class="lpUnit lpInvisible">
+		{{#units}}
+			<option value="{{unit}}" {{#selected}}selected{{/selected}}>{{unit}}</option>
+		{{/units}}
+	</select>
+
+	{{#weight}}
+		<input type="hidden" class="lpMG" value="{{weight}}">
+	{{/weight}}
+
+	{{^weight}}
+		<input type="hidden" class="lpMG" value="{{listTotalWeight}}"/>
+	{{/weight}}
+
+	{{#authorUnit}}
+		<span class="lpDisplay">{{authorUnit}}</span>
+	{{/authorUnit}}
+
+	{{^authorUnit}}
+		<span class="lpDisplay">{{totalUnit}}</span>
+	{{/authorUnit}}
+
+	<i class="lpSprite lpExpand"></i>
+	<ul class="lpUnitDropdown">
+		{{#units}}
+			<li class="{{unit}}">{{unit}}</li>
+		{{/units}}
+	</ul>
+</div>
diff --git a/public/templates/view.mustache b/public/templates/view.mustache
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html class="lp">
+	<head>
+		<title>{{listName}}</title>
+		<link href='/css/fonts.css' rel='stylesheet' type='text/css' />
+		<link href='/dist/view.css' rel='stylesheet' type='text/css' />
+		<style>.lpModalOverlay, #lpImageDialog { display: none;}</style>
+		<meta name="viewport" content="width=device-width; initial-scale=1.0">
+	</head>
+	<body>
+		<div id="main" class="lpShare">
+			<nav>
+			{{#lists}}
+				<a {{#active}}class="active"{{/active}} href="/list/{{id}}">{{name}}</a>
+			{{/lists}}
+				<a class="right" href="/editor">edit</a>
+			</nav>
+
+			<div class="lpList {{#show.images}}lpShowImages{{/show.images}} {{#show.worn}}lpShowWorn{{/show.worn}} {{#show.consumable}}lpShowConsumable{{/show.consumable}} {{#show.price}}lpShowPrices{{/show.price}}">
+				<h1 class="lpListName">{{listName}}</h1>
+
+				{{#show.weight}}
+				<div class="lpListSummary">
+					<canvas id="chartContainer" class="lpChart" height="260" width="260"></canvas>
+					<div class="lpTotalsContainer">
+						{{>total}}
+					</div>
+				</div>
+				<div style="clear:both"></div>
+				{{/show.weight}}
+
+				{{#listDescription}}
+					<div id="lpListDescription">
+						{{{listDescription}}}
+					</div>
+				{{/listDescription}}
+
+				<ul class="lpCategories">
+					{{>categories}}
+				</ul>
+			</div>
+		</div>
+
+		<div class="lpDialog" id="lpImageDialog"></div>
+		<div class="lpModalOverlay" id="modalOverlay"></div>
+
+		<script type="text/javascript" src="/dist/view.js"></script>
+		{{#chartData}}
+		<script type="text/javascript">chartData="{{{chartData}}}";</script>
+		{{/chartData}}
+	</body>
+</html>
diff --git a/src/app.js b/src/app.js
@@ -0,0 +1,63 @@
+import Vue       from 'vue';
+import VueRouter from 'vue-router';
+
+import store     from './store.js';
+
+import editor    from './views/editor.vue';
+import login     from './views/login.vue';
+
+import focusDirectives from './utils/focus.js';
+import dataTypes       from './dataTypes.js';
+import utils           from './utils/utils.js';
+
+const Item     = dataTypes.Item;
+const Category = dataTypes.Category;
+const List     = dataTypes.List;
+const Library  = dataTypes.Library;
+
+var init = function () {
+	window.app = new Vue({
+		router,
+		store,
+		data: {
+			path: '',
+			fatal: '',
+		},
+		watch: {
+			$route(to, from) {
+				this.path = to.path;
+			},
+		},
+		mounted() {
+			this.path = router.currentRoute.path;
+		},
+	}).$mount('#lp');
+};
+
+
+Vue.use(VueRouter);
+
+window.Vue    = Vue; // surfacing Vue globally for utils methods
+window.bus    = new Vue(); // global event bus
+window.router = new VueRouter({
+	mode: 'history',
+	routes: [
+		{ path: '/login',  component: login  },
+		{ path: '/editor', component: editor },
+		{ path: '*',       component: editor },
+	],
+});
+
+bus.$on('unauthorized', (error) => {
+	window.location = '/login';
+});
+
+store.dispatch('init')
+	.then(() => { init(); })
+	.catch((error) => {
+		if (!store.state.library) {
+			router.push('/login');
+		}
+
+		init();
+	});
diff --git a/src/chart.js b/src/chart.js
@@ -0,0 +1,427 @@
+import colorUtils from './utils/color.js';
+
+export default function (args) {
+	let container;
+	let context;
+	let srcData;
+	let data = {};
+	let bounds;
+	let center;
+	let radius;
+	const pieHole = 0.3;
+	const spacing = 0.1;
+	let hovered = null;
+	let hoverCallback = null;
+	let clickCallback = null;
+	const backgroundColor = 'rgb(245,245,245)';
+	const firstRing = { inner: 25, outer: 70 };
+	const secondRing = { inner: 80, outer: 120 };
+	let tooltip;
+	const isAnimating = false;
+	const frameRate = 10;
+
+	function init() {
+		if (!args.container || (!args.data && !args.processedData)) {
+			console.warn('invalid params!!');
+			return;
+		}
+		container = args.container;
+		container.style.position = 'relative';
+		context = container.getContext('2d');
+		if (args.data) {
+			srcData = args.data;
+			data = preprocess(srcData);
+		} else if (args.processedData) {
+			data = args.processedData;
+		}
+
+		bounds = { x: context.canvas.width, y: context.canvas.height };
+		center = { x: bounds.x / 2, y: bounds.y / 2 };
+
+		if (center.x < center.y) radius = center.x;
+		else radius = center.y;
+
+		if (args.clickCallback) clickCallback = args.clickCallback;
+		if (args.hoverCallback) hoverCallback = args.hoverCallback;
+		drawGraph();
+
+		tooltip = document.createElement('div');
+		tooltip.classList.add('tooltip');
+		document.getElementsByTagName('body')[0].appendChild(tooltip);
+	}
+
+	function update(args) {
+		const oldVisibleRings = getVisibleRings(data);
+
+		if (args.data) {
+			data = preprocess(args.data);
+		} else if (args.processedData) {
+			data = args.processedData;
+		} else {
+			return;
+		}
+		setVisibleRings(oldVisibleRings, data);
+
+		context.clearRect(0, 0, bounds.x, bounds.y);
+		drawGraph();
+	}
+
+	function preprocess(srcData, parent) {
+		const data = {};
+		let total = 0;
+		data.points = {};
+
+		for (var key in srcData) {
+			var value = srcData[key];
+			if (typeof (value) === 'object') {
+				total += preprocess(value).total;
+			} else {
+				total += value;
+			}
+		}
+		data.total = total;
+
+		for (var key in srcData) {
+			var value = srcData[key];
+			if (typeof (value) === 'object') {
+				data.points[key] = preprocess(value, data);
+				data.points[key].name = key;
+				data.points[key].visiblePoints = false;
+			} else {
+				data.points[key] = {
+					value,
+					percent: value / total,
+					name: key,
+					parent: data,
+				};
+			}
+		}
+
+		if (parent) {
+			data.percent = total / parent.total;
+			data.parent = parent;
+		}
+		return data;
+	}
+
+	function render(myData) {
+		const offset = 5;
+		let parentColor = false;
+		let angleMultiplier = 1;
+		let lastAngle = 0.0;
+		const minRadius = myData.innerMinRadius;
+		const maxRadius = myData.innerMaxRadius;
+
+		if (myData.startAngle) lastAngle = myData.startAngle;
+		const startAngle = lastAngle;
+		if (myData.angleMultiplier) angleMultiplier = myData.angleMultiplier;
+
+		if (myData.color) {
+			parentColor = myData.color;
+		}
+
+		let count = 0;
+		for (const key in myData.points) {
+			const slice = myData.points[key];
+
+			slice.startAngle = startAngle;
+
+			slice.minRadius = minRadius;
+			slice.maxRadius = maxRadius;
+			slice.myAngle = slice.percent * Math.PI * 2 * angleMultiplier;
+			slice.minAngle = lastAngle;
+			slice.maxAngle = lastAngle + slice.myAngle;
+			slice.avgAngle = (slice.minAngle + slice.maxAngle) / 2;
+			slice.outsideMinPoint = {
+				x: center.x + Math.cos(slice.minAngle) * maxRadius,
+				y: center.y + Math.sin(slice.minAngle) * maxRadius,
+			};
+			slice.outsideMaxPoint = {
+				x: center.x + Math.cos(slice.maxAngle) * maxRadius,
+				y: center.y + Math.sin(slice.maxAngle) * maxRadius,
+			};
+			slice.insideMinPoint = {
+				x: center.x + Math.cos(slice.minAngle) * slice.minRadius,
+				y: center.y + Math.sin(slice.minAngle) * slice.minRadius,
+			};
+			slice.insideMaxPoint = {
+				x: center.x + Math.cos(slice.maxAngle) * slice.minRadius,
+				y: center.y + Math.sin(slice.maxAngle) * slice.minRadius,
+			};
+
+			if (!slice.color) {
+				slice.color = colorUtils.getColor(count, parentColor);
+			}
+
+			slice.startAngle = lastAngle;
+
+			drawSlice(slice);
+
+			lastAngle = slice.maxAngle;
+			count++;
+		}
+	}
+
+	function drawSlice(slice, color) {
+		if (color) {
+			context.strokeStyle = color;
+			context.lineWidth = 2;
+		} else {
+			context.strokeStyle = backgroundColor;
+			context.lineWidth = 3;
+		}
+
+		context.fillStyle = colorUtils.rgbToString(slice.color);
+
+		context.beginPath();
+		context.moveTo(slice.outsideMinPoint.x, slice.outsideMinPoint.y);
+		context.arc(center.x, center.y, slice.maxRadius, slice.minAngle, slice.maxAngle);
+		context.lineTo(slice.insideMaxPoint.x, slice.insideMaxPoint.y);
+		context.arc(center.x, center.y, slice.minRadius, slice.maxAngle, slice.minAngle, true);
+		context.lineTo(slice.outsideMinPoint.x, slice.outsideMinPoint.y);
+		context.stroke();
+		context.fill();
+		context.closePath();
+	}
+
+	function animateAdd() {
+		const rings = getVisibleRings(data);
+		const duration = 600;
+		const d = new Date();
+		for (let i = 0; i < rings.length; i++) {
+			const ring = rings[i];
+			const minMax = getMinMax(i + 1, rings.length);
+			if (ring.innerMinRadius) {
+				ring.startMinRadius = ring.innerMinRadius;
+				ring.startMaxRadius = ring.innerMaxRadius;
+			} else {
+				ring.startMinRadius = minMax[1];
+				ring.startMaxRadius = minMax[1];
+			}
+
+			ring.startAngleMultiplier = 1;
+			if (i == rings.length - 1 && rings.length > 1) {
+				ring.startAngleMultiplier = ring.percent;
+				ring.startMinRadius = minMax[1];
+			}
+
+			ring.targetMinRadius = minMax[0];
+			ring.targetMaxRadius = minMax[1];
+			ring.targetAngleMultiplier = 1;
+
+			ring.deltaMinRadius = ring.targetMinRadius - ring.startMinRadius;
+			ring.deltaMaxRadius = ring.targetMaxRadius - ring.startMaxRadius;
+			ring.deltaAngleMultiplier = ring.targetAngleMultiplier - ring.startAngleMultiplier;
+
+			ring.startTime = d.getTime();
+			ring.finishTime = ring.startTime + duration;
+			ring.deltaTime = duration;
+
+			ring.angleMultiplierFunction = delayThenEase;
+			ring.radiusFunction = easeThenDelay;
+		}
+
+		setTimeout(animateStep, frameRate);
+	}
+
+	function animateStep() {
+		const rings = getVisibleRings(data);
+		const d = new Date();
+		if (d.getTime() - frameRate > rings[0].finishTime) return;
+
+		context.clearRect(0, 0, bounds.x, bounds.y);
+
+		for (const i in rings) {
+			const ring = rings[i];
+
+			let percentAnimated = (d.getTime() - ring.startTime) / ring.deltaTime;
+			if (percentAnimated > 1) percentAnimated = 1;
+			ring.innerMinRadius = ring.startMinRadius + (ring.deltaMinRadius * ring.radiusFunction(percentAnimated));
+			ring.innerMaxRadius = ring.startMaxRadius + (ring.deltaMaxRadius * ring.radiusFunction(percentAnimated));
+			ring.angleMultiplier = ring.startAngleMultiplier + (ring.deltaAngleMultiplier * ring.angleMultiplierFunction(percentAnimated));
+			render(ring);
+		}
+		setTimeout(animateStep, frameRate);
+	}
+
+	function attachEvents() {
+		container.addEventListener('mousemove', hoverHandle);
+		container.addEventListener('click', clickHandle);
+	}
+
+	function hoverHandle(evt) {
+		const containerRect = container.getBoundingClientRect();
+		const size = { x: containerRect.width, y: containerRect.height };
+		const offset = { x: (evt.pageX - containerRect.left) * (bounds.x / size.x), y: (evt.pageY - containerRect.top) * (bounds.y / size.y) };
+		const dX = (offset.x - center.x);
+		const dY = (offset.y - center.y);
+		let angle = Math.atan(dY / dX);
+		if (dX < 0) angle += Math.PI;
+		else if (dY < 0) angle += Math.PI * 2;
+		const radius = Math.sqrt(dX * dX + dY * dY);
+
+		const newHovered = findHovered(data, angle, radius);
+
+		if (newHovered) {
+			if (newHovered != hovered) {
+				if (hovered) drawSlice(hovered);
+				hovered = newHovered;
+				drawSlice(hovered, 'rgb(50,50,50)');
+				container.classList.add('activeHover');
+				if (hoverCallback) hoverCallback(newHovered);
+			}
+			tooltip.style.display = 'block';
+			tooltip.innerText = newHovered.name;
+			tooltip.style.top = `${evt.pageY - 10}px`;
+			tooltip.style.left = `${evt.pageX + 15}px`;
+		} else {
+			if (hovered) {
+				drawSlice(hovered);
+				if (hoverCallback) hoverCallback(newHovered);
+			}
+			hovered = 0;
+			container.classList.remove('activeHover');
+			tooltip.style.display = 'none';
+		}
+	}
+
+	function clickHandle(evt) {
+		if (!hovered) {
+			recursivelySetAttribute(data, 'visiblePoints', false);
+			data.visiblePoints = true;
+			animateAdd();
+			return;
+		}
+		if (!hovered.points) return;
+
+		recursivelySetAttribute(hovered.parent, 'visiblePoints', false);
+		hovered.parent.visiblePoints = true;
+		hovered.visiblePoints = true;
+
+		animateAdd();
+		if (clickCallback) clickCallback(hovered);
+	}
+
+	function open(key) {
+		if (key) {
+			hovered = data.points[key];
+		}
+		if (!hovered) {
+			return;
+		}
+		recursivelySetAttribute(hovered.parent, 'visiblePoints', false);
+		hovered.parent.visiblePoints = true;
+		hovered.visiblePoints = true;
+		animateAdd();
+	}
+
+	function close() {
+		recursivelySetAttribute(data, 'visiblePoints', false);
+		data.visiblePoints = true;
+		context.clearRect(0, 0, bounds.x, bounds.y);
+		animateAdd();
+	}
+
+	function findHovered(data, angle, radius) {
+		for (const key in data.points) {
+			const i = data.points[key];
+			if (i.points && i.visiblePoints) {
+				const found = findHovered(i, angle, radius);
+				if (found) return found;
+			}
+			let startAngle = 0;
+			if (typeof i.parent.startAngle !== 'undefined') startAngle = i.parent.startAngle;
+
+			if (radius < i.maxRadius && radius > i.minRadius) {
+				if (i.minAngle <= Math.PI * 2 && i.maxAngle > Math.PI * 2 && (angle > i.minAngle || angle + Math.PI * 2 < i.maxAngle)) {
+					return i;
+				} if (i.minAngle >= Math.PI * 2 && i.maxAngle > Math.PI * 2 && angle + Math.PI * 2 > i.minAngle && angle + Math.PI * 2 < i.maxAngle) {
+					return i;
+				} if (angle > i.minAngle && angle < i.maxAngle) {
+					return i;
+				}
+			}
+		}
+		return null;
+	}
+
+	function findSectionById(id) {
+		for (const i in data.points) {
+			if (data.points[i].id && data.points[i].id == id) return data.points[i];
+		}
+		return false;
+	}
+
+	function recursivelySetAttribute(data, key, value) {
+		data[key] = value;
+		for (const i in data.points) {
+			recursivelySetAttribute(data.points[i], key, value);
+		}
+	}
+
+	function drawGraph() {
+		data.visiblePoints = true;
+		const rings = getVisibleRings(data);
+
+		for (let i = 0; i < rings.length; i++) {
+			const ring = rings[i];
+			const minMax = getMinMax(i + 1, rings.length);
+			ring.innerMinRadius = minMax[0];
+			ring.innerMaxRadius = minMax[1];
+			render(ring);
+		}
+
+		attachEvents();
+	}
+
+	function getVisibleRings(data) {
+		let out = [data];
+		for (const i in data.points) {
+			if (data.points[i].visiblePoints) {
+				if (data.points[i].points) out = out.concat(getVisibleRings(data.points[i]));
+				break;
+			}
+		}
+		return out;
+	}
+
+	function setVisibleRings(rings, data) {
+		// pass in old visible rings to translate to new data
+		if (typeof data.id !== 'undefined') {
+			for (var i in rings) {
+				if (data.id == rings[i].id) {
+					data.visiblePoints = true;
+				}
+			}
+		}
+		for (var i in data.points) {
+			setVisibleRings(rings, data.points[i]);
+		}
+	}
+
+	function getMinMax(level, levels) {
+		const minRadius = pieHole * (1 / levels) + (((level - 1) / levels) * (1 - pieHole));
+		let maxRadius = pieHole * (1 / levels) + ((level / levels - spacing) * (1 - pieHole));
+		if (levels == 1) {
+			maxRadius *= 0.8;
+		}
+		return [minRadius * radius, maxRadius * radius];
+	}
+
+	function easeThenDelay(x) {
+		if (x > 0.5) return 1;
+		let out = (1 - Math.cos(x * 2 * Math.PI)) / 2;
+		if (out > 1) out = 1;
+		return out;
+	}
+
+	function delayThenEase(x) {
+		if (x < 0.5) return 0;
+		let out = (1 - Math.cos((x - 0.5) * 2 * Math.PI)) / 2;
+		if (out > 1) out = 1;
+		return out;
+	}
+
+	init();
+	return { update, open, close };
+};
diff --git a/src/components/category.vue b/src/components/category.vue
@@ -0,0 +1,84 @@
+<style lang="scss">
+
+.lpQtySubtotal {
+	padding-right: 25px; /* Accommodates delete column */
+}
+
+.lpPriceSubtotal { /* unused? */
+	padding-right: 4px;
+}
+
+</style>
+
+<template>
+	<li :id="category.id" class="lpCategory">
+		<ul class="lpItems lpDataTable">
+			<li class="lpHeader lpItemsHeader">
+				<span class="handleCell">
+					<div class="handle lpCategoryHandle" title="Reorder this category" />
+				</span>
+				<input v-focus-on-create="category._isNew" type="text" :value="category.name" placeholder="Category Name" class="lpCategoryName lpSilent" @input="updateCategoryName">
+				<span v-if="library.optionalFields['price']" class="lpPriceCell">Price</span>
+				<span class="lpWeightCell">Weight</span>
+				<span class="lpQtyCell">qty</span>
+				<span class="lpRemoveCell"><a class="lpRemove lpRemoveCategory" title="Remove this category" @click="removeCategory(category)"><i class="lpSprite lpSpriteRemove" /></a></span>
+			</li>
+			<item v-for="itemContainer in itemContainers" :key="itemContainer.item.id" :item-container="itemContainer" :category="category" @keyup.enter.native="newItem" />
+			<li class="lpFooter lpItemsFooter">
+				<span class="lpAddItemCell">
+					<a class="lpAdd lpAddItem" @click="newItem"><i class="lpSprite lpSpriteAdd" />Add new item</a>
+				</span>
+				<span v-if="library.optionalFields['price']" class="lpPriceCell lpNumber lpSubtotal">
+					{{ category.subtotalPrice | displayPrice(library.currencySymbol) }}
+				</span>
+				<span class="lpWeightCell lpNumber lpSubtotal">
+					<span class="lpDisplaySubtotal">{{ category.subtotalWeight | displayWeight(library.totalUnit) }}</span>
+					<span class="lpSubtotalUnit">{{ library.totalUnit }}</span>
+				</span>
+				<span class="lpQtyCell lpSubtotal">
+					<span class="lpQtySubtotal">{{ category.subtotalQty }}</span>
+				</span>
+				<span class="lpRemoveCell" />
+			</li>
+		</ul>
+	</li>
+</template>
+
+<script>
+import mixinUtils from '../utils/mixin.js';
+import item       from './item.vue';
+
+export default {
+	name: 'Category',
+	components: {
+		item,
+	},
+	mixins: [mixinUtils],
+	props: ['category'],
+	computed: {
+		library() {
+			return this.$store.state.library;
+		},
+		itemContainers() {
+			return this.category.categoryItems.map(categoryItem => ({ categoryItem, item: this.library.getItemById(categoryItem.itemId) }));
+		},
+	},
+	methods: {
+		newItem() {
+			this.$store.commit('newItem', { category: this.category, _isNew: true });
+		},
+		updateCategoryName(evt) {
+			this.$store.commit('updateCategoryName', { id: this.category.id, name: evt.target.value });
+		},
+		removeCategory(category) {
+			const callback = function () {
+				this.$store.commit('removeCategory', category);
+			};
+			const speedbumpOptions = {
+				body: 'Are you sure you want to delete this category? This cannot be undone.',
+			};
+			bus.$emit('initSpeedbump', callback, speedbumpOptions);
+		},
+	},
+};
+</script>
diff --git a/src/components/changePassword.vue b/src/components/changePassword.vue
@@ -0,0 +1,113 @@
+<style lang="scss">
+</style>
+
+<template>
+	<modal id="changePassword" :shown="shown" @hide="shown = false">
+		<h2>Change password</h2>
+
+		<form id="accountForm" @submit.prevent="updateAccount()">
+			<div class="lpFields">
+				<input v-model="currentPassword" type="password" placeholder="Current Password" name="currentPassword" class="currentPassword">
+				<hr>
+				<input v-model="newPassword" type="password" placeholder="New Password" name="newPassword" class="newPassword">
+				<input v-model="confirmNewPassword" type="password" placeholder="Confirm New Password" name="confirmNewPassword" class="confirmNewPassword">
+			</div>
+
+			<errors :errors="errors" />
+
+			<div class="lpButtons">
+				<button class="lpButton">
+					Change
+					<spinner v-if="saving" />
+				</button>
+			</div>
+		</form>
+	</modal>
+</template>
+
+<script>
+import errors  from './errors.vue';
+import modal   from './modal.vue';
+import spinner from './spinner.vue';
+
+export default {
+	name: 'changePassword',
+	components: {
+		errors,
+		modal,
+		spinner,
+	},
+	data() {
+		return {
+			saving: false,
+			errors: [],
+			currentPassword: '',
+			newPassword: '',
+			confirmNewPassword: '',
+			shown: false,
+		};
+	},
+	computed: {
+		library() {
+			return this.$store.state.library;
+		},
+	},
+	beforeMount() {
+		bus.$on('showChangePassword', () => {
+			this.shown = true;
+		});
+	},
+	methods: {
+		updateAccount() {
+			this.errors = [];
+
+			if (!this.currentPassword) {
+				this.errors.push({ field: 'currentPassword', message: 'Please enter your current password.' });
+			}
+
+			if (this.newPassword && this.newPassword != this.confirmNewPassword) {
+				this.errors.push({ field: 'newPassword', message: "Your passwords don't match." });
+			}
+
+			if (this.newPassword && (this.newPassword.length < 5 || this.newPassword.length > 60)) {
+				this.errors.push({ field: 'newPassword', message: 'Please enter a password between 5 and 60 characters.' });
+			}
+
+			if (this.errors.length) {
+				return;
+			}
+
+			const data = { currentPassword: this.currentPassword };
+
+			let dirty = false;
+
+			if (this.newPassword) {
+				dirty = true;
+				data.newPassword = this.newPassword;
+			}
+
+			if (!dirty) return;
+
+			this.currentPassword = '';
+			this.saving = true;
+
+			fetchJson('/changePassword', {
+				method: 'POST',
+				headers: {
+					'Content-Type': 'application/json',
+				},
+				credentials: 'same-origin',
+				body: JSON.stringify(data),
+			})
+				.then((response) => {
+					this.saving = false;
+					this.shown = false;
+				})
+				.catch((err) => {
+					this.errors = err;
+					this.saving = false;
+				});
+		},
+	},
+};
+</script>
diff --git a/src/components/colorpicker.vue b/src/components/colorpicker.vue
@@ -0,0 +1,36 @@
+<style lang="scss">
+</style>
+
+<template>
+	<Popover id="lpPickerContainer" :shown="shown" @hide="shown = false">
+		<span slot="target" class="lpLegend" :style="{'background-color': color}" @click="shown = true" />
+		<VueColorPicker slot="content" :width="150" :height="150" :disabled="false" :start-color="color" @color-change="onColorChange" />
+	</Popover>
+</template>
+
+<script>
+import VueColorPicker from 'vue-color-picker-wheel';
+import Popover        from './popover.vue';
+
+export default {
+	name: 'ColorPicker',
+	components: {
+		VueColorPicker,
+		Popover,
+	},
+	props: [
+		'color',
+	],
+	data() {
+		return {
+			shown: false,
+		};
+	},
+	methods: {
+		onColorChange(newColor) {
+			this.$emit('colorChange', newColor);
+		},
+	},
+};
+
+</script>
diff --git a/src/components/copyList.vue b/src/components/copyList.vue
@@ -0,0 +1,56 @@
+<style lang="scss">
+@import "../css/_globals";
+</style>
+
+<template>
+	<modal id="copyListDialog" :shown="shown" @hide="shown = false">
+		<h2>Choose the list to copy</h2>
+		<select id="listToCopy" v-model="listId">
+			<option v-for="list in library.lists" :value="list.id">
+				{{ list.name }}
+			</option>
+		</select>
+		<br><br>
+		<p class="lpWarning">
+			<b>Note:</b> Copying a list will link the items between your lists. Updating an item in one list will alter that item in all other lists that item is in.
+		</p>
+		<a id="copyConfirm" class="lpButton" @click="copyList">Copy List</a>
+		<a class="lpButton close" @click="shown = false">Cancel</a>
+	</modal>
+</template>
+
+<script>
+import modal from './modal.vue';
+
+export default {
+	name: 'CopyList',
+	components: {
+		modal,
+	},
+	data() {
+		return {
+			listId: false,
+			shown: false,
+		};
+	},
+	computed: {
+		library() {
+			return this.$store.state.library;
+		},
+	},
+	beforeMount() {
+		bus.$on('copyList', () => {
+			this.shown = true;
+		});
+	},
+	methods: {
+		copyList() {
+			if (!this.listId) {
+				return; // TODO: errors
+			}
+			this.$store.commit('copyList', this.listId);
+			this.shown = false;
+		},
+	},
+};
+</script>
diff --git a/src/components/errors.vue b/src/components/errors.vue
@@ -0,0 +1,62 @@
+<style lang="scss">
+</style>
+
+<template>
+	<ul v-if="sanitizedErrors && sanitizedErrors.length" class="lpError">
+		<li v-for="error in sanitizedErrors">
+			{{ error.message }}
+		</li>
+	</ul>
+</template>
+
+<script>
+
+export default {
+	name: 'Errors',
+	props: ['errors'],
+	computed: {
+		sanitizedErrors() {
+			let errors = this.errors;
+			if (!errors) {
+				return [];
+			}
+
+			if (typeof errors === 'string') {
+				return [{ message: errors }];
+			}
+
+			if (typeof errors === 'object' && !(errors instanceof Array) && errors.message) {
+				return [errors];
+			}
+
+			if (typeof errors === 'object' && errors.errors && errors.errors instanceof Array) {
+				errors = errors.errors;
+			}
+
+			if (typeof errors === 'object' && errors instanceof Array) {
+				if (errors.length === 0) {
+					return errors;
+				}
+
+				const massagedErrors = errors.map((error) => {
+					if (typeof error === 'string') {
+						return { message: error };
+					}
+
+					if (typeof error === 'object' && error.message) {
+						return error;
+					}
+					return false;
+				})
+					.filter(error => !!error.message);
+
+				if (massagedErrors.length) {
+					return massagedErrors;
+				}
+			}
+
+			return [{ message: 'An unknown error occurred.' }];
+		},
+	},
+};
+</script>
diff --git a/src/components/globalAlerts.vue b/src/components/globalAlerts.vue
@@ -0,0 +1,50 @@
+<style lang="scss">
+@import "../css/_globals";
+
+.lpGlobalAlerts {
+	background: $yellow1;
+	border: 1px solid $darkYellow;
+	border-radius: 0 0 10px 10px;
+	border-top: none;
+	left: 50%;
+	margin: 0;
+	padding: 0;
+	position: fixed;
+	text-align: center;
+	top: 0;
+	transform: translateX(-50%);
+	width: 50%;
+	z-index: $aboveDialog;
+}
+
+.lpGlobalAlert {
+	border-bottom: 1px solid $darkYellow;
+	list-style-type: none;
+	margin: 0;
+	padding: $spacingMedium;
+
+	&:last-child {
+		border-bottom: none;
+	}
+}
+</style>
+
+<template>
+	<ul v-if="alerts && alerts.length" class="lpGlobalAlerts">
+		<li v-for="alert in alerts" class="lpGlobalAlert">
+			{{ alert.message }}
+		</li>
+	</ul>
+</template>
+
+<script>
+
+export default {
+	name: 'GlobalAlerts',
+	computed: {
+		alerts() {
+			return this.$store.state.globalAlerts;
+		},
+	},
+};
+</script>
diff --git a/src/components/help.vue b/src/components/help.vue
@@ -0,0 +1,47 @@
+<style lang="scss">
+
+#help {
+	width: 800px;
+}
+
+</style>
+
+<template>
+	<modal id="help" :shown="shown" @hide="shown = false">
+		<h2>Help</h2>
+
+		<p>Getting Started:</p>
+		<ol>
+			<li>Click on things to edit them. Give your list and category a name.</li>
+			<li>Add new categories and items to your list.</li>
+			<li>When you're done, share your list with others!</li>
+		</ol>
+		<hr>
+		<strong>Quantity and worn values</strong>
+		<p>If you have multiple quantity of an item and mark that item as worn, only the first quantity will count towards your worn weight. The rest will count towards your pack weight. This is because most items you have multiple of, you only wear one at once. This means you can't list your shoes/trekking poles with weights as individual weights and quantity of two - you should list as the combined weight with quantity of one.</p>
+		<hr>
+		<strong>Items in multiple lists</strong>
+		<p>If you copy your list or drag an item from the gear library into a second list, those items are now <strong>linked</strong>. This means that changes to an item in one place will update that list everywhere. If you want to copy your list without links, for now you can export to CSV and re-import the list.</p>
+	</modal>
+</template>
+
+<script>
+import modal from './modal.vue';
+
+export default {
+	name: 'Help',
+	components: {
+		modal,
+	},
+	data() {
+		return {
+			shown: false,
+		};
+	},
+	beforeMount() {
+		bus.$on('showHelp', () => {
+			this.shown = true;
+		});
+	},
+};
+</script>
diff --git a/src/components/importCsv.vue b/src/components/importCsv.vue
@@ -0,0 +1,172 @@
+<style lang="scss">
+
+#importValidate {
+	height: 500px;
+	overflow-y: scroll;
+	width: 650px;
+
+	.lpButton {
+		margin-bottom: 30px;
+	}
+}
+
+</style>
+
+<template>
+	<div id="importCSV">
+		<modal id="importValidate" :shown="shown" @hide="shown = false">
+			<h2>Confirm your import</h2>
+			<div id="importData">
+				<ul class="lpTable lpDataTable">
+					<li class="lpRow lpHeader">
+						<span class="lpCell">Item Name</span>
+						<span class="lpCell">Category</span>
+						<span class="lpCell">Description</span>
+						<span class="lpCell">Qty</span>
+						<span class="lpCell">Weight</span>
+						<span class="lpCell">Unit</span>
+					</li>
+					<li v-for="row in importData.data" class="lpRow">
+						<span class="lpCell">{{ row.name }}</span>
+						<span class="lpCell">{{ row.category }}</span>
+						<span class="lpCell">{{ row.description }}</span>
+						<span class="lpCell">{{ row.qty }}</span>
+						<span class="lpCell">{{ row.weight }}</span>
+						<span class="lpCell">{{ row.unit }}</span>
+					</li>
+				</ul>
+			</div>
+			<a id="importConfirm" class="lpButton" @click="importList">Import List</a>
+			<a class="lpButton close" @click="shown = false">Cancel Import</a>
+		</modal>
+		<form id="csvUpload">
+			<input id="csv" type="file" name="csv">
+		</form>
+	</div>
+</template>
+
+<script>
+import modal from './modal.vue';
+
+export default {
+	name: 'ImportCsv',
+	components: {
+		modal,
+	},
+	data() {
+		return {
+			csvInput: false,
+			listId: false,
+			importData: {},
+			fullUnitToUnit: {
+				gram: 'g', grams: 'g', g: 'g', kilogram: 'kg', kilograms: 'kg', kg: 'kg', kgs: 'kg',
+			},
+			shown: false,
+		};
+	},
+	computed: {
+		library() {
+			return this.$store.state.library;
+		},
+	},
+	mounted() {
+		this.csvInput = document.getElementById('csv');
+		this.csvInput.onchange = this.importCSV;
+
+		bus.$on('importCSV', () => {
+			this.csvInput.click();
+		});
+	},
+	methods: {
+		importCSV(evt) {
+			const file = evt.target.files[0];
+			const name = file.name;
+			const size = file.size;
+			const type = file.type;
+
+			if (file.name.length < 1) {
+				return;
+			}
+			if (file.size > 1000000) {
+				alert('File is too big');
+				return;
+			}
+			if (name.substring(name.length - 4).toLowerCase() != '.csv') {
+				alert('Please select a CSV.');
+				return;
+			}
+			const reader = new FileReader();
+
+			reader.onload = ((theFile) => {
+				this.validateImport(theFile.target.result, file.name.substring(0, file.name.length - 4).replace(/\_/g, ' '));
+			});
+
+			reader.readAsText(file);
+		},
+		CSVToArray(strData) {
+			const strDelimiter = ',';
+			const arrData = [[]];
+			let arrMatches = null;
+
+
+			const objPattern = new RegExp(
+				(
+					`(\\${strDelimiter}|\\r?\\n|\\r|^)`
+					+ '(?:"([^"]*(?:""[^"]*)*)"|'
+					+ `([^"\\${strDelimiter}\\r\\n]*))`
+				), 'gi',
+			);
+
+			while (arrMatches = objPattern.exec(strData)) {
+				const strMatchedDelimiter = arrMatches[1];
+				if (strMatchedDelimiter.length && (strMatchedDelimiter != strDelimiter)) {
+					arrData.push([]);
+				}
+
+				if (arrMatches[2]) {
+					var strMatchedValue = arrMatches[2].replace(new RegExp('""', 'g'), '"');
+				} else {
+					var strMatchedValue = arrMatches[3];
+				}
+
+				arrData[arrData.length - 1].push(strMatchedValue);
+			}
+
+			return arrData;
+		},
+		validateImport(input, name) {
+			const csv = this.CSVToArray(input);
+			this.importData = { data: [], name };
+
+			for (const i in csv) {
+				const row = csv[i];
+				if (row.length < 6) continue;
+				if (row[0].toLowerCase() == 'item name') continue;
+				if (isNaN(parseInt(row[3]))) continue;
+				if (isNaN(parseInt(row[4]))) continue;
+				if (typeof this.fullUnitToUnit[row[5]] === 'undefined') continue;
+
+				this.importData.data.push({
+					name: row[0],
+					category: row[1],
+					description: row[2],
+					qty: parseFloat(row[3]),
+					weight: parseFloat(row[4]),
+					unit: this.fullUnitToUnit[row[5]],
+				});
+			}
+
+			if (!this.importData.data.length) {
+				alert('Unable to load spreadsheet - please verify the format.');
+			} else {
+				this.shown = true;
+			}
+		},
+		importList() {
+			this.$store.commit('importCSV', this.importData);
+			this.shown = false;
+		},
+
+	},
+};
+</script>
diff --git a/src/components/item.vue b/src/components/item.vue
@@ -0,0 +1,332 @@
+<style lang="scss">
+
+.lpItem {
+	&:hover,
+	&.ui-sortable-helper {
+		background: #fff;
+
+		.lpRemove,
+		.lpWorn,
+		.lpConsumable,
+		.lpCamera,
+		.lpLink,
+		.handle,
+		.lpArrows,
+		.lpStar {
+			visibility: visible;
+		}
+	}
+
+	input,
+	select {
+		padding: 3px;
+	}
+}
+
+.lpArrows {
+	display: inline-block;
+	height: 14px;
+	position: relative;
+	visibility: hidden;
+	width: 10px;
+
+	.lpUp,
+	.lpDown {
+		cursor: pointer;
+		left: 0;
+		margin: 2px;
+		opacity: 0.5;
+		position: absolute;
+		top: 0;
+
+		&:hover {
+			opacity: 1;
+		}
+	}
+
+	.lpDown {
+		top: 11px;
+	}
+}
+
+</style>
+
+<template>
+	<li :id="item.id" :class="'lpItem '+ item.classes">
+		<span class="handleCell">
+			<div class="lpItemHandle handle" title="Reorder this item" />
+		</span>
+		<span v-if="library.optionalFields['images']" class="lpImageCell">
+			<img v-if="thumbnailImage" class="lpItemImage" :src="thumbnailImage" @click="viewItemImage()">
+		</span>
+		<input v-model="item.name" v-focus-on-create="categoryItem._isNew" type="text" class="lpName lpSilent" placeholder="Name" @input="saveItem">
+		<input v-model="item.description" type="text" class="lpDescription lpSilent" placeholder="Description" @input="saveItem">
+		<span class="lpActionsCell">
+			<i class="lpSprite lpCamera" title="Upload a photo or use a photo from the web" @click="updateItemImage" />
+			<i class="lpSprite lpLink" :class="{lpActive: item.url}" title="Add a link for this item" @click="updateItemLink" />
+			<i v-if="library.optionalFields['worn']" class="lpSprite lpWorn" :class="{lpActive: categoryItem.worn}" title="Mark this item as worn" @click="toggleWorn" />
+			<i v-if="library.optionalFields['consumable']" class="lpSprite lpConsumable" :class="{lpActive: categoryItem.consumable}" title="Mark this item as a consumable" @click="toggleConsumable" />
+			<i :class="'lpSprite lpStar lpStar' + categoryItem.star" title="Star this item" @click="cycleStar" />
+		</span>
+		<span v-if="library.optionalFields['price']" class="lpPriceCell">
+			<input v-model="displayPrice" v-empty-if-zero type="text" :class="{lpPrice: true, lpNumber: true, lpSilent: true, lpSilentError: priceError}" @input="savePrice" @keydown.up="incrementPrice($event)" @keydown.down="decrementPrice($event)" @blur="setDisplayPrice">
+		</span>
+		<span class="lpWeightCell lpNumber">
+			<input v-model="displayWeight" v-empty-if-zero type="text" :class="{lpWeight: true, lpNumber: true, lpSilent: true, lpSilentError: weightError}" @input="saveWeight" @keydown.up="incrementWeight($event)" @keydown.down="decrementWeight($event)">
+			<unitSelect :unit="item.authorUnit" :on-change="setUnit" />
+		</span>
+		<span class="lpQtyCell">
+			<input v-model="displayQty" type="text" :class="{lpQty: true, lpNumber: true, lpSilent: true, lpSilentError: qtyError}" @input="saveQty" @keydown.up="incrementQty($event)" @keydown.down="decrementQty($event)">
+			<span class="lpArrows">
+				<span class="lpSprite lpUp" @click="incrementQty($event)" />
+				<span class="lpSprite lpDown" @click="decrementQty($event)" />
+			</span>
+		</span>
+		<span class="lpRemoveCell">
+			<a class="lpRemove lpRemoveItem" title="Remove this item" @click="removeItem"><i class="lpSprite lpSpriteRemove" /></a>
+		</span>
+	</li>
+</template>
+
+<script>
+import unitSelect  from './unitSelect.vue';
+
+import mixinUtils  from '../utils/mixin.js';
+import weightUtils from '../utils/weight.js';
+
+export default {
+	name: 'Item',
+	components: {
+		unitSelect,
+	},
+	mixins: [mixinUtils],
+	props: ['category', 'itemContainer'],
+	data() {
+		return {
+			displayWeight: 0,
+			displayPrice: 0,
+			displayQty: 0,
+			weightError: false,
+			priceError: false,
+			qtyError: false,
+			numStars: 4,
+		};
+	},
+	computed: {
+		library() {
+			return this.$store.state.library;
+		},
+		item() {
+			return Vue.util.extend({}, this.itemContainer.item);
+		},
+		categoryItem() {
+			return Vue.util.extend({}, this.itemContainer.categoryItem);
+		},
+		thumbnailImage() {
+			if (this.item.image) {
+				return `https://i.imgur.com/${this.item.image}s.jpg`;
+			} if (this.item.imageUrl) {
+				return this.item.imageUrl;
+			}
+			return '';
+		},
+		fullImage() {
+			if (this.item.image) {
+				return `https://i.imgur.com/${this.item.image}l.jpg`;
+			} if (this.item.imageUrl) {
+				return this.item.imageUrl;
+			}
+			return '';
+		},
+	},
+	watch: {
+		item() {
+			this.setDisplayWeight();
+		},
+		categoryItem() {
+			this.setDisplayQty();
+		},
+	},
+	beforeMount() {
+		this.setDisplayWeight();
+		this.setDisplayPrice();
+		this.setDisplayQty();
+	},
+	methods: {
+		saveItem() {
+			this.$store.commit('updateItem', this.item);
+		},
+		saveCategoryItem() {
+			this.$store.commit('updateCategoryItem', { category: this.category, categoryItem: this.categoryItem });
+		},
+		setUnit(unit) {
+			this.item.authorUnit = unit;
+			this.$store.commit('updateItemUnit', unit);
+			this.setDisplayWeight()
+		},
+		savePrice() {
+			const priceFloat = parseFloat(this.displayPrice, 10);
+
+			if (!isNaN(priceFloat)) {
+				this.item.price = Math.round(priceFloat * 100) / 100;
+				this.saveItem();
+				this.priceError = false;
+			} else {
+				this.priceError = true;
+			}
+		},
+		saveQty() {
+			const qtyFloat = parseFloat(this.displayQty, 10);
+
+			if (!isNaN(qtyFloat)) {
+				this.categoryItem.qty = qtyFloat;
+				this.saveCategoryItem();
+				this.qtyError = false;
+			} else {
+				this.qtyError = true;
+			}
+		},
+		saveWeight() {
+			const weightFloat = parseFloat(this.displayWeight, 10);
+
+			if (!isNaN(weightFloat)) {
+				this.item.weight = weightUtils.WeightToMg(weightFloat, this.item.authorUnit);
+				this.saveItem();
+				this.weightError = false;
+			} else {
+				this.weightError = true;
+			}
+		},
+		setDisplayPrice() {
+			if (!this.priceError) {
+				this.displayPrice = this.item.price.toFixed(2);
+			}
+		},
+		setDisplayQty() {
+			if (!this.qtyError) {
+				this.displayQty = this.categoryItem.qty;
+			}
+		},
+		setDisplayWeight() {
+			this.displayWeight = weightUtils.MgToWeight(this.item.weight, this.item.authorUnit);
+		},
+		updateItemLink() {
+			bus.$emit('updateItemLink', this.item);
+		},
+		updateItemImage() {
+			bus.$emit('updateItemImage', this.item);
+		},
+		viewItemImage() {
+			bus.$emit('viewItemImage', this.fullImage);
+		},
+		toggleWorn() {
+			if (this.categoryItem.consumable) {
+				return;
+			}
+			this.categoryItem.worn = !this.categoryItem.worn;
+			this.saveCategoryItem();
+		},
+		toggleConsumable() {
+			if (this.categoryItem.worn) {
+				return;
+			}
+			this.categoryItem.consumable = !this.categoryItem.consumable;
+			this.saveCategoryItem();
+		},
+		cycleStar() {
+			if (!this.categoryItem.star) {
+				this.categoryItem.star = 0;
+			}
+			this.categoryItem.star = (this.categoryItem.star + 1) % this.numStars;
+			this.saveCategoryItem();
+		},
+		incrementPrice(evt) {
+			evt.stopImmediatePropagation();
+
+			if (this.priceError) {
+				return;
+			}
+
+			this.item.price = this.item.price + 1;
+
+			this.saveItem();
+			this.setDisplayPrice();
+		},
+		decrementPrice(evt) {
+			evt.stopImmediatePropagation();
+
+			if (this.priceError) {
+				return;
+			}
+
+			this.item.price = this.item.price - 1;
+
+			if (this.item.price < 0) {
+				this.item.price = 0;
+			}
+
+			this.saveItem();
+			this.setDisplayPrice();
+		},
+		incrementQty(evt) {
+			evt.stopImmediatePropagation();
+
+			if (this.qtyError) {
+				return;
+			}
+
+			this.categoryItem.qty = this.categoryItem.qty + 1;
+			this.saveCategoryItem();
+		},
+		decrementQty(evt) {
+			evt.stopImmediatePropagation();
+
+			if (this.qtyError) {
+				return;
+			}
+
+			this.categoryItem.qty = this.categoryItem.qty - 1;
+
+			if (this.categoryItem.qty < 0) {
+				this.categoryItem.qty = 0;
+			}
+
+			this.saveCategoryItem();
+		},
+		incrementWeight(evt) {
+			evt.stopImmediatePropagation();
+
+			if (this.weightError) {
+				return;
+			}
+
+			const newWeight = weightUtils.MgToWeight(this.item.weight, this.item.authorUnit) + 1;
+			this.item.weight = weightUtils.WeightToMg(newWeight, this.item.authorUnit);
+
+			this.saveItem();
+		},
+		decrementWeight(evt) {
+			evt.stopImmediatePropagation();
+
+			if (this.weightError) {
+				return;
+			}
+
+			const newWeight = weightUtils.MgToWeight(this.item.weight, this.item.authorUnit) - 1;
+			this.item.weight = weightUtils.WeightToMg(newWeight, this.item.authorUnit);
+
+			if (this.item.weight < 0) {
+				this.item.weight = 0;
+			}
+
+			this.saveItem();
+		},
+		removeItem() {
+			this.$store.commit('removeItemFromCategory', { itemId: this.item.id, category: this.category });
+
+			// if the removed item has a blank name remove it from the gear list
+			if (!this.item.name || !this.item.name.trim().length) this.$store.commit('removeItem', { item: this.item });
+		},
+	},
+};
+</script>,
diff --git a/src/components/itemImage.vue b/src/components/itemImage.vue
@@ -0,0 +1,50 @@
+<style lang="scss">
+</style>
+
+<template>
+	<modal id="itemImageDialog" :shown="shown" @hide="shown = false">
+		<h2>Add image</h2>
+		<form id="itemImageUrlForm" @submit.prevent="saveImageUrl()">
+			<div class="lpFields">
+				<input id="itemImageUrl" v-model="imageUrl" type="text" placeholder="Image URL">
+				<div class="lpButtons">
+					<input type="submit" class="lpButton" value="Save">
+				</div>
+			</div>
+		</form>
+	</modal>
+</template>
+
+<script>
+import modal from './modal.vue';
+
+export default {
+	name: 'ItemImage',
+	components: {
+		modal,
+	},
+	data() {
+		return {
+			shown:    false,
+			item:     false,
+			imageUrl: null,
+		};
+	},
+	mounted() {
+		bus.$on('updateItemImage', (item) => {
+			this.shown    = true;
+			this.item     = item;
+			this.imageUrl = item.imageUrl;
+		});
+	},
+	methods: {
+		saveImageUrl() {
+			this.$store.commit('updateItemImageUrl', {
+				item:     this.item,
+				imageUrl: this.imageUrl,
+			});
+			this.shown = false;
+		},
+	},
+};
+</script>
diff --git a/src/components/itemLink.vue b/src/components/itemLink.vue
@@ -0,0 +1,53 @@
+<style lang="scss">
+</style>
+
+<template>
+	<modal id="itemLinkDialog" :shown="shown" @hide="shown = false">
+		<h2>Add URL</h2>
+		<form id="itemLinkForm" @submit.prevent="addLink">
+			<div class="lpFields">
+				<input v-model="url" v-select-on-bus="'focus-url-field'" type="text" d="itemLink" placeholder="Item Link">
+				<div class="lpButtons">
+					<input type="submit" class="lpButton" value="Save">
+				</div>
+			</div>
+		</form>
+	</modal>
+</template>
+
+<script>
+import modal from './modal.vue';
+
+export default {
+	name: 'ItemLink',
+	components: {
+		modal,
+	},
+	data() {
+		return {
+			shown: false,
+			item:  false,
+			url:   '',
+		};
+	},
+	beforeMount() {
+		bus.$on('updateItemLink', (item) => {
+			this.shown = true;
+			this.item  = item;
+			this.url   = item.url;
+		});
+	},
+	mounted() {
+		bus.$emit('focus-url-field');
+	},
+	methods: {
+		addLink() {
+			this.$store.commit('updateItemLink', {
+				item: this.item,
+				url:  this.url,
+			});
+			this.shown = false;
+		},
+	},
+};
+</script>
diff --git a/src/components/itemViewImage.vue b/src/components/itemViewImage.vue
@@ -0,0 +1,40 @@
+<style lang="scss">
+
+#itemImageDialog {
+	width: 640px;
+
+	.imageUploadDescription {
+		margin-bottom: 19px;
+	}
+}
+
+</style>
+
+<template>
+	<modal id="lpImageDialog" :shown="shown" @hide="shown = false">
+		<img :src="imageUrl">
+	</modal>
+</template>
+
+<script>
+import modal from './modal.vue';
+
+export default {
+	name: 'ItemViewImage',
+	components: {
+		modal,
+	},
+	data() {
+		return {
+			imageUrl: '',
+			shown: false,
+		};
+	},
+	mounted() {
+		bus.$on('viewItemImage', (imageUrl) => {
+			this.shown = true;
+			this.imageUrl = imageUrl;
+		});
+	},
+};
+</script>
diff --git a/src/components/libraryItems.vue b/src/components/libraryItems.vue
@@ -0,0 +1,259 @@
+<style lang="scss">
+
+#libraryContainer {
+	display: flex;
+	flex: 2 0 30vh;
+	flex-direction: column;
+}
+
+#library {
+	flex: 1 0 25vh;
+	overflow-y: scroll;
+}
+
+#librarySearch {
+	position: relative;
+	display: table;
+	margin-bottom: 15px;
+	padding: 0px;
+}
+
+#librarySearch input {
+    display: table-cell;
+	background: #666;
+	border: 1px solid #888;
+	color: #fff;
+	width: 100%;
+	padding: 3px 6px;
+}
+
+#librarySearch p {
+    position: absolute;
+    right: 0px;
+    top: 3px;
+    padding: 0 8px;
+    margin: 0px;
+    font-size: 1.2em;
+    user-select: none;
+    cursor: pointer;
+}
+.lpLibraryItem {
+	border-top: 1px dotted #999;
+	list-style: none;
+	margin: 0 10px 5px;
+	min-height: 43px;
+	overflow: hidden;
+	padding: 5px 5px 0 15px;
+	position: relative;
+
+	&:first-child {
+		border-top: none;
+		padding-top: 10px;
+	}
+
+	&:last-child {
+		border-bottom: none;
+	}
+
+	&.gu-mirror {
+		background: #606060;
+		border: 1px solid #999;
+		color: #fff;
+	}
+
+	.lpName {
+		float: left;
+		margin: 0;
+		max-width: 190px;
+		overflow: hidden;
+		text-overflow: ellipsis;
+	}
+
+	.lpWeight {
+		float: right;
+		width: auto;
+	}
+
+	.lpDescription {
+		clear: both;
+		color: #ccc;
+		display: block;
+		overflow: hidden;
+		text-overflow: ellipsis;
+		width: 235px;
+	}
+
+	.handle {
+		height: 80px;
+		left: 0;
+		position: absolute;
+		top: 5px;
+	}
+
+	.lpRemove {
+		bottom: 0;
+		position: absolute;
+		right: 14px;
+	}
+
+	#library.lpSearching & {
+		display: none;
+	}
+
+	#library.lpSearching &.lpHit {
+		display: block;
+	}
+
+	#main > & {
+		background: #666;
+		color: #fff;
+		padding: 10px;
+		width: 235px;
+	}
+}
+</style>
+
+<template>
+	<section id="libraryContainer">
+		<h2>Gear</h2>
+
+		<div id="librarySearch">
+			<input v-model="searchText" type="text" placeholder="search items"/>
+			<p v-if="searchText.length > 0" @click="clearSearch">&times;</p>
+		</div>
+		<ul id="library">
+			<li v-for="item in filteredItems" class="lpLibraryItem" :data-item-id="item.id" :key="item.id">
+				<a v-if="item.url" :href="item.url" target="_blank" class="lpName lpHref">{{ item.name }}</a>
+				<span v-if="!item.url" class="lpName">{{ item.name }}</span>
+				<span class="lpWeight">
+					{{ item.weight | displayWeight(item.authorUnit) }}
+					{{ item.authorUnit }}
+				</span>
+				<span class="lpDescription">
+					{{ item.description }}
+				</span>
+				<a class="lpRemove lpRemoveLibraryItem speedbump" title="Delete this item permanently" @click="removeItem(item)"><i class="lpSprite lpSpriteRemove" /></a>
+				<div v-if="!item.inCurrentList" class="handle lpLibraryItemHandle" title="Reorder this item" />
+			</li>
+		</ul>
+	</section>
+</template>
+
+<script>
+import dragula    from 'dragula';
+
+import mixinUtils from '../utils/mixin.js';
+
+export default {
+	name: 'LibraryItem',
+	mixins: [mixinUtils],
+	props: ['item'],
+	data() {
+		return {
+			searchText: '',
+			itemDragId: false,
+			drake: null,
+		};
+	},
+	computed: {
+		library() {
+			return this.$store.state.library;
+		},
+		filteredItems() {
+			let i;
+			let item;
+			let filteredItems = [];
+			if (!this.searchText) {
+				filteredItems = this.library.items.map(item => Vue.util.extend({}, item));
+			} else {
+				const lowerCaseSearchText = this.searchText.toLowerCase();
+
+				for (i = 0; i < this.library.items.length; i++) {
+					item = this.library.items[i];
+					if (item.name.toLowerCase().indexOf(lowerCaseSearchText) > -1 || item.description.toLowerCase().indexOf(lowerCaseSearchText) > -1) {
+						filteredItems.push(Vue.util.extend({}, item));
+					}
+				}
+			}
+
+			const currentListItems = this.library.getItemsInCurrentList();
+
+			for (i = 0; i < filteredItems.length; i++) {
+				item = filteredItems[i];
+				if (currentListItems.indexOf(item.id) > -1) {
+					item.inCurrentList = true;
+				}
+			}
+
+			return filteredItems;
+		},
+		list() {
+			return this.library.getListById(this.library.defaultListId);
+		},
+		categories() {
+			return this.list.categoryIds.map(id => this.library.getCategoryById(id));
+		},
+	},
+	watch: {
+		categories() {
+			Vue.nextTick(() => {
+				this.handleItemDrag();
+			});
+		},
+	},
+	mounted() {
+		this.handleItemDrag();
+	},
+	methods: {
+		handleItemDrag() {
+			if (this.drake) {
+				this.drake.destroy();
+			}
+
+			const self = this;
+			const $library = document.getElementById('library');
+			const $categoryItems = Array.prototype.slice.call(document.getElementsByClassName('lpItems')); // list.vue
+			const drake = dragula([$library].concat($categoryItems), {
+				copy: true,
+				moves($el, $source, $handle, $sibling) {
+					const items = self.library.getItemsInCurrentList();
+					if (items.indexOf(parseInt($el.dataset.itemId)) > -1) {
+						return false;
+					}
+					return $handle.classList.contains('lpLibraryItemHandle');
+				},
+				accepts($el, $target, $source, $sibling) {
+					if ($target.id === 'library' || !$sibling || $sibling.classList.contains('lpItemsHeader')) {
+						return false; // header and footer are technically part of this list - exclude them both.
+					}
+					return true;
+				},
+			});
+			drake.on('drag', ($el, $target, $source, $sibling) => {
+				this.itemDragId = parseInt($el.dataset.itemId); // fragile
+			});
+			drake.on('drop', ($el, $target, $source, $sibling) => {
+				if (!$target || $target.id === 'library') {
+					return;
+				}
+				const categoryId = parseInt($target.parentElement.id); // fragile
+				this.$store.commit('addItemToCategory', { itemId: this.itemDragId, categoryId, dropIndex: getElementIndex($el) - 1 });
+				drake.cancel(true);
+			});
+			this.drake = drake;
+		},
+		removeItem(item) {
+			const callback = function () {
+				this.$store.commit('removeItem', item);
+			};
+			const speedbumpOptions = {
+				body: 'Are you sure you want to delete this item? This cannot be undone.',
+			};
+			bus.$emit('initSpeedbump', callback, speedbumpOptions);
+		},
+		clearSearch() {
+			this.searchText = "";
+		}
+	},
+};
+</script>
diff --git a/src/components/libraryLists.vue b/src/components/libraryLists.vue
@@ -0,0 +1,178 @@
+<style lang="scss">
+@import "../css/_globals";
+
+#listContainer {
+	flex: 0 0 auto;
+
+	#lists {
+		max-height: 25vh;
+	}
+}
+
+.lpLibraryList {
+	border-top: 1px dotted #999;
+	display: flex;
+	list-style: none;
+	margin: 0 10px;
+	overflow-y: auto;
+	padding: 6px 0;
+	position: relative;
+
+	&:first-child {
+		border-top: none;
+		padding-top: 10px;
+	}
+
+	&:last-child {
+		border-bottom: none;
+	}
+
+	&.lpActive {
+		color: $yellow1;
+		font-weight: bold;
+
+		.lpRemove {
+			display: none;
+		}
+	}
+
+	&.gu-mirror {
+		background: #606060;
+		border: 1px solid #999;
+		color: #fff;
+	}
+
+	.handle {
+		flex: 0 0 12px;
+		height: 18px;
+		margin-right: 5px;
+	}
+
+	&:hover .handle {
+		visibility: visible;
+	}
+
+	.lpListName {
+		flex: 1 1 auto;
+		overflow: hidden;
+		text-overflow: ellipsis;
+		white-space: nowrap;
+
+		&:hover {
+			cursor: pointer;
+			text-decoration: underline;
+		}
+	}
+
+	.lpRemove {
+		flex: 0 0 8px;
+		margin-bottom: 0;
+	}
+}
+
+.listContainerHeader {
+	display: flex;
+	justify-content: space-between;
+}
+
+#addListFlyout {
+	.lpContent a {
+		display: block;
+		margin-bottom: 5px;
+
+		&:last-child {
+			margin-bottom: 0;
+		}
+	}
+}
+</style>
+
+<template>
+	<section id="listContainer">
+		<div class="listContainerHeader">
+			<h2>Lists</h2>
+			<PopoverHover id="addListFlyout">
+				<span slot="target"><a class="lpAdd" @click="newList"><i class="lpSprite lpSpriteAdd" />Add new list</a></span>
+				<div slot="content">
+					<a class="lpAdd" @click="newList"><i class="lpSprite lpSpriteAdd" />Add new list</a>
+					<a class="lpAdd" @click="importCSV"><i class="lpSprite lpSpriteUpload" />Import CSV</a>
+					<a class="lpCopy" @click="copyList"><i class="lpSprite lpSpriteCopy" />Copy a list</a>
+				</div>
+			</PopoverHover>
+		</div>
+		<ul id="lists">
+			<li v-for="list in library.lists" :key="list.id" class="lpLibraryList" :class="{lpActive: (library.defaultListId == list.id)}">
+				<div class="handle" title="Reorder this item" />
+				<span class="lpLibraryListSwitch lpListName" @click="setDefaultList(list)">
+					{{ list | listName }}
+				</span>
+				<a class="lpRemove" title="Remove this list" @click="removeList(list)"><i class="lpSprite lpSpriteRemove" /></a>
+			</li>
+		</ul>
+	</section>
+</template>
+
+<script>
+import dragula      from 'dragula';
+
+import PopoverHover from './popoverHover.vue';
+
+export default {
+	name: 'LibraryList',
+	components: {
+		PopoverHover,
+	},
+	filters: {
+		listName(list) {
+			return list.name || 'New list';
+		},
+	},
+	props: ['list'],
+	computed: {
+		library() {
+			return this.$store.state.library;
+		},
+	},
+	mounted() {
+		this.handleListReorder();
+	},
+	methods: {
+		setDefaultList(list) {
+			this.$store.commit('setDefaultList', list);
+		},
+		newList() {
+			this.$store.commit('newList');
+		},
+		copyList() {
+			bus.$emit('copyList');
+		},
+		importCSV() {
+			bus.$emit('importCSV');
+		},
+		handleListReorder() {
+			const $lists = document.getElementById('lists');
+			const drake = dragula([$lists], {
+				moves($el, $source, $handle, $sibling) {
+					return $handle.classList.contains('handle');
+				},
+			});
+			drake.on('drag', ($el, $target, $source, $sibling) => {
+				this.dragStartIndex = getElementIndex($el);
+			});
+			drake.on('drop', ($el, $target, $source, $sibling) => {
+				this.$store.commit('reorderList', { before: this.dragStartIndex, after: getElementIndex($el) });
+				drake.cancel(true);
+			});
+		},
+		removeList(list) {
+			const callback = function () {
+				this.$store.commit('removeList', list);
+			};
+			const speedbumpOptions = {
+				body: 'Are you sure you want to delete this list? This cannot be undone.',
+			};
+			bus.$emit('initSpeedbump', callback, speedbumpOptions);
+		},
+	},
+};
+</script>
diff --git a/src/components/list.vue b/src/components/list.vue
@@ -0,0 +1,184 @@
+<style lang="scss">
+@import "../css/_globals";
+
+#listDescriptionContainer {
+	margin: 25px 0;
+
+	h3,
+	p {
+		display: inline-block;
+		margin: 0 0 5px;
+	}
+
+	h3 {
+		margin-right: 10px;
+	}
+
+	textarea {
+		height: 65px;
+		width: 100%;
+	}
+}
+
+#getStarted {
+	background: darken($background1, 10%);
+	display: flex;
+	flex-direction: column;
+	height: 220px;
+	justify-content: center;
+	line-height: 1.6;
+	padding: $spacingLarge;
+
+	h2 {
+		font-size: 24px;
+		line-height: 1;
+	}
+
+	h2,
+	p,
+	ol {
+		margin: 0 0 $spacingMedium;
+
+		&:last-child {
+			margin-bottom: 0;
+		}
+	}
+}
+
+</style>
+
+<template>
+	<div class="lpListBody">
+		<div v-if="isListNew" id="getStarted">
+			<h2>Welcome to ctucx.things!</h2>
+			<p>Here's what you need to get started:</p>
+			<ol>
+				<li>Click on things to edit them. Give your list and category a name.</li>
+				<li>Add new categories and give items weights to start the visualization.</li>
+				<li v-if="!isLocalSaving">
+					When you're done, share your list with others!
+				</li>
+			</ol>
+		</div>
+		<list-summary v-if="!isListNew" :list="list" />
+
+
+		<div style="clear: both;" />
+
+		<div v-if="library.optionalFields['listDescription']" id="listDescriptionContainer">
+			<h3>List Description</h3> <p>(<a href="https://guides.github.com/features/mastering-markdown/" target="_blank" class="lpHref">Markdown</a> supported)</p>
+			<textarea id="listDescription" v-model="list.description" @input="updateListDescription" />
+		</div>
+
+		<ul class="lpCategories">
+			<category v-for="category in categories" :key="category.id" :category="category" />
+		</ul>
+
+		<hr>
+
+		<a class="lpAdd addCategory" @click="newCategory"><i class="lpSprite lpSpriteAdd" />Add new category</a>
+	</div>
+</template>
+
+<script>
+import dragula     from 'dragula';
+
+import category    from './category.vue';
+import listSummary from './listSummary.vue';
+
+export default {
+	name: 'List',
+	components: {
+		listSummary,
+		category,
+		categoryDragStartIndex: false,
+		itemDragId: false,
+	},
+	mixins: [],
+	data() {
+		return {
+			onboardingCompleted: false,
+			itemDrake: null,
+		};
+	},
+	computed: {
+		library() {
+			return this.$store.state.library;
+		},
+		list() {
+			return this.$store.getters.activeList;
+		},
+		categories() {
+			return this.list.categoryIds.map(id => this.library.getCategoryById(id));
+		},
+		isListNew() {
+			return this.list.totalWeight === 0;
+		},
+		isLocalSaving() {
+			return this.$store.state.saveType === 'local';
+		},
+	},
+	watch: {
+		categories() {
+			Vue.nextTick(() => {
+				this.handleItemReorder();
+			});
+		},
+	},
+	mounted() {
+		this.handleCategoryReorder();
+		this.handleItemReorder();
+	},
+	methods: {
+		newCategory() {
+			this.$store.commit('newCategory', this.list);
+		},
+		updateListDescription() {
+			this.$store.commit('updateListDescription', this.list);
+		},
+		handleItemReorder() {
+			if (this.itemDrake) {
+				this.itemDrake.destroy();
+			}
+			const $categoryItems = Array.prototype.slice.call(document.getElementsByClassName('lpItems'));
+			const drake = dragula($categoryItems, {
+				moves($el, $source, $handle, $sibling) {
+					return $handle.classList.contains('lpItemHandle');
+				},
+				accepts($el, $target, $source, $sibling) {
+					if (!$sibling || $sibling.classList.contains('lpItemsHeader')) {
+						return false; // header and footer are technically part of this list - exclude them both.
+					}
+					return true;
+				},
+			});
+			drake.on('drag', ($el, $target, $source, $sibling) => {
+				this.itemDragId = parseInt($el.id); // fragile
+			});
+			drake.on('drop', ($el, $target, $source, $sibling) => {
+				const categoryId = parseInt($target.parentElement.id); // fragile
+				this.$store.commit('reorderItem', {
+					list: this.list, itemId: this.itemDragId, categoryId, dropIndex: getElementIndex($el) - 1,
+				});
+				drake.cancel(true);
+			});
+			this.itemDrake = drake;
+		},
+		handleCategoryReorder() {
+			const $categories = document.getElementsByClassName('lpCategories')[0];
+			const drake = dragula([$categories], {
+				moves(el, $source, $handle, $sibling) {
+					return $handle.classList.contains('lpCategoryHandle');
+				},
+			});
+			drake.on('drag', ($el, $target, $source, $sibling) => {
+				this.categoryDragStartIndex = getElementIndex($el);
+			});
+			drake.on('drop', ($el, $target, $source, $sibling) => {
+				this.$store.commit('reorderCategory', { list: this.list, before: this.categoryDragStartIndex, after: getElementIndex($el) });
+				drake.cancel(true);
+			});
+		},
+	},
+};
+</script>
diff --git a/src/components/listSettings.vue b/src/components/listSettings.vue
@@ -0,0 +1,130 @@
+<style lang="scss">
+
+#csvUrl {
+	display: block;
+	margin-top: 15px;
+}
+
+#lpOptionalFields {
+	margin: 0;
+	padding: 0;
+}
+
+.lpOptionalField {
+	list-style-type: none;
+	margin: 0;
+	padding: 0;
+}
+
+#lpPriceSettings {
+	input {
+		display: inline-block;
+		margin-left: 10px;
+		width: 50px;
+	}
+}
+
+#share .lpContent {
+	width: 330px;
+}
+
+#settings .lpContent {
+	width: 200px;
+}
+</style>
+
+<template>
+	<span id="settings" class="headerItem hasPopover">
+		<PopoverHover>
+			<span slot="target"><i class="lpSprite lpSettings" /> Settings</span>
+			<div slot="content">
+				<ul id="lpOptionalFields">
+					<li v-for="optionalField in optionalFieldsLookup" :key="optionalField.name" class="lpOptionalField">
+						<label>
+							<input v-model="optionalField.value" type="checkbox" @change="toggleOptionalField($event, optionalField.name)">
+							{{ optionalField.displayName }}
+						</label>
+					</li>
+				</ul>
+				<div v-if="library.optionalFields['price']" id="lpPriceSettings">
+					<hr>
+					<label>
+						Currency:
+						<input id="currencySymbol" type="text" maxlength="4" :value="library.currencySymbol" @input="updateCurrencySymbol($event)">
+					</label>
+				</div>
+			</div>
+		</PopoverHover>
+	</span>
+</template>
+
+<script>
+import PopoverHover from './popoverHover.vue';
+
+export default {
+	name: 'ListSettings',
+	components: {
+		PopoverHover,
+	},
+	data() {
+		return {
+			optionalFieldsLookup: [{
+				name: 'images',
+				displayName: 'Item images',
+				cssClass: 'lpShowImages',
+				value: false,
+			}, {
+				name: 'price',
+				displayName: 'Item prices',
+				cssClass: 'lpShowPrices',
+				value: false,
+			}, {
+				name: 'worn',
+				displayName: 'Worn items',
+				cssClass: 'lpShowWorn',
+				value: false,
+			}, {
+				name: 'consumable',
+				displayName: 'Consumable items',
+				cssClass: 'lpShowConsumable',
+				value: false,
+			}, {
+				name: 'listDescription',
+				displayName: 'List descriptions',
+				cssClass: 'lpShowListDescription',
+				value: false,
+			}],
+		};
+	},
+	computed: {
+		library() {
+			return this.$store.state.library;
+		},
+	},
+	beforeMount() {
+		this.updateOptionalFieldValues();
+	},
+	mounted() {
+		bus.$on('optionalFieldChanged', () => {
+			this.updateOptionalFieldValues();
+		});
+	},
+	methods: {
+		toggleOptionalField(evt, optionalField) {
+			this.$store.commit('toggleOptionalField', optionalField);
+		},
+		updateCurrencySymbol(evt) {
+			this.$store.commit('updateCurrencySymbol', evt.target.value);
+		},
+		updateOptionalFieldValues() {
+			let i;
+			let fieldLookup;
+
+			for (i = 0; i < this.optionalFieldsLookup.length; i++) {
+				fieldLookup = this.optionalFieldsLookup[i];
+				fieldLookup.value = this.library.optionalFields[fieldLookup.name];
+			}
+		},
+	},
+};
+</script>
diff --git a/src/components/listSummary.vue b/src/components/listSummary.vue
@@ -0,0 +1,178 @@
+<style lang="scss">
+
+.lpLegend {
+	&:hover {
+		border-color: #666;
+		cursor: pointer;
+	}
+}
+</style>
+
+<template>
+	<div class="lpListSummary">
+		<div class="lpChartContainer">
+			<canvas class="lpChart" height="260" width="260" />
+		</div>
+		<div class="lpTotalsContainer">
+			<ul class="lpTotals lpTable lpDataTable">
+				<li class="lpRow lpHeader">
+					<span class="lpCell">&nbsp;</span>
+					<span class="lpCell">
+						Category
+					</span>
+					<span v-if="library.optionalFields['price']" class="lpCell">
+						Price
+					</span>
+					<span class="lpCell">
+						Weight
+					</span>
+				</li>
+				<li v-for="category in categories" :key="category.id" :class="{'hover': category.activeHover, 'lpTotalCategory lpRow': true}">
+					<span class="lpCell lpLegendCell">
+						<colorPicker v-if="category.displayColor" :color="colorToHex(category.displayColor)" @colorChange="updateColor(category, $event)" />
+					</span>
+					<span class="lpCell">
+						{{ category.name }}
+					</span>
+					<span v-if="library.optionalFields['price']" class="lpCell lpNumber">
+						{{ category.subtotalPrice | displayPrice(library.currencySymbol) }}
+					</span>
+					<span class="lpCell lpNumber">
+						<span class="lpDisplaySubtotal" :mg="category.subtotalWeight">{{ category.subtotalWeight | displayWeight(library.totalUnit) }}</span> <span class="lpSubtotalUnit">{{ library.totalUnit }}</span>
+					</span>
+				</li>
+				<li class="lpRow lpFooter lpTotal">
+					<span class="lpCell" />
+					<span class="lpCell lpSubtotal" :title="list.totalQty +' items'">
+						Total
+					</span>
+					<span v-if="library.optionalFields['price']" class="lpCell lpNumber lpSubtotal" :title="list.totalQty +' items'">
+						{{ list.totalPrice | displayPrice(library.currencySymbol) }}
+					</span>
+					<span class="lpCell lpNumber lpSubtotal">
+						<span class="lpTotalValue" :title="list.totalQty + ' items'">
+							{{ list.totalWeight | displayWeight(library.totalUnit) }}
+						</span>
+						<span class="lpTotalUnit"><unitSelect :unit="library.totalUnit" :on-change="setTotalUnit" /></span>
+					</span>
+				</li>
+				<li v-if="list.totalConsumableWeight" data-weight-type="consumable" class="lpRow lpFooter lpBreakdown lpConsumableWeight">
+					<span class="lpCell" />
+					<span class="lpCell lpSubtotal">
+						Consumable
+					</span>
+					<span v-if="library.optionalFields['price']" class="lpCell lpNumber lpSubtotal">
+						{{ list.totalConsumablePrice | displayPrice(library.currencySymbol) }}
+					</span>
+					<span class="lpCell lpNumber lpSubtotal">
+						<span class="lpDisplaySubtotal" :mg="list.totalConsumableWeight">{{ list.totalConsumableWeight | displayWeight(library.totalUnit) }}</span>
+						<span class="lpSubtotalUnit">{{ library.totalUnit }}</span>
+					</span>
+				</li>
+				<li v-if="list.totalWornWeight" data-weight-type="worn" class="lpRow lpFooter lpBreakdown lpWornWeight">
+					<span class="lpCell" />
+					<span class="lpCell lpSubtotal">
+						Worn
+					</span>
+					<span v-if="library.optionalFields['price']" class="lpCell lpNumber" />
+					<span class="lpCell lpNumber lpSubtotal">
+						<span class="lpDisplaySubtotal" :mg="list.totalWornWeight">{{ list.totalWornWeight | displayWeight(library.totalUnit) }}</span>
+						<span class="lpSubtotalUnit">{{ library.totalUnit }}</span>
+					</span>
+				</li>
+				<li v-if="list.totalWornWeight || list.totalConsumableWeight" data-weight-type="base" class="lpRow lpFooter lpBreakdown lpBaseWeight">
+					<span class="lpCell" />
+					<span class="lpCell lpSubtotal" :title="$options.filters.displayWeight(list.totalPackWeight, library.totalUnit) + ' ' + library.totalUnit + ' pack weight (consumable + base weight)'">
+						Base Weight
+					</span>
+					<span v-if="library.optionalFields['price']" class="lpCell lpNumber" />
+					<span class="lpCell lpNumber lpSubtotal">
+						<span class="lpDisplaySubtotal" :mg="list.totalBaseWeight" :title="$options.filters.displayWeight(list.totalPackWeight, library.totalUnit) + ' ' + library.totalUnit + ' pack weight (consumable + base weight)'">
+							{{ list.totalBaseWeight | displayWeight(library.totalUnit) }}
+						</span>
+						<span class="lpSubtotalUnit">{{ library.totalUnit }}</span>
+					</span>
+				</li>
+			</ul>
+		</div>
+	</div>
+</template>
+
+<script>
+import colorPicker from './colorpicker.vue';
+import unitSelect  from './unitSelect.vue';
+import utilsMixin  from '../utils/mixin.js';
+
+import colorUtils  from '../utils/color.js';
+import chart       from '../chart.js';
+
+export default {
+	name: 'ListSummary',
+	components: {
+		colorPicker,
+		unitSelect,
+	},
+	mixins: [utilsMixin],
+	props: ['list'],
+	data() {
+		return {
+			chart: null,
+			hoveredCategoryId: null,
+		};
+	},
+	computed: {
+		library() {
+			return this.$store.state.library;
+		},
+		categories() {
+			return this.list.categoryIds.map((id) => {
+				const category = this.library.getCategoryById(id);
+				category.activeHover = (this.hoveredCategoryId === category.id);
+				return category;
+			});
+		},
+	},
+	watch: {
+		'$store.state.library.defaultListId': 'updateChart',
+		'list.totalWeight': 'updateChart',
+		'list.categoryIds': 'updateChart',
+	},
+	mounted() {
+		this.updateChart();
+	},
+	methods: {
+		updateChart(type) {
+			const chartData = this.library.renderChart(type);
+
+			if (chartData) {
+				if (this.chart) {
+					this.chart.update({ processedData: chartData });
+				} else {
+					this.chart = chart({ processedData: chartData, container: document.getElementsByClassName('lpChart')[0], hoverCallback: this.chartHover });
+				}
+			}
+			return chartData;
+		},
+		chartHover(chartItem) {
+			if (chartItem && chartItem.id) {
+				this.hoveredCategoryId = chartItem.id;
+			} else {
+				this.hoveredCategoryId = null;
+			}
+		},
+		setTotalUnit(unit) {
+			this.$store.commit('setTotalUnit', unit);
+		},
+		updateColor(category, color) {
+			category.color = colorUtils.hexToRgb(color);
+			category.displayColor = colorUtils.rgbToString(colorUtils.hexToRgb(color));
+			this.$store.commit('updateCategoryColor', category);
+			this.updateChart();
+		},
+		colorToHex(color) {
+			return colorUtils.rgbToHex(colorUtils.stringToRgb(color));
+		},
+	},
+};
+
+</script>
diff --git a/src/components/modal.vue b/src/components/modal.vue
@@ -0,0 +1,136 @@
+<style lang="scss">
+@import "../css/_globals";
+
+.lpModal {
+	background: $background1;
+	box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
+	left: 50%;
+	max-height: calc(90% - (#{$spacingLarge} * 2));
+	overflow-y: auto;
+	padding: $spacingLarge;
+	position: fixed;
+	text-align: left;
+	top: 50%;
+	transform: translateX(-50%) translateY(-50%);
+	transition: all $transitionDuration;
+	width: 420px;
+	z-index: $dialog;
+
+	.lpHalf {
+		padding: 0 20px;
+
+		&:first-child {
+			padding-left: 0;
+		}
+
+		&:last-child {
+			padding-right: 0;
+		}
+	}
+
+	p {
+		margin: 5px 0 10px;
+	}
+
+	ul {
+		padding-left: 15px;
+	}
+
+	.lpContent {
+		max-height: 400px;
+		overflow-y: scroll;
+	}
+}
+
+.lpModalHeader {
+	align-items: baseline;
+	display: flex;
+	justify-content: space-between;
+}
+
+.lpModalOverlay {
+	background: rgba(0, 0, 0, 0.5);
+	height: 100%;
+	left: 0;
+	position: fixed;
+	top: 0;
+	transition: all $transitionDuration;
+	width: 100%;
+	z-index: $belowDialog;
+
+	&.lpTransparent {
+		background: rgba(0, 0, 0, 0.01);
+	}
+}
+
+.lpModal-enter,
+.lpModal-leave-active {
+	opacity: 0;
+
+	&.lpModal {
+		transform: translateX(-50%) translateY(-50%) scale(0.95);
+	}
+}
+
+</style>
+
+<template>
+	<div class="lpModalContainer">
+		<transition name="lpModal">
+			<div v-if="shown" :id="id" class="lpModal">
+				<slot />
+			</div>
+		</transition>
+		<transition name="lpModal">
+			<div v-if="shown" :class="{'lpModalOverlay': true, 'lpBlackout': blackout, 'lpTransparent': transparentOverlay}" @click="hide" />
+		</transition>
+	</div>
+</template>
+
+<script>
+export default {
+	name: 'Modal',
+	props: {
+		id: {
+			type: String,
+			required: false,
+		},
+		shown: {
+			type: Boolean,
+			required: true,
+		},
+		blackout: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+		transparentOverlay: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+	},
+	beforeMount() {
+		this.bindEscape();
+	},
+	beforeDestroy() {
+		this.unbindEscape();
+	},
+	methods: {
+		hide() {
+			this.$emit('hide');
+		},
+		bindEscape() {
+			window.addEventListener('keyup', this.closeOnEscape);
+		},
+		unbindEscape() {
+			window.removeEventListener('keyup', this.closeOnEscape);
+		},
+		closeOnEscape(evt) {
+			if (this.shown && evt.keyCode === 27) {
+				this.hide();
+			}
+		},
+	},
+};
+</script>
diff --git a/src/components/moreDropdown.vue b/src/components/moreDropdown.vue
@@ -0,0 +1,46 @@
+<style lang="scss">
+#headerPopover .lpContent {
+	min-width: 9em;
+}
+</style>
+
+<template>
+	<span class="headerItem hasPopover">
+		<PopoverHover id="headerPopover">
+			<span slot="target">More <i class="lpSprite lpExpand" /></span>
+			<div slot="content">
+				<a class="lpHref" @click="showChangePassword">Change password</a><br>
+				<a class="lpHref" @click="showHelp">Help</a><br>
+				<a class="lpHref" @click="logout">Logout</a>
+			</div>
+		</PopoverHover>
+	</span>
+</template>
+
+<script>
+import PopoverHover from './popoverHover.vue';
+
+export default {
+	name: 'MoreDropdown',
+	components: {
+		PopoverHover,
+	},
+	computed: {
+		library() {
+			return this.$store.state.library;
+		},
+	},
+	methods: {
+		showChangePassword() {
+			bus.$emit('showChangePassword');
+		},
+		showHelp() {
+			bus.$emit('showHelp');
+		},
+		logout() {
+			this.$store.commit('logout');
+			router.push('/login');
+		},
+	},
+};
+</script>
diff --git a/src/components/popover.vue b/src/components/popover.vue
@@ -0,0 +1,145 @@
+<style lang="scss">
+@import "../css/_globals";
+
+.lpPopover {
+	display: block;
+	position: relative;
+
+	.lpTarget {
+		cursor: default;
+		display: inline-block;
+		margin-bottom: -10px;
+		padding-bottom: 10px;
+		position: relative;
+	}
+
+	.lpContent {
+		background: #fff;
+		box-shadow: 0 0 6px rgba(0, 0, 0, 0.25);
+		color: $content;
+		left: 50%;
+		margin-top: 15px;
+		min-width: 100%;
+		opacity: 0;
+		padding: 12px;
+		pointer-events: none;
+		position: absolute;
+		top: 100%;
+		transform: translateX(-50%);
+		transition: all 0.15s;
+		white-space: nowrap;
+		z-index: $dialog;
+
+		&::before {
+			background-color: #fff;
+			box-shadow: 0 0 6px rgba(0, 0, 0, 0.25);
+			content: "";
+			display: block;
+			height: 20px;
+			left: 50%;
+			margin-left: -10px;
+			position: absolute;
+			top: -10px;
+			transform: rotate(45deg);
+			width: 20px;
+			z-index: $dialog - 1;
+		}
+
+		&::after {
+			background: #fff;
+			content: "";
+			display: block;
+			height: 15px;
+			left: 0;
+			position: absolute;
+			top: 0;
+			width: 100%;
+			z-index: $dialog + 1;
+		}
+
+		& > *:first-child {
+			margin-top: 0;
+		}
+
+		& > *:last-child {
+			margin-bottom: 0;
+		}
+
+		h3 {
+			margin-bottom: 0;
+		}
+
+		ul, a {
+			line-height: 25px;
+		}
+
+		hr {
+			border-color: $border1;
+			margin: 7px -0;
+			padding: 0;
+		}
+	}
+
+	&.lpPopoverShown {
+		.lpTarget {
+			z-index: $aboveDialog;
+		}
+
+		.lpContent {
+			margin-top: 10px;
+			opacity: 1;
+			pointer-events: all;
+		}
+	}
+}
+
+</style>
+
+<template>
+	<div v-click-outside="hide" :class="{'lpPopover': true, 'lpPopoverShown': shown}">
+		<div class="lpTarget">
+			<slot name="target" />
+		</div>
+		<div class="lpContent">
+			<slot name="content" />
+		</div>
+	</div>
+</template>
+
+<script>
+export default {
+	name: 'Popover',
+	props: {
+		id: {
+			type: String,
+			required: false,
+		},
+		shown: {
+			type: Boolean,
+			required: true,
+		},
+	},
+	beforeMount() {
+		this.bindEscape();
+	},
+	beforeDestroy() {
+		this.unbindEscape();
+	},
+	methods: {
+		hide() {
+			this.$emit('hide');
+		},
+		bindEscape() {
+			window.addEventListener('keyup', this.closeOnEscape);
+		},
+		unbindEscape() {
+			window.removeEventListener('keyup', this.closeOnEscape);
+		},
+		closeOnEscape(evt) {
+			if (this.shown && evt.keyCode === 27) {
+				this.hide();
+			}
+		},
+	},
+};
+</script>
diff --git a/src/components/popoverHover.vue b/src/components/popoverHover.vue
@@ -0,0 +1,45 @@
+<style lang="scss">
+@import "../css/_globals";
+
+</style>
+
+<template>
+    <Popover :shown="shown" @mouseenter.native="show" @mouseleave.native="startHideTimeout">
+        <slot slot="target" name="target" />
+        <slot slot="content" name="content" />
+    </Popover>
+</template>
+
+<script>
+import Popover from './popover.vue';
+
+export default {
+    name: 'PopoverHover',
+    components: {
+        Popover,
+    },
+    data() {
+        return {
+            shown: false,
+            hideTimeout: null,
+        };
+    },
+    methods: {
+        show() {
+            if (this.hideTimeout) {
+                clearTimeout(this.hideTimeout);
+                this.hideTimeout = null;
+            }
+            this.shown = true;
+            this.$emit('shown');
+        },
+        startHideTimeout() {
+            this.hideTimeout = setTimeout(this.hide, 50);
+        },
+        hide() {
+            this.shown = false;
+            this.$emit('hidden');
+        },
+    },
+};
+</script>
diff --git a/src/components/share.vue b/src/components/share.vue
@@ -0,0 +1,52 @@
+<style lang="scss">
+#share label {
+	font-weight: bold;
+}
+</style>
+
+<template>
+	<span class="headerItem hasPopover">
+		<PopoverHover id="share" @shown="focusShare">
+			<span slot="target"><i class="lpSprite lpLink" /> Share</span>
+			<div slot="content" class="lpFields">
+				<div class="lpField">
+					<label for="shareUrl">Share your list</label>
+					<input id="shareUrl" v-select-on-bus="'show-share-box'"  v-select-on-focus type="text" :value="shareUrl">
+				</div>
+				<a id="csvUrl" :href="csvUrl" target="_blank" class="lpHref"><i class="lpSprite lpSpriteDownload" />Export to CSV</a>
+			</div>
+		</PopoverHover>
+	</span>
+</template>
+
+<script>
+import PopoverHover from './popoverHover.vue';
+
+export default {
+	name: 'Share',
+	components: {
+		PopoverHover,
+	},
+	computed: {
+		library() {
+			return this.$store.state.library;
+		},
+		list() {
+			return this.library.getListById(this.library.defaultListId);
+		},
+		externalId() {
+			return this.list.externalId || '';
+		},
+		baseUrl() {
+			const location = window.location;
+			return location.origin ? location.origin : `${location.protocol}//${location.hostname}`;
+		},
+		shareUrl() {
+			return `${this.baseUrl}/list/${this.list.id}`;
+		},
+		csvUrl() {
+			return `${this.baseUrl}/csv/${this.list.id}`;
+		},
+	}
+};
+</script>
diff --git a/src/components/sidebar.vue b/src/components/sidebar.vue
@@ -0,0 +1,97 @@
+<style lang="scss">
+@import "../css/_globals";
+
+$sidebarWidth: 280px;
+$sidebarOverflow: 1000px;
+$sidebarPadding: 20px;
+
+#sidebar {
+	background: #555;
+	box-shadow: -7px 0 7px rgba(0, 0, 0, 0.2) inset;
+	color: #fff;
+	height: 100%;
+	margin-left: -$sidebarOverflow;
+	opacity: 0;
+	padding-left: $sidebarOverflow + $sidebarPadding;
+	padding-right: $sidebarPadding;
+	position: fixed;
+	transition: opacity $transitionDurationSlow ease-in-out 0s;
+	width: $sidebarWidth + $sidebarOverflow + $sidebarPadding*2;
+	z-index: $sidebar;
+
+	.lpHasSidebar & {
+		opacity: 1;
+	}
+
+	h1 {
+		@include fullBleedLeft();
+
+		height: 60px;
+		margin: 0 -20px 20px 0;
+		padding: 20px 0 20px;
+		position: relative;
+
+		span {
+			color: #aaa;
+		}
+	}
+
+	section {
+		margin-bottom: 40px;
+		position: relative;
+	}
+
+	h2 {
+		font-size: 16px;
+		margin: 0 0 10px;
+	}
+
+	ul {
+		background: #606060;
+		margin: 0;
+		overflow-x: hidden;
+		padding: 0;
+	}
+
+	.lpHref {
+		color: $blue2;
+	}
+}
+
+#scrollable {
+	display: flex;
+	flex-direction: column;
+	height: 100%;
+	position: relative;
+	top: 0;
+
+	> h1 {
+		flex: 0 0 auto;
+	}
+}
+
+</style>
+
+<template>
+	<div id="sidebar">
+		<div id="scrollable">
+			<h1>ctucx.things <span>(beta)</span></h1>
+
+			<libraryLists />
+			<libraryItems />
+		</div>
+	</div>
+</template>
+
+<script>
+import libraryItems from './libraryItems.vue';
+import libraryLists from './libraryLists.vue';
+
+export default {
+	name: 'Sidebar',
+	components: {
+		libraryItems,
+		libraryLists,
+	},
+};
+</script>
diff --git a/src/components/speedbump.vue b/src/components/speedbump.vue
@@ -0,0 +1,69 @@
+<style lang="scss">
+
+</style>
+
+<template>
+	<modal id="speedbump" :shown="shown" @hide="shown = false">
+		<h2 v-if="messages.title">
+			{{ messages.title }}
+		</h2>
+
+		<p>{{ messages.body }}</p>
+
+		<div class="buttons">
+			<button v-focus-on-create class="lpButton" @click="confirmSpeedbump()">
+				{{ messages.confirm }}
+			</button>
+			&nbsp;<button class="lpButton" @click="shown = false">
+				{{ messages.cancel }}
+			</button>
+		</div>
+	</modal>
+</template>
+
+<script>
+import modal from './modal.vue';
+
+export default {
+	name: 'Speedbump',
+	components: {
+		modal,
+	},
+	data() {
+		return {
+			defaultMessages: {
+				title: '',
+				body: '',
+				confirm: 'Yes',
+				cancel: 'No',
+			},
+			messages: {},
+			callback: null,
+			shown: false,
+		};
+	},
+	beforeMount() {
+		bus.$on('initSpeedbump', (callback, options) => {
+			this.initSpeedbump(callback, options);
+		});
+	},
+	methods: {
+		initSpeedbump(callback, options) {
+			this.callback = callback;
+			this.messages = Vue.util.extend({}, this.defaultMessages);
+			if (typeof options === 'string') {
+				this.messages.body = options;
+			} else {
+				this.messages = Vue.util.extend(this.messages, options);
+			}
+			this.shown = true;
+		},
+		confirmSpeedbump() {
+			if (this.callback && typeof this.callback === 'function') {
+				this.callback(true);
+			}
+			this.shown = false;
+		},
+	},
+};
+</script>
diff --git a/src/components/spinner.vue b/src/components/spinner.vue
@@ -0,0 +1,47 @@
+<style lang="scss">
+@import "../css/_globals";
+
+$spinnerSize: 18px;
+
+@keyframes spinner {
+	to { transform: rotate(360deg); }
+}
+
+.lpSpinner::before {
+	animation: spinner 0.6s linear infinite;
+	border: 1px solid $orange1;
+	border-radius: 50%;
+	border-top-color: transparent;
+	content: "";
+	display: block;
+	height: $spinnerSize;
+	width: $spinnerSize;
+}
+
+.lpButton .lpSpinner {
+	position: absolute;
+	right: 8px;
+	top: 50%;
+	transform: translateY(-50%);
+
+	&::before {
+		border-color: $background1;
+		border-top-color: transparent;
+	}
+}
+
+.lpButton.lpSmall .lpSpinner {
+	right: 50%;
+	transform: translateY(-50%) translateX(50%);
+}
+</style>
+
+<template>
+	<div class="lpSpinner" />
+</template>
+
+<script>
+export default {
+	name: 'Spinner',
+};
+</script>
diff --git a/src/components/unitSelect.vue b/src/components/unitSelect.vue
@@ -0,0 +1,147 @@
+<style lang="scss">
+@import "../css/_globals";
+
+.lpUnitSelect {
+	border: 1px solid transparent;
+	cursor: pointer;
+	display: inline-block;
+	padding: 0 5px;
+	position: relative;
+
+	&:hover,
+	&.lpHover {
+		background: #fff;
+		border: 1px solid $border1;
+
+		i {
+			opacity: 1;
+		}
+	}
+
+	i {
+		opacity: 0.6;
+	}
+
+	&.lpOpen {
+		background: #fff;
+
+		.lpUnitDropdown {
+			display: block;
+		}
+	}
+
+	.lpDisplay {
+		display: inline-block;
+		width: 1.1em;
+	}
+
+	.lpUnitDropdown {
+		background: #fff;
+		border: 1px solid #ccc;
+		display: none;
+		left: 0;
+		padding: 0;
+		position: absolute;
+		top: -1px;
+		z-index: $aboveSidebar+1;
+
+		&.kg {
+			top: -30px;
+		}
+
+		li {
+			list-style: none;
+			padding: 2px 14px;
+
+			&:hover {
+				background: $blue1;
+				color: #fff;
+			}
+		}
+	}
+}
+</style>
+
+<template>
+	<div class="lpUnitSelect" :class="{lpOpen: isOpen, lpHover: isFocused}" @click="toggle($event)">
+		<select class="lpUnit lpInvisible" :value="unit" @keyup="keyup($event)" @focus="focusSelect" @blur="blurSelect">
+			<option v-for="unit in units" :value="unit">
+				{{ unit }}
+			</option>
+		</select>
+		<span class="lpDisplay">{{ unit }}</span>
+		<i class="lpSprite lpExpand" />
+		<ul :class="'lpUnitDropdown ' + unit">
+			<li v-for="unit in units" :class="unit" @click="select(unit)">
+				{{ unit }}
+			</li>
+		</ul>
+	</div>
+</template>
+
+<script>
+export default {
+	name: 'UnitSelect',
+	props: ['weight', 'unit', 'onChange'],
+	data() {
+		return {
+			units: [
+				'g',
+				'kg',
+			],
+			isOpen: false,
+			isFocused: false,
+		};
+	},
+	methods: {
+		toggle(evt) {
+			evt.stopPropagation();
+			if (!this.isOpen) {
+				this.open();
+			} else {
+				this.close();
+			}
+		},
+		open() {
+			this.isOpen = true;
+			this.bindCloseListeners();
+		},
+		close() {
+			this.isOpen = false;
+			this.unbindCloseListeners();
+		},
+		select(unit) {
+			if (typeof this.onChange === 'function') {
+				this.onChange(unit);
+			}
+		},
+		keyup(evt) {
+			if (typeof this.onChange === 'function') {
+				this.onChange(evt.target.value);
+			}
+		},
+		bindCloseListeners() {
+			window.addEventListener('keyup', this.closeOnEscape);
+			window.addEventListener('click', this.closeOnClick);
+		},
+		unbindCloseListeners() {
+			window.removeEventListener('keyup', this.closeOnEscape);
+			window.removeEventListener('click', this.closeOnClick);
+		},
+		closeOnEscape(evt) {
+			if (evt.keyCode === 27) {
+				this.close();
+			}
+		},
+		closeOnClick(evt) {
+			this.close();
+		},
+		focusSelect() {
+			this.isFocused = true;
+		},
+		blurSelect() {
+			this.isFocused = false;
+		},
+	},
+};
+</script>
diff --git a/src/css/_common.scss b/src/css/_common.scss
@@ -0,0 +1,362 @@
+@import "globals";
+
+.lp {
+	&,
+	body {
+		background: $background1;
+		box-sizing: border-box;
+		color: $content;
+		font-family: 'Open Sans', sans-serif;
+		font-size: 13px;
+		height: 100%;
+		line-height: 1.3;
+		margin: 0;
+		padding: 0;
+	}
+
+	hr {
+		border: none;
+		border-top: 1px solid #aaa;
+		margin: 25px 0;
+	}
+
+	input,
+	select,
+	textarea {
+		color: $content;
+		font-family: 'Open Sans', sans-serif;
+		font-size: 13px;
+	}
+
+	h2 {
+		margin: 0 0 $spacingLarge;
+	}
+
+	h3 {
+		margin: 0 0 $spacingMedium;
+	}
+
+	.hover {
+		background: $yellow2;
+	}
+
+	.tooltip {
+		background: #444;
+		border: 1px solid #fff;
+		box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
+		color: #fff;
+		display: none;
+		padding: 3px;
+		position: absolute;
+		z-index: 105;
+	}
+
+	*,
+	*::before,
+	*::after {
+		box-sizing: inherit;
+	}
+}
+
+.lpHref {
+	color: $blue1;
+	cursor: pointer;
+	text-decoration: none;
+
+	&:hover {
+		text-decoration: underline;
+	}
+}
+
+.lpInvisible {
+	height: 1px;
+	left: -9999px;
+	overflow: hidden;
+	position: absolute;
+	width: 1px;
+}
+
+.lpNotSupported {
+	background: $red1;
+	color: #fff;
+	font-size: 24px;
+	padding: 100px;
+	text-align: center;
+}
+
+.lpNumber {
+	text-align: right;
+}
+
+.lpAdd,
+.lpCopy {
+	color: $green1;
+	cursor: pointer;
+	display: inline-block;
+	text-decoration: none;
+
+	i {
+		margin-right: 5px;
+	}
+
+	&:hover {
+		text-decoration: underline;
+	}
+
+	#sidebar & {
+		color: $green1;
+	}
+}
+
+.lpHalf {
+	float: left;
+	width: 50%;
+}
+
+#lp {
+	background: inherit;
+}
+
+#main {
+	background: inherit;
+	margin: 0 auto;
+	max-width: 960px;
+	min-height: 100%;
+	position: relative;
+	text-align: left;
+	transition: all $transitionDurationSlow;
+
+	&.lpHasSidebar {
+		max-width: 1280px;
+	}
+}
+
+.lpRemove {
+	border-radius: 16px;
+	cursor: pointer;
+	display: inline-block;
+	line-height: 8px;
+	margin-bottom: 3px;
+	opacity: 0.4;
+	padding: 5px 6px 6px;
+	visibility: hidden;
+
+	&:hover {
+		background: $red1;
+		box-shadow: 0 3px 3px rgba(0, 0, 0, 0.25) inset;
+		opacity: 1;
+	}
+}
+
+.handle {
+	background: url(/images/handle.png);
+	cursor: move;
+	cursor: -webkit-grab;
+	cursor: -moz-grab;
+	float: left;
+	height: 24px;
+	margin-top: 2px;
+	visibility: hidden;
+	width: 12px;
+}
+
+.lpAlignRight {
+	text-align: right;
+}
+
+.lpLegend {
+	border: 1px solid #fff;
+	display: block;
+	height: 10px;
+	width: 10px;
+}
+
+.lpFields {
+	input[type=text],
+	input[type=email],
+	input[type=password],
+	textarea {
+		background: #fff;
+		border: 1px solid $border1;
+		display: block;
+		margin: 0 0 $spacingMedium;
+		padding: $spacingSmall;
+		width: 100%;
+
+		&:focus {
+			border-color: $orange1;
+			outline: none;
+		}
+	}
+}
+
+.lpButtons {
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	text-align: center;
+
+	> * {
+		margin-bottom: $spacingMedium;
+
+		&:last-child {
+			margin-bottom: 0;
+		}
+	}
+}
+
+.lp .lpButton { /* TODO: find out why this specificity is needed */
+	background-image: linear-gradient(165deg, $orange1 50%, $orange2 100%);
+	background-position: 100% 100%;
+	background-size: 200%;
+	border: none;
+	border-radius: 4px;
+	box-shadow: 0 0 1px rgba(255, 255, 255, 0) inset, 0 0 2px rgba(0, 0, 0, 0.2);
+	color: #fff;
+	cursor: pointer;
+	display: inline-block;
+	font-size: 16px;
+	font-weight: 600;
+	line-height: 23px;
+	padding: 8px 20px;
+	position: relative;
+	text-align: center;
+	text-decoration: none;
+	text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.15);
+	transition: all 0.2s;
+	width: auto;
+
+	&.lpSmall {
+		font-size: 13px;
+		padding: 5px 10px;
+	}
+
+	&:hover:not(:disabled),
+	&:active {
+		background-position: 0 0;
+		box-shadow: 0 0 1px rgba(255, 255, 255, 0.5) inset, 0 1px 3px darken($orange2, 25%);
+	}
+
+	&:focus,
+	&:active {
+		outline: none;
+	}
+
+	&:focus:not(:active) {
+		box-shadow: 0 0 0 1px darken($orange2, 15%) inset, 0 1px 3px darken($orange2, 25%);
+		outline: none;
+	}
+
+	&::-moz-focus-inner {
+		border: none;
+	}
+
+	&:disabled,
+	&.lpButtonDisabled {
+		cursor: default;
+		opacity: 0.5;
+	}
+}
+
+.lpButtons .lpButton {
+	width: 100%;
+}
+
+.lpListSummary {
+	align-items: center;
+	display: flex;
+	flex-direction: row;
+	justify-content: center;
+}
+
+.lpTable {
+	display: table;
+	margin: 0;
+	padding: 0;
+}
+
+.lpRow {
+	display: table-row;
+
+	&:first-child > .lpCell {
+		border-top: none;
+	}
+
+	&.lpHeader {
+		.lpCell {
+			border-bottom: 1px solid #aaa;
+			vertical-align: bottom;
+		}
+
+		+ .lpRow .lpCell {
+			border-top: none;
+		}
+	}
+}
+
+.lpFooter {
+	.lpCell {
+		border-top: 1px solid #aaa;
+	}
+}
+
+.lpCell {
+	border-top: 1px dotted #aaa;
+	display: table-cell;
+	padding: 1px 8px;
+
+	&:first-child {
+		padding-left: 0;
+	}
+
+	&:last-child {
+		padding-right: 0;
+	}
+}
+
+.lpHeader,
+.lpSubtotal {
+	font-weight: 600;
+
+	+ .lpItem {
+		border-top: none;
+	}
+}
+
+.lpError {
+	color: $red1;
+}
+
+.lpSuccess {
+	color: darken($green1, 10%);
+}
+
+.lpWarning {
+	background: $yellow2;
+	border: 1px solid $darkYellow;
+	border-radius: 5px;
+	padding: 10px;
+}
+
+#lpImageDialog {
+	width: auto;
+
+	img {
+		max-width: 800px;
+	}
+}
+
+#lpFooter {
+	color: #777;
+	display: flex;
+	justify-content: space-between;
+	padding: 150px 0 20px;
+}
+
+.lpLibraryList:hover .lpRemove,
+.lpCategory .lpHeader:hover .lpRemove,
+.lpLibraryItem:hover .lpRemove,
+.lpLibraryItem:hover .handle,
+.lpCategory .lpHeader:hover .handle {
+	visibility: visible;
+}
diff --git a/src/css/_edit.scss b/src/css/_edit.scss
@@ -0,0 +1,75 @@
+@import "globals";
+
+.lpSilent {
+	background: transparent;
+	border: 1px solid transparent;
+
+	&:hover {
+		background-color: #fff;
+		border: 1px solid rgba(0, 0, 0, 0.15);
+	}
+
+	&:focus {
+		background-color: #fff;
+		border: 1px solid rgba(0, 0, 0, 0.15);
+		box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15) inset;
+		outline: none;
+	}
+
+	&.lpSilentError {
+		background-color: lighten(desaturate($red1, 10%), 40%);
+		border: 1px solid $red1;
+		outline-color: $red1;
+
+		&:focus {
+			background-color: #fff;
+		}
+	}
+}
+
+/* Body */
+
+.addCategory,
+.lpAddItem {
+	margin-left: 20px;
+	margin-top: 6px;
+}
+
+#csv,
+#image {
+	height: 1px;
+	left: -999px;
+	overflow: hidden;
+	position: absolute;
+	width: 1px;
+}
+
+.lpHref.close {
+	margin-left: 10px;
+}
+
+.gu-mirror {
+	-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
+	filter: alpha(opacity=80);
+	margin: 0 !important;
+	opacity: 0.8;
+	position: fixed !important;
+	z-index: 9999 !important;
+}
+
+.gu-hide {
+	display: none !important;
+}
+
+.gu-unselectable {
+	-webkit-user-select: none !important;
+	-moz-user-select: none !important;
+	-ms-user-select: none !important;
+	user-select: none !important;
+}
+
+.gu-transit {
+	-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
+	filter: alpha(opacity=20);
+	opacity: 0.2;
+}
diff --git a/src/css/_globals.scss b/src/css/_globals.scss
@@ -0,0 +1,73 @@
+$background1: #f7f7f7;
+$content: #282828;
+$content2: #666;
+
+$border1: #ccc;
+
+$blue1: #1b77d3;
+$blue2: #3db7ff;
+$darkBlue: #064989;
+
+$green1: #7ab317;
+$green2: #a8d46f;
+
+$orange1: #efa026;
+//$orange1: #ffb036;
+//$orange2: #F18828;
+$orange2: #ee801c;
+
+$red1: #ce1836;
+
+$yellow1: #f2d000;
+$yellow2: #fcefa9;
+
+$darkYellow: #a58c00;
+
+/* spacing */
+$spacingSmaller: 5px;
+$spacingSmall: 10px;
+$spacingMedium: 15px;
+$spacingLarge: 30px;
+
+/* z-index stack */
+$belowSidebar: 10;
+$sidebar: 20;
+$aboveSidebar: 30;
+
+$belowDialog: 90;
+$dialog: 100;
+$aboveDialog: 110;
+
+$transitionDurationFast: 0.1s;
+$transitionDuration: 0.2s;
+$transitionDurationSlow: 0.4s;
+
+@mixin fullBleedLeft() {
+	&::before {
+		background: inherit;
+		border-bottom: inherit;
+		bottom: -1px;
+		content: "";
+		display: block;
+		position: absolute;
+		right: 100%;
+		top: 0;
+		width: 999px;
+		z-index: $belowSidebar;
+	}
+}
+
+@mixin fullBleedRight() {
+	&::after {
+		background: inherit;
+		border-bottom: inherit;
+		bottom: -1px;
+		content: "";
+		display: block;
+		left: 100%;
+		position: absolute;
+		top: 0;
+		width: 999px;
+		z-index: $belowSidebar;
+	}
+}
diff --git a/src/css/_list.scss b/src/css/_list.scss
@@ -0,0 +1,221 @@
+@import "globals";
+
+.lpList {
+	background: inherit;
+	min-height: 100%;
+	padding: 0 20px;
+	position: relative;
+	transition: all $transitionDurationSlow;
+	z-index: $aboveSidebar;
+
+	.lpHasSidebar & {
+		margin-left: 320px;
+	}
+}
+
+#totals {
+	margin: 0;
+	padding: 0 0 0 10px;
+	text-align: left;
+}
+
+#chart.activeHover {
+	cursor: pointer;
+}
+
+.lpCategories {
+	margin: 0;
+	padding: 0;
+}
+
+.lpCategory {
+	border: 2px solid transparent;
+	list-style: none;
+	margin: 0 0 20px;
+
+	&.dropAccept {
+		background: $yellow2;
+	}
+
+	&.dropHover {
+		background: $green2;
+	}
+
+	&.gu-mirror {
+		background: #f7f7f7;
+		border: 1px solid #999;
+	}
+}
+
+#lpListDescription {
+	h2 {
+		border-bottom: 1px solid #aaa;
+	}
+}
+
+input.lpCategoryName { /* specificity */
+	border: 1px solid transparent;
+	flex: 1 0 300px;
+	font-size: 16px;
+	font-weight: 600;
+	padding-bottom: 0;
+}
+
+.lpSubtotal {
+	text-align: right;
+	white-space: nowrap;
+}
+
+.lpSubtotalUnit {
+	border-left: 1px solid transparent;
+	display: inline-block;
+	padding-left: 5px;
+	padding-right: 23px;
+	width: 43px;
+}
+
+.lpDisplaySubtotal,
+.lpTotalValue {
+	padding-right: 4px;
+}
+
+.lpItems {
+	margin: 0;
+	padding: 0;
+	width: 100%;
+}
+
+.lpItem,
+.lpItemsHeader,
+.lpItemsFooter {
+	align-items: center;
+	border-top: 1px dotted #aaa;
+	display: flex;
+	flex-direction: row;
+	justify-content: stretch;
+
+	&.gu-mirror {
+		background: #f7f7f7;
+		border: 1px solid #999;
+	}
+
+	.handleCell {
+		flex: 0 0 12px;
+		padding-right: 4px;
+	}
+
+	.lpImageCell {
+		flex: 0 0 90px;
+
+		.lpItemImage {
+			cursor: pointer;
+			max-width: 90px;
+			position: relative;
+			top: 3px;
+		}
+	}
+
+	.lpName {
+		flex: 1 1 150px;
+		margin-right: 4px;
+		min-width: 50px;
+	}
+
+	.lpDescription {
+		flex: 2 0 200px;
+		margin-right: 4px;
+		min-width: 100px;
+	}
+
+	.lpActionsCell {
+		align-self: center;
+		flex: 0 0 130px;
+	}
+
+	.lpWeightCell {
+		flex: 0 0 110px;
+
+		.lpWeight {
+			width: 50px;
+		}
+	}
+
+	.lpPriceCell {
+		flex: 0 0 66px;
+		margin-right: 4px;
+
+		.lpPrice {
+			width: 66px;
+		}
+	}
+
+	.lpQtyCell {
+		flex: 0 0 60px;
+
+		.lpQty {
+			width: 39px;
+		}
+	}
+
+	.lpRemoveCell {
+		align-self: center;
+		flex: 0 0 20px;
+	}
+
+	.lpCategoryName { /* Header */
+		flex: 1 1 auto;
+	}
+
+	.lpAddItemCell { /* Footer */
+		flex: 1 1 auto;
+	}
+}
+
+.lpItemsHeader {
+	align-items: flex-end;
+	border-bottom: 1px solid #aaa;
+	border-top: none;
+	height: 31px;
+}
+
+.lpItemsFooter {
+	border-top-style: solid;
+	justify-content: flex-end;
+}
+
+.lpActionsCell {
+	.lpWorn,
+	.lpConsumable,
+	.lpCamera,
+	.lpLink,
+	.lpStar {
+		cursor: pointer;
+		margin-right: 8px;
+		opacity: 0.5;
+		visibility: hidden;
+
+		&:hover {
+			opacity: 1;
+		}
+
+		&.lpActive,
+		&.lpStar1,
+		&.lpStar2,
+		&.lpStar3 {
+			opacity: 1;
+			visibility: visible;
+		}
+	}
+
+	i:last-child {
+		margin-right: 0;
+	}
+}
+
+.lpHeader {
+	.lpWeightCell,
+	.lpQtyCell,
+	.lpPriceCell {
+		text-align: center;
+	}
+}
diff --git a/src/css/_print.scss b/src/css/_print.scss
@@ -0,0 +1,39 @@
+@media print {
+	body,
+	#content,
+	#container {
+		background: #fff;
+		float: none;
+		margin: 0;
+		width: 100%;
+	}
+
+	#topRight,
+	#sidebar,
+	.addCategory,
+	.lpAddItem,
+	.lpExpand,
+	.ad,
+	.noprint,
+	#lpFooter,
+	hr,
+	#hamburger {
+		display: none;
+	}
+
+	.lpHasSidebar .lpList,
+	.lpListName {
+		margin-left: 0;
+	}
+
+	.handleCell,
+	.lpItems .lpFooter > .lpCell:first-child,
+	.lpRemoveCell,
+	.lpActionsCell .lpLink,
+	.lpActionsCell .lpCamera,
+	.lpQtyCell .lpArrows { display: none !important; }
+
+	.lpActionsCell { width: 80px !important; }
+	.lpQtyCell { width: 20px !important; }
+	.lpWeightCell { width: 99px !important; }
+}
diff --git a/src/css/_share.scss b/src/css/_share.scss
@@ -0,0 +1,277 @@
+#main.lpShare, .lpList {
+	transition: none !important;
+}
+
+.lpShare {
+	.lpCategoryName {
+		font-size: 18px;
+		margin: 0;
+	}
+
+	.lpItem {
+		&:hover {
+			background: #fff;
+		}
+
+		.lpActionsCell {
+			flex-basis: 75px;
+		}
+
+		&.lpItemHasImage .lpImageCell {
+			padding-right: 5px;
+		}
+	}
+
+	#lpListDescription {
+		margin: 30px 0;
+	}
+}
+
+.lpUnitSelect {
+	border: 1px solid transparent;
+	cursor: pointer;
+	display: inline-block;
+	padding: 0 5px;
+	position: relative;
+
+	&:hover,
+	&.lpHover {
+		background: #fff;
+		border: 1px solid $border1;
+
+		i {
+			opacity: 1;
+		}
+	}
+
+	i {
+		opacity: 0.6;
+	}
+
+	&.lpOpen {
+		background: #fff;
+
+		.lpUnitDropdown {
+			display: block;
+		}
+	}
+
+	.lpDisplay {
+		display: inline-block;
+		width: 1.1em;
+	}
+
+	.lpUnitDropdown {
+		background: #fff;
+		border: 1px solid #ccc;
+		display: none;
+		left: 0;
+		padding: 0;
+		position: absolute;
+		top: -1px;
+		z-index: $aboveSidebar+1;
+
+		&.kg {
+			top: -30px;
+		}
+
+		li {
+			list-style: none;
+			padding: 2px 14px;
+
+			&:hover {
+				background: $blue1;
+				color: #fff;
+			}
+		}
+	}
+}
+
+.lpDialog {
+	animation: fadein 0.25s linear 0s 1 forwards;
+	opacity: 0;
+	background: #F7F7F7;
+	box-shadow: 0 0 30px rgba(0, 0, 0, 0.4);
+	left: 50%;
+	padding: 35px;
+	position: fixed;
+	text-align: left;
+	top: 50%;
+	transform: translateX(-50%) translateY(-50%);
+	width: 350px;
+	z-index: 100;
+  }
+  @keyframes fadein {
+	from {
+	  opacity: 0;
+	}
+	to {
+	  opacity: 1;
+	}
+  }
+  .lpDialog.fadeout {
+	animation: fadeout 0.25s linear 0s 1 forwards;
+  }
+  @keyframes fadeout {
+	from {
+	  opacity: 1;
+	}
+	to {
+	  opacity: 0;
+	}
+  }
+  .lpModalOverlay {
+	animation: fadein 0.25s linear 0s 1 forwards;
+	opacity: 0;
+	background: rgba(0, 0, 0, 0.5);
+	height: 100%;
+	left: 0;
+	position: fixed;
+	top: 0;
+	width: 100%;
+	z-index: 90;
+  }
+  @keyframes fadein {
+	from {
+	  opacity: 0;
+	}
+	to {
+	  opacity: 1;
+	}
+  }
+  .lpModalOverlay.fadeout {
+	animation: fadeout 0.25s linear 0s 1 forwards;
+  }
+  @keyframes fadeout {
+	from {
+	  opacity: 1;
+	}
+	to {
+	  opacity: 0;
+	}
+  }
+@media only screen and (max-width: 720px) {
+	.lpChart {
+		max-width: 100%;
+	}
+
+	.lpItems {
+		position: relative;
+
+		.lpWeightCell {
+			flex-basis: 100px;
+		}
+
+		.lpPriceCell {
+			flex-basis: 65px;
+		}
+
+		.lpHeader {
+			.lpWeightCell,
+			.lpPriceCell,
+			.lpQtyCell {
+				display: none;
+			}
+		}
+
+		.lpFooter {
+			justify-content: flex-end;
+
+			.lpQtyCell {
+				display: none;
+			}
+		}
+	}
+
+	.lpItem {
+		align-items: baseline;
+		flex-flow: row wrap;
+		justify-content: flex-end;
+		padding: 3px 0;
+
+		.lpShowImages &.lpItemHasImage {
+			min-height: 90px;
+			padding-left: 100px;
+			position: relative;
+
+			.lpImageCell {
+				height: 90px;
+				left: 0;
+				position: absolute;
+				top: 0;
+				width: 90px;
+			}
+		}
+
+		.lpImageCell {
+			flex-basis: 0;
+		}
+
+		.lpName {
+			flex-basis: 90px;
+			font-weight: bold;
+			order: 2;
+		}
+
+		.lpQtyCell {
+			flex-basis: auto;
+			order: 1;
+
+			&[qty1] {
+				display: none;
+			}
+
+			&::after {
+				content: " x ";
+				display: inline;
+				margin-right: 5px;
+			}
+		}
+
+		.lpPriceCell {
+			order: 4;
+		}
+
+		.lpWeightCell {
+			order: 5;
+		}
+
+		.lpDescription {
+			flex-basis: calc(100% - 50px); /* Always wrap */
+			order: 98;
+		}
+
+		.lpActionsCell {
+			flex: 0 0 auto;
+			order: 99;
+			padding: 0 10px;
+			text-align: right;
+
+			.lpHidden,
+			i:not(.lpActive) {
+				display: none;
+			}
+		}
+	}
+
+	#lpImageDialog {
+		padding: 5px;
+		width: 90%;
+
+		img {
+			display: block;
+			max-width: 100%;
+		}
+	}
+}
+
+@media only screen and (max-width: 480px) {
+	.lpListSummary {
+		flex-direction: column;
+		margin-bottom: 30px;
+	}
+
+	.lpChartContainer {
+		max-width: 200px;
+	}
+}
diff --git a/src/css/_sprite.scss b/src/css/_sprite.scss
@@ -0,0 +1,118 @@
+.lpSprite {
+	background-image: url(/images/sprite2x.png);
+	background-size: 400px;
+	display: inline-block;
+	height: 14px;
+	overflow: hidden;
+	position: relative;
+	top: 2px;
+	width: 14px;
+
+	&.hidden {
+		visibility: hidden;
+	}
+}
+
+.lpSpriteAdd {
+	background-position: 0 0;
+}
+
+.lpSpriteUpload {
+	background-position: -250px 0;
+}
+
+.lpSpriteDownload {
+	background-position: -250px -25px;
+}
+
+.lpSpriteCopy {
+	background-position: 0 -50px;
+}
+
+.lpSpriteRemove {
+	background-position: -25px 0;
+	height: 8px;
+	top: 0;
+	width: 8px;
+
+	.lpRemove:hover & {
+		background-position: -25px -25px;
+	}
+}
+
+.lpWorn {
+	background-position: -50px 0;
+	width: 17px;
+
+	&.lpActive {
+		background-position: -50px -25px;
+	}
+
+	&.lpWornOne {
+		background-position: -50px -50px;
+	}
+}
+
+.lpLinkBig {
+	background-position: -75px 0;
+	height: 20px;
+	width: 18px;
+}
+
+.lpCamera {
+	background-position: -100px 0;
+	width: 18px;
+}
+
+.lpExpand {
+	background-position: -125px 0;
+}
+
+.lpHamburger {
+	background-position: -150px 0;
+	height: 20px;
+	width: 20px;
+}
+
+.lpLink {
+	background-position: -175px 0;
+	width: 13px;
+
+	&.lpActive {
+		background-position: -175px -25px;
+	}
+}
+
+.lpUp {
+	background-position: -200px 0;
+	height: 7px;
+	width: 7px;
+}
+
+.lpDown {
+	background-position: -200px -25px;
+	height: 7px;
+	width: 7px;
+}
+
+.lpStar {
+	background-position: -225px 0;
+	height: 14px;
+	width: 14px;
+	&.lpStar1 { background-position: -225px -25px; }
+	&.lpStar2 { background-position: -225px -50px; }
+	&.lpStar3 { background-position: -225px -75px; }
+}
+
+.lpConsumable {
+	background-position: -275px 0;
+	width: 17px;
+
+	&.lpActive {
+		background-position: -275px -25px;
+	}
+}
+
+.lpSettings {
+	background-position: -300px 0;
+}
diff --git a/src/css/app.scss b/src/css/app.scss
@@ -0,0 +1,5 @@
+@import "_common";
+@import "_edit";
+@import "_list";
+@import "_print";
+@import "_sprite";
diff --git a/src/css/view.scss b/src/css/view.scss
@@ -0,0 +1,26 @@
+@import "_common";
+@import "_list";
+@import "_print";
+@import "_share";
+@import "_sprite";
+
+nav {
+	margin-top: 2em;
+	padding: 0px 1em;
+	vertical-align: bottom;
+}
+
+nav a {
+	text-decoration: none;
+	padding: 2px 0.75em;
+	font-size: 110%;
+}
+
+nav a.active {
+	color: #000;
+	background-color: #ccc;
+}
+
+nav a.right {
+	float: right;
+}
diff --git a/src/dataTypes.js b/src/dataTypes.js
@@ -0,0 +1,768 @@
+import assignIn    from 'lodash/assignIn.js';
+
+import colorUtils  from './utils/color.js';
+import weightUtils from './utils/weight.js';
+
+const defaultOptionalFields = {
+	images: false,
+	price: false,
+	worn: true,
+	consumable: true,
+	listDescription: false,
+};
+
+const Item = function ({ id, unit }) {
+	this.id = id;
+	this.name = '';
+	this.description = '';
+	this.weight = 0;
+	this.authorUnit = 'g';
+	if (unit) {
+		this.authorUnit = unit;
+	}
+	this.price = 0.00;
+	this.imageUrl = '';
+	this.url = '';
+
+	return this;
+};
+
+Item.prototype.save = function () {
+	return this;
+};
+
+Item.prototype.load = function (input) {
+	assignIn(this, input);
+	if (typeof this.price === 'string') {
+		this.price = parseFloat(this.price, 10);
+	}
+};
+
+const Category = function ({ library, id, _isNew }) {
+	this.library = library;
+	this.id = id;
+	this.name = '';
+	this.categoryItems = [];
+
+	this.subtotalWeight = 0;
+	this.subtotalWornWeight = 0;
+	this.subtotalConsumableWeight = 0;
+	this.subtotalPrice = 0;
+	this.subtotalConsumablePrice = 0;
+	this.subtotalQty = 0;
+
+	this._isNew = _isNew;
+	return this;
+};
+
+Category.prototype.addItem = function (partialCategoryItem) {
+	const tempCategoryItem = {
+		qty: 1,
+		worn: 0,
+		consumable: false,
+		star: 0,
+		itemId: null,
+		_isNew: false,
+	};
+	assignIn(tempCategoryItem, partialCategoryItem);
+	this.categoryItems.push(tempCategoryItem);
+};
+
+Category.prototype.updateCategoryItem = function (categoryItem) {
+	const oldCategoryItem = this.getCategoryItemById(categoryItem.itemId);
+	assignIn(oldCategoryItem, categoryItem);
+};
+
+Category.prototype.removeItem = function (itemId) {
+	const categoryItem = this.getCategoryItemById(itemId);
+	const index = this.categoryItems.indexOf(categoryItem);
+	this.categoryItems.splice(index, 1);
+};
+
+Category.prototype.calculateSubtotal = function () {
+	this.subtotalWeight = 0;
+	this.subtotalWornWeight = 0;
+	this.subtotalConsumableWeight = 0;
+	this.subtotalPrice = 0;
+	this.subtotalConsumablePrice = 0;
+	this.subtotalQty = 0;
+
+	for (const i in this.categoryItems) {
+		const categoryItem = this.categoryItems[i];
+		const item = this.library.getItemById(categoryItem.itemId);
+		if (!item) {
+			continue;
+		}
+		this.subtotalWeight += item.weight * categoryItem.qty;
+		this.subtotalPrice += item.price * categoryItem.qty;
+
+		if (this.library.optionalFields.worn && categoryItem.worn) {
+			this.subtotalWornWeight += item.weight * ((categoryItem.qty > 0) ? 1 : 0);
+		}
+		if (this.library.optionalFields.consumable && categoryItem.consumable) {
+			this.subtotalConsumableWeight += item.weight * categoryItem.qty;
+			this.subtotalConsumablePrice += item.price * categoryItem.qty;
+		}
+		this.subtotalQty += categoryItem.qty;
+	}
+};
+
+Category.prototype.getCategoryItemById = function (id) {
+	for (const i in this.categoryItems) {
+		const categoryItem = this.categoryItems[i];
+		if (categoryItem.itemId == id) return categoryItem;
+	}
+	return null;
+};
+
+Category.prototype.getExtendedItemByIndex = function (index) {
+	const categoryItem = this.categoryItems[index];
+	const item = this.library.getItemById(categoryItem.itemId);
+	const extendedItem = assignIn({}, item);
+	assignIn(extendedItem, categoryItem);
+	return extendedItem;
+};
+
+Category.prototype.save = function () {
+	const out = assignIn({}, this);
+
+	delete out.library;
+	delete out.template;
+	delete out._isNew;
+
+	return out;
+};
+
+Category.prototype.load = function (input) {
+	delete input._isNew;
+
+	assignIn(this, input);
+
+	this.categoryItems.forEach((categoryItem, index) => {
+		delete categoryItem._isNew;
+		if (typeof categoryItem.price !== 'undefined') {
+			delete categoryItem.price;
+		}
+		if (!categoryItem.star) {
+			categoryItem.star = 0;
+		}
+		if (!this.library.getItemById(categoryItem.itemId)) {
+			this.categoryItems.splice(index, 1);
+		}
+	});
+};
+
+const List = function ({ id, library }) {
+	this.library = library;
+	this.id = id;
+	this.name = '';
+	this.categoryIds = [];
+	this.chart = null;
+	this.description = '';
+	this.externalId = '';
+
+	this.totalWeight = 0;
+	this.totalWornWeight = 0;
+	this.totalConsumableWeight = 0;
+	this.totalBaseWeight = 0;
+	this.totalPackWeight = 0;
+	this.totalPrice = 0;
+	this.totalConsumablePrice = 0;
+	this.totalQty = 0;
+
+	return this;
+};
+
+List.prototype.addCategory = function (categoryId) {
+	this.categoryIds.push(categoryId);
+};
+
+List.prototype.removeCategory = function (categoryId) {
+	categoryId = parseInt(categoryId);
+	let index = this.categoryIds.indexOf(categoryId);
+	if (index == -1) {
+		index = this.categoryIds.indexOf(`${categoryId}`);
+		if (index == -1) {
+			console.warn(`Unable to delete category, it does not exist in this list:${categoryId}`);
+			return false;
+		}
+	}
+
+	this.categoryIds.splice(index, 1);
+	return true;
+};
+
+List.prototype.renderChart = function (type, linkParent) {
+	const chartData = { points: {} };
+	let total = 0;
+
+	if (typeof linkParent === 'undefined') linkParent = true;
+
+	for (var i in this.categoryIds) {
+		var category = this.library.getCategoryById(this.categoryIds[i]);
+		if (category) {
+			category.calculateSubtotal();
+
+			if (type === 'consumable') {
+				total += category.subtotalConsumableWeight;
+			} else if (type === 'worn') {
+				total += category.subtotalWornWeight;
+			} else if (type === 'base') {
+				total += (category.subtotalWeight - (category.subtotalConsumableWeight + category.subtotalWornWeight));
+			} else { // total weight
+				total += category.subtotalWeight;
+			}
+		}
+	}
+
+	if (!total) return false;
+
+	const getTooltipText = function (name, valueMg, unit) {
+		return `${name}: ${weightUtils.MgToWeight(valueMg, unit)} ${unit}`;
+	};
+
+	for (var i in this.categoryIds) {
+		var category = this.library.getCategoryById(this.categoryIds[i]);
+		if (category) {
+			const points = {};
+
+			var categoryTotal;
+			if (type === 'consumable') {
+				categoryTotal = category.subtotalConsumableWeight;
+			} else if (type === 'worn') {
+				categoryTotal = category.subtotalWornWeight;
+			} else if (type === 'base') {
+				categoryTotal = (category.subtotalWeight - (category.subtotalConsumableWeight + category.subtotalWornWeight));
+			} else { // total weight
+				categoryTotal = category.subtotalWeight;
+			}
+
+			const tempColor = category.color || colorUtils.getColor(i);
+			category.displayColor = colorUtils.rgbToString(tempColor);
+			const tempCategory = {};
+
+			for (const j in category.categoryItems) {
+				const item = category.getExtendedItemByIndex(j);
+				let value = item.weight * item.qty;
+				if (!value) value = 0;
+				let name = getTooltipText(item.name, value, item.authorUnit);
+				const color = colorUtils.getColor(j, tempColor);
+				if (item.qty > 1) name += ` x ${item.qty}`;
+				var percent = value / categoryTotal;
+				const tempItem = {
+					value, id: item.id, name, color, percent,
+				};
+				if (linkParent) tempItem.parent = tempCategory;
+				points[j] = tempItem;
+			}
+			var percent = categoryTotal / total;
+			const tempCategoryData = {
+				points, color: category.color, id: category.id, name: getTooltipText(category.name, categoryTotal, this.library.totalUnit), total: categoryTotal, percent, visiblePoints: false,
+			};
+			if (linkParent) tempCategoryData.parent = chartData;
+			assignIn(tempCategory, tempCategoryData);
+			chartData.points[i] = tempCategory;
+		}
+	}
+	chartData.total = total;
+
+	return chartData;
+};
+
+List.prototype.calculateTotals = function () {
+	let totalWeight = 0;
+	let totalPrice = 0;
+	let totalWornWeight = 0;
+	let totalConsumableWeight = 0;
+	let totalConsumablePrice = 0;
+	let totalBaseWeight = 0;
+	let totalPackWeight = 0;
+	let totalQty = 0;
+	const out = { categories: [] };
+
+	for (const i in this.categoryIds) {
+		const category = this.library.getCategoryById(this.categoryIds[i]);
+
+		if (category) {
+			category.calculateSubtotal();
+
+			totalWeight += category.subtotalWeight;
+			totalWornWeight += category.subtotalWornWeight;
+			totalConsumableWeight += category.subtotalConsumableWeight;
+
+			totalPrice += category.subtotalPrice;
+			totalConsumablePrice += category.subtotalConsumablePrice;
+
+			totalQty += category.subtotalQty;
+
+			out.categories.push(category);
+		}
+	}
+
+	totalBaseWeight = totalWeight - (totalWornWeight + totalConsumableWeight);
+	totalPackWeight = totalWeight - totalWornWeight;
+
+	this.totalWeight = totalWeight;
+	this.totalWornWeight = totalWornWeight;
+	this.totalConsumableWeight = totalConsumableWeight;
+
+	this.totalBaseWeight = totalBaseWeight;
+	this.totalPackWeight = totalPackWeight;
+
+	this.totalPrice = totalPrice;
+	this.totalConsumablePrice = totalConsumablePrice;
+
+	this.totalQty = totalQty;
+};
+
+List.prototype.save = function () {
+	const out = assignIn({}, this);
+	delete out.library;
+	delete out.chart;
+	return out;
+};
+
+List.prototype.load = function (input) {
+	assignIn(this, input);
+	this.calculateTotals();
+};
+
+const Library = function () {
+	this.version = '0.3';
+	this.idMap = {};
+	this.items = [];
+	this.categories = [];
+	this.lists = [];
+	this.sequence = 0;
+	this.defaultListId = 1;
+	this.totalUnit = 'kg';
+	this.itemUnit = 'g';
+	this.showSidebar = true;
+	this.showImages = false;
+	this.optionalFields = assignIn({}, defaultOptionalFields);
+	this.currencySymbol = '€';
+	this.firstRun();
+	return this;
+};
+
+
+Library.prototype.firstRun = function () {
+	const firstList = this.newList();
+	const firstCategory = this.newCategory({ list: firstList });
+	const firstItem = this.newItem({ category: firstCategory });
+};
+
+Library.prototype.newItem = function ({ category, _isNew }) {
+	const temp = new Item({ id: this.nextSequence(), unit: this.itemUnit });
+	this.items.push(temp);
+	this.idMap[temp.id] = temp;
+	if (category) {
+		category.addItem({ itemId: temp.id, _isNew });
+	}
+	return temp;
+};
+
+Library.prototype.updateItem = function (item) {
+	const oldItem = this.getItemById(item.id);
+	assignIn(oldItem, item);
+	return oldItem;
+};
+
+Library.prototype.removeItem = function (id) {
+	const item = this.getItemById(id);
+	for (const i in this.lists) {
+		const category = this.findCategoryWithItemById(id, this.lists[i].id);
+		if (category) {
+			category.removeItem(id);
+		}
+	}
+
+	this.items.splice(this.items.indexOf(item), 1);
+	delete this.idMap[id];
+
+	return true;
+};
+
+Library.prototype.newCategory = function ({ list, _isNew }) {
+	const temp = new Category({ id: this.nextSequence(), _isNew, library: this });
+
+	this.categories.push(temp);
+	this.idMap[temp.id] = temp;
+	if (list) {
+		list.addCategory(temp.id);
+	}
+
+	return temp;
+};
+
+Library.prototype.removeCategory = function (id, force) {
+	const category = this.getCategoryById(id);
+	const list = this.findListWithCategoryById(id);
+
+	if (list && list.categoryIds.length == 1 && !force) {
+		alert("Can't remove the last category in a list!");
+		return false;
+	}
+
+	if (list) {
+		list.removeCategory(id);
+	}
+
+	this.categories.splice(this.categories.indexOf(category), 1);
+	delete this.idMap[id];
+
+	return true;
+};
+
+Library.prototype.newList = function () {
+	const temp = new List({ id: this.nextSequence(), library: this });
+	this.lists.push(temp);
+	this.idMap[temp.id] = temp;
+	if (!this.defaultListId) this.defaultListId = temp.id;
+	return temp;
+};
+
+Library.prototype.removeList = function (id) {
+	if (Object.size(this.lists) == 1) return;
+	const list = this.getListById(id);
+
+	for (var i = 0; i < list.categoryIds; i++) {
+		this.removeCategory(list.categoryIds[i], true);
+	}
+
+	this.lists.splice(this.lists.indexOf(list), 1);
+	delete this.idMap[id];
+
+	if (this.defaultListId == id) {
+		let newId = -1;
+		for (var i in lists) {
+			newId = i;
+			break;
+		}
+		this.defaultListId = newId;
+	}
+};
+
+Library.prototype.copyList = function (id) {
+	const oldList = this.getListById(id);
+	if (!oldList) return;
+
+	const copiedList = this.newList();
+
+	copiedList.name = `Copy of ${oldList.name}`;
+	for (const i in oldList.categoryIds) {
+		const oldCategory = this.getCategoryById(oldList.categoryIds[i]);
+		const copiedCategory = this.newCategory({ list: copiedList });
+
+		copiedCategory.name = oldCategory.name;
+
+		for (const j in oldCategory.categoryItems) {
+			copiedCategory.addItem(oldCategory.categoryItems[j]);
+		}
+	}
+
+	return copiedList;
+};
+
+Library.prototype.renderChart = function (type) {
+	return this.getListById(this.defaultListId).renderChart(type);
+};
+
+Library.prototype.getCategoryById = function (id) {
+	return this.idMap[id];
+};
+
+Library.prototype.getItemById = function (id) {
+	return this.idMap[id];
+};
+
+Library.prototype.getListById = function (id) {
+	return this.idMap[id];
+};
+
+Library.prototype.getItemsInCurrentList = function () {
+	const out = [];
+	const list = this.getListById(this.defaultListId);
+	for (let i = 0; i < list.categoryIds.length; i++) {
+		const category = this.getCategoryById(list.categoryIds[i]);
+		if (category) {
+			for (const j in category.categoryItems) {
+				const categoryItem = category.categoryItems[j];
+				out.push(categoryItem.itemId);
+			}
+		}
+	}
+	return out;
+};
+
+Library.prototype.findCategoryWithItemById = function (itemId, listId) {
+	if (listId) {
+		const list = this.getListById(listId);
+		for (i in list.categoryIds) {
+			var category = this.getCategoryById(list.categoryIds[i]);
+			if (category) {
+				for (var j in category.categoryItems) {
+					var categoryItem = category.categoryItems[j];
+					if (categoryItem.itemId == itemId) return category;
+				}
+			}
+		}
+	} else {
+		for (var i in this.categories) {
+			var category = this.categories[i];
+			if (category) {
+				for (var j in category.categoryItems) {
+					var categoryItem = category.categoryItems[j];
+					if (categoryItem.itemId == itemId) return category;
+				}
+			}
+		}
+	}
+};
+
+Library.prototype.findListWithCategoryById = function (id) {
+	for (const i in this.lists) {
+		const list = this.lists[i];
+		for (const j in list.categoryIds) {
+			if (list.categoryIds[j] == id) return list;
+		}
+	}
+};
+
+Library.prototype.nextSequence = function () {
+	return ++this.sequence;
+};
+
+Library.prototype.save = function () {
+	const out = {};
+
+	out.version = this.version;
+	out.totalUnit = this.totalUnit;
+	out.itemUnit = this.itemUnit;
+	out.defaultListId = this.defaultListId;
+	out.sequence = this.sequence;
+	out.showSidebar = this.showSidebar;
+	out.optionalFields = this.optionalFields;
+	out.currencySymbol = this.currencySymbol;
+
+	out.items = [];
+	for (var i in this.items) {
+		out.items.push(this.items[i].save());
+	}
+
+	out.categories = [];
+	for (var i in this.categories) {
+		out.categories.push(this.categories[i].save());
+	}
+
+	out.lists = [];
+	for (var i in this.lists) {
+		out.lists.push(this.lists[i].save());
+	}
+
+	return out;
+};
+
+Library.prototype.load = function (serializedLibrary) {
+	// upgrades should update "serializedLibrary" in-place instead of modifying "this"
+	if (serializedLibrary.version === '0.1' || !serializedLibrary.version) {
+		this.upgrade01to02(serializedLibrary);
+	}
+	if (serializedLibrary.version === '0.2') {
+		this.upgrade02to03(serializedLibrary);
+	}
+
+	this.items = [];
+
+	assignIn(this.optionalFields, serializedLibrary.optionalFields);
+
+	for (var i in serializedLibrary.items) {
+		var temp = new Item({ id: serializedLibrary.items[i].id });
+		temp.load(serializedLibrary.items[i]);
+		this.items.push(temp);
+		this.idMap[temp.id] = temp;
+	}
+
+	this.categories = [];
+	for (var i in serializedLibrary.categories) {
+		var temp = new Category({ id: serializedLibrary.categories[i].id, library: this });
+		temp.load(serializedLibrary.categories[i]);
+		this.categories.push(temp);
+		this.idMap[temp.id] = temp;
+	}
+
+	this.lists = [];
+	for (var i in serializedLibrary.lists) {
+		var temp = new List({ id: serializedLibrary.lists[i].id, library: this });
+		temp.load(serializedLibrary.lists[i]);
+		this.lists.push(temp);
+		this.idMap[temp.id] = temp;
+	}
+
+	if (serializedLibrary.showSidebar) this.showSidebar = serializedLibrary.showSidebar;
+	if (serializedLibrary.totalUnit) this.totalUnit = serializedLibrary.totalUnit;
+	if (serializedLibrary.itemUnit) this.itemUnit = serializedLibrary.itemUnit;
+	if (serializedLibrary.currencySymbol) this.currencySymbol = serializedLibrary.currencySymbol;
+
+	this.version = serializedLibrary.version;
+	this.sequence = serializedLibrary.sequence;
+	this.defaultListId = serializedLibrary.defaultListId;
+};
+
+Library.prototype.upgrade01to02 = function (serializedLibrary) {
+	if (!serializedLibrary.optionalFields) {
+		serializedLibrary.optionalFields = assignIn({}, defaultOptionalFields);
+	}
+
+	if (serializedLibrary.showImages) {
+		serializedLibrary.optionalFields.images = true;
+	} else {
+		serializedLibrary.optionalFields.images = false;
+	}
+	serializedLibrary.version = '0.2';
+};
+
+Library.prototype.upgrade02to03 = function (serializedLibrary) {
+	this.sequenceShouldBeCorrect(serializedLibrary);
+	this.idsShouldBeInts(serializedLibrary);
+	this.renameCategoryIds(serializedLibrary);
+	this.fixDuplicateIds(serializedLibrary);
+	serializedLibrary.version = '0.3';
+};
+
+Library.prototype.sequenceShouldBeCorrect = function (serializedLibrary) {
+	let sequence = 0;
+
+	serializedLibrary.lists.forEach((list) => {
+		if (list.id > sequence) {
+			sequence = list.id;
+		}
+	});
+
+	serializedLibrary.categories.forEach((category) => {
+		if (category.id > sequence) {
+			sequence = category.id;
+		}
+	});
+
+	serializedLibrary.items.forEach((item) => {
+		if (item.id > sequence) {
+			sequence = item.id;
+		}
+	});
+	serializedLibrary.sequence = (sequence + 1);
+};
+
+Library.prototype.idsShouldBeInts = function (serializedLibrary) {
+	// Some lists of Ids were strings previously. They should be numbers.
+	serializedLibrary.lists.forEach((list) => {
+		list.categoryIds = list.categoryIds.map(categoryId => parseInt(categoryId, 10));
+	});
+};
+
+Library.prototype.renameCategoryIds = function (serializedLibrary) {
+	// categoryIds was previously itemIds. Renaming for clarity.
+	serializedLibrary.categories.forEach((category) => {
+		if (typeof category.itemIds !== 'undefined') {
+			if (!category.categoryItems || category.categoryItems.length === 0) {
+				category.categoryItems = category.itemIds;
+				delete category.itemIds;
+			} else {
+				delete category.itemIds;
+			}
+		}
+		if (typeof category.categoryItems === 'undefined') {
+			category.categoryItems = [];
+		}
+	});
+};
+
+Library.prototype.fixDuplicateIds = function (serializedLibrary) {
+	const foundIds = {};
+
+	serializedLibrary.items.forEach((item) => {
+		if (!foundIds[item.id]) {
+			foundIds[item.id] = [];
+		}
+		foundIds[item.id].push({ type: 'item', item });
+	});
+
+	serializedLibrary.categories.forEach((category) => {
+		if (!foundIds[category.id]) {
+			foundIds[category.id] = [];
+		}
+		foundIds[category.id].push({ type: 'category', category });
+	});
+
+	serializedLibrary.lists.forEach((list) => {
+		if (!foundIds[list.id]) {
+			foundIds[list.id] = [];
+		}
+		foundIds[list.id].push({ type: 'list', list });
+	});
+
+	for (id in foundIds) {
+		if (foundIds[id].length > 1) {
+			const duplicateSet = foundIds[id];
+			duplicateSet.forEach((duplicate, index) => {
+				if (index === 0) {
+					return;
+				}
+				if (duplicate.type === 'item') {
+					this.updateItemId(serializedLibrary, duplicate.item, ++serializedLibrary.sequence);
+				} else if (duplicate.type === 'category') {
+					this.updateCategoryId(serializedLibrary, duplicate.category, ++serializedLibrary.sequence);
+				} else if (duplicate.type === 'list') {
+					this.updateListId(serializedLibrary, duplicate.list, ++serializedLibrary.sequence);
+				}
+			});
+		}
+	}
+};
+
+Library.prototype.updateListId = function (serializedLibrary, list, newId) {
+	list.id = newId;
+};
+Library.prototype.updateCategoryId = function (serializedLibrary, category, newId) {
+	const oldId = category.id;
+
+	category.id = newId;
+
+	serializedLibrary.lists.forEach((list) => {
+		list.categoryIds.forEach((categoryId, index) => {
+			if (categoryId === oldId) {
+				list.categoryIds[index] = newId;
+			}
+		});
+	});
+};
+
+Library.prototype.updateItemId = function (serializedLibrary, item, newId) {
+	const oldId = item.id;
+
+	item.id = newId;
+
+	serializedLibrary.categories.forEach((category) => {
+		category.categoryItems.forEach((categoryItem) => {
+			if (categoryItem.itemId === oldId) {
+				categoryItem.itemId = newId;
+			}
+		});
+	});
+};
+
+Object.size = function (obj) {
+	let size = 0; let
+		key;
+	for (key in obj) {
+		if (obj.hasOwnProperty(key)) size++;
+	}
+	return size;
+};
+
+export default {
+	Library,
+	List,
+	Category,
+	Item,
+};
diff --git a/src/store.js b/src/store.js
@@ -0,0 +1,350 @@
+import debounce from 'lodash/debounce.js';
+import Vuex     from 'vuex';
+import Vue      from 'vue';
+
+import weightUtils from './utils/weight.js';
+import dataTypes   from './dataTypes.js';
+
+const Library  = dataTypes.Library;
+const List     = dataTypes.List;
+const Category = dataTypes.Category;
+const Item     = dataTypes.Item;
+
+const saveInterval = 10000;
+
+Vue.use(Vuex);
+
+const store = new Vuex.Store({
+	state: {
+		library: false,
+		isSaving: false,
+		syncToken: false,
+		lastSaveData: null,
+		loggedIn: false,
+		directiveInstances: {},
+		globalAlerts: [],
+	},
+	getters: {
+		activeList(state) {
+			return state.library.getListById(state.library.defaultListId);
+		},
+	},
+	mutations: {
+		setSyncToken(state, syncToken) {
+			state.syncToken = syncToken;
+		},
+		setLastSaveData(state, lastSaveData) {
+			state.lastSaveData = lastSaveData;
+		},
+		setIsSaving(state, isSaving) {
+			state.isSaving = isSaving;
+		},
+		logout(state) {
+			document.cookie = 'lp=; path=/; domain='+window.location.hostname+'; Max-Age=-99999999; SameSite=none; Secure=true;';  ;
+			state.library = false; // duplicate logic
+			state.loggedIn = false; // duplicate logic
+		},
+		setLoggedIn(state, loggedIn) {
+			state.loggedIn = loggedIn;
+		},
+		loadLibraryData(state, libraryData) {
+			const library = new Library();
+			try {
+				libraryData = JSON.parse(libraryData);
+				library.load(libraryData);
+				state.library = library;
+			} catch (err) {
+				state.globalAlerts.push({ message: 'An error occurred while loading your data.' });
+			}
+			state.lastSaveData = JSON.stringify(library.save());
+		},
+		clearLibraryData(state) {
+			state.library = false;
+		},
+		toggleSidebar(state) {
+			state.library.showSidebar = !state.library.showSidebar;
+		},
+		setDefaultList(state, list) {
+			state.library.defaultListId = list.id;
+			state.library.getListById(state.library.defaultListId).calculateTotals();
+		},
+		setTotalUnit(state, unit) {
+			state.library.totalUnit = unit;
+		},
+		toggleOptionalField(state, optionalField) {
+			state.library.optionalFields[optionalField] = !state.library.optionalFields[optionalField];
+			state.library.getListById(state.library.defaultListId).calculateTotals();
+		},
+		updateCurrencySymbol(state, currencySymbol) {
+			state.library.currencySymbol = currencySymbol;
+		},
+		newItem(state, { category, _isNew }) {
+			state.library.newItem({ category, _isNew });
+			state.library.getListById(state.library.defaultListId).calculateTotals();
+		},
+		newCategory(state, list) {
+			const category = state.library.newCategory({ list, _isNew: true });
+			state.library.getListById(state.library.defaultListId).calculateTotals();
+		},
+		newList(state) {
+			const list = state.library.newList();
+			const category = state.library.newCategory({ list });
+			list.calculateTotals();
+			state.library.defaultListId = list.id;
+		},
+		removeItem(state, item) {
+			state.library.removeItem(item.id);
+			state.library.getListById(state.library.defaultListId).calculateTotals();
+		},
+		removeCategory(state, category) {
+			state.library.removeCategory(category.id);
+		},
+		removeList(state, list) {
+			state.library.removeList(list.id);
+		},
+		reorderList(state, args) {
+			state.library.lists = arrayMove(state.library.lists, args.before, args.after);
+		},
+		reorderCategory(state, args) {
+			const list = state.library.getListById(args.list.id);
+			list.categoryIds = arrayMove(list.categoryIds, args.before, args.after);
+			state.library.getListById(state.library.defaultListId).calculateTotals();
+		},
+		reorderItem(state, args) {
+			const item = state.library.getItemById(args.itemId);
+			const dropCategory = state.library.getCategoryById(args.categoryId);
+			const list = state.library.getListById(args.list.id);
+			const originalCategory = state.library.findCategoryWithItemById(item.id, list.id);
+			const oldCategoryItem = originalCategory.getCategoryItemById(item.id);
+			const oldIndex = originalCategory.categoryItems.indexOf(oldCategoryItem);
+
+			if (originalCategory === dropCategory) {
+				dropCategory.categoryItems = arrayMove(dropCategory.categoryItems, oldIndex, args.dropIndex);
+			} else {
+				originalCategory.categoryItems.splice(oldIndex, 1);
+				dropCategory.categoryItems.splice(args.dropIndex, 0, oldCategoryItem);
+			}
+			state.library.getListById(state.library.defaultListId).calculateTotals();
+		},
+		addItemToCategory(state, args) {
+			const item = state.library.getItemById(args.itemId);
+			const dropCategory = state.library.getCategoryById(args.categoryId);
+
+			if (item && dropCategory) {
+				dropCategory.addItem({ itemId: item.id });
+				const categoryItem = dropCategory.getCategoryItemById(item.id);
+				const categoryItemIndex = dropCategory.categoryItems.indexOf(categoryItem);
+				if (categoryItem && categoryItemIndex !== -1) {
+					dropCategory.categoryItems = arrayMove(dropCategory.categoryItems, categoryItemIndex, args.dropIndex);
+				}
+				state.library.getListById(state.library.defaultListId).calculateTotals();
+			}
+		},
+		updateListName(state, updatedList) {
+			const list = state.library.getListById(updatedList.id);
+			list.name = updatedList.name;
+		},
+		updateListDescription(state, updatedList) {
+			const list = state.library.getListById(updatedList.id);
+			list.description = updatedList.description;
+		},
+		setExternalId(state, args) {
+			const list = state.library.getListById(args.list.id);
+			list.externalId = args.externalId;
+		},
+		updateCategoryName(state, updatedCategory) {
+			const category = state.library.getCategoryById(updatedCategory.id);
+			category.name = updatedCategory.name;
+			state.library.getListById(state.library.defaultListId).calculateTotals();
+		},
+		updateCategoryColor(state, updatedCategory) {
+			const category = state.library.getCategoryById(updatedCategory.id);
+			category.color = updatedCategory.color;
+		},
+		updateItem(state, item) {
+			state.library.updateItem(item);
+			state.library.getListById(state.library.defaultListId).calculateTotals();
+		},
+		updateItemLink(state, args) {
+			const item = state.library.getItemById(args.item.id);
+			item.url = args.url;
+		},
+		updateItemImageUrl(state, args) {
+			const item = state.library.getItemById(args.item.id);
+			item.imageUrl = args.imageUrl;
+			state.library.optionalFields.images = true;
+			bus.$emit('optionalFieldChanged');
+		},
+		updateItemImage(state, args) {
+			const item = state.library.getItemById(args.item.id);
+			item.image = args.image;
+			state.library.optionalFields.images = true;
+			bus.$emit('optionalFieldChanged');
+		},
+		updateItemUnit(state, unit) {
+			state.library.itemUnit = unit;
+		},
+		removeItemImage(state, updateItem) {
+			const item = state.library.getItemById(updateItem.id);
+			item.image = '';
+		},
+		updateCategoryItem(state, args) {
+			args.category.updateCategoryItem(args.categoryItem);
+			state.library.getListById(state.library.defaultListId).calculateTotals();
+		},
+		removeItemFromCategory(state, args) {
+			args.category.removeItem(args.itemId);
+			state.library.getListById(state.library.defaultListId).calculateTotals();
+		},
+		copyList(state, listId) {
+			const copiedList = state.library.copyList(listId);
+			state.library.defaultListId = copiedList.id;
+		},
+		importCSV(state, importData) {
+			const list = state.library.newList({});
+			let category;
+			const newCategories = {};
+			let item;
+			let categoryItem;
+			let row;
+			let i;
+
+			list.name = importData.name;
+
+			for (i in importData.data) {
+				row = importData.data[i];
+				if (newCategories[row.category]) {
+					category = newCategories[row.category];
+				} else {
+					category = state.library.newCategory({ list });
+					newCategories[row.category] = category;
+				}
+
+				item = state.library.newItem({ category, _isNew: false });
+				categoryItem = category.getCategoryItemById(item.id);
+
+				item.name = row.name;
+				item.description = row.description;
+				categoryItem.qty = parseFloat(row.qty);
+				item.weight = weightUtils.WeightToMg(parseFloat(row.weight), row.unit);
+				item.authorUnit = row.unit;
+				category.name = row.category;
+			}
+			list.calculateTotals();
+			state.library.defaultListId = list.id;
+		},
+		save() {
+			// no-op
+		},
+		addDirectiveInstance(state, { key, value }) {
+			state.directiveInstances[key] = value;
+		},
+		removeDirectiveInstance(state, key) {
+			delete state.directiveInstances[key];
+		},
+	},
+	actions: {
+		init(context) {
+			if (readCookie('lp')) {
+				return context.dispatch('loadRemote');
+			}
+
+			return new Promise((resolve, reject) => {
+				context.commit('setLoggedIn', false);
+				context.commit('clearLibraryData');
+				resolve();
+			});
+		},
+		loadRemote(context) {
+			return fetchJson('/getLibrary', {
+				method: 'POST',
+				headers: {
+					'Content-Type': 'application/json',
+				},
+				credentials: 'same-origin',
+			})
+				.then((response) => {
+					context.commit('setSyncToken', response.syncToken);
+					if (response.library !== "") {
+						context.commit('loadLibraryData', response.library);
+					} else {
+						let freshLibrary = JSON.stringify(new Library().save());
+						context.commit('loadLibraryData', freshLibrary);
+					}
+				})
+				.catch((response) => {
+					if (response.status == 401) {
+						bus.$emit('unauthorized');
+					} else {
+						return new Promise((resolve, reject) => {
+							reject('An error occurred while fetching your data, please try again later.');
+						});
+					}
+				});
+		},
+	},
+	plugins: [
+		function save(store) {
+			store.subscribe(debounce((mutation, state) => {
+				const ignore = [
+					'setIsSaving',
+					'setSyncToken',
+					'setLastSaveData',
+					'logout',
+					'setLoggedIn',
+					'loadLibraryData',
+					'clearLibraryData',
+				];
+				if (!state.library || ignore.indexOf(mutation.type) > -1) {
+					return;
+				}
+				const saveData = JSON.stringify(state.library.save());
+
+				if (saveData == state.lastSaveData) {
+					return;
+				}
+
+				if (state.isSaving) {
+					setTimeout(() => { store.commit('save', true); }, saveInterval + 1);
+					return;
+				}
+
+				if (!saveData) {
+					saveData = JSON.stringify(state.library.save());
+				}
+
+				store.commit('setIsSaving', true);
+				store.commit('setLastSaveData', saveData);
+
+				return fetchJson('/saveLibrary', {
+					method: 'POST',
+					body: JSON.stringify({ syncToken: state.syncToken, library: saveData }),
+					headers: {
+						'Content-Type': 'application/json',
+					},
+					credentials: 'same-origin',
+				})
+					.then((response) => {
+						bus.$emit('lastSyncDate', new Date().toLocaleString());
+						store.commit('setSyncToken', response.syncToken);
+						store.commit('setIsSaving', false);
+					})
+					.catch((response) => {
+						store.commit('setIsSaving', false);
+						let error = 'An error occurred while attempting to save your data.';
+						if (response.json && response.json.status) {
+							error = response.json.status;
+						}
+						if (response.status == 401) {
+							bus.$emit('unauthorized', error);
+						} else {
+							alert(error); // TODO
+						}
+					});
+
+			}, saveInterval, { maxWait: saveInterval * 3 }));
+		},
+	],
+});
+
+export default store;
diff --git a/src/utils/color.js b/src/utils/color.js
@@ -0,0 +1,120 @@
+const getColor = (index, baseColor) => {
+	if (baseColor) {
+		const hsv = rgbToHsv(baseColor);
+		hsv.s -= Math.round(((hsv.s) / 10) * (index % 10));
+		hsv.v += Math.round(((100 - hsv.v) / 10) * (index % 10));
+		return hsvToRgb(hsv);
+	}
+	// colors = [{r:57, g:142, b:221}, {r:251, g:51, b:74}, {r:248, g:202, b:0}, {r:174, g:226, b:57}, {r:184, g:51, b:222}, {r:255, g:142, b:50}, {r:220, g:242, b:51}, {r:86, g:174, b:226}, {r:226, g:86, b:174}, {r:226, g:137, b:86}, {r:86, g:226, b:207}];
+	const colors = [{ r: 27, g: 119, b: 211 }, { r: 206, g: 24, b: 54 }, { r: 242, g: 208, b: 0 }, { r: 122, g: 179, b: 23 }, { r: 130, g: 33, b: 198 }, { r: 232, g: 110, b: 28 }, { r: 220, g: 242, b: 51 }, { r: 86, g: 174, b: 226 }, { r: 226, g: 86, b: 174 }, { r: 226, g: 137, b: 86 }, { r: 86, g: 226, b: 207 }];
+	return colors[index % colors.length];
+}
+
+const hsvToRgb = (hsv) => {
+	let r; let g; let b; let i; let f; let p; let q; let
+		t;
+	const h = hsv.h / 360;
+	const s = hsv.s / 100;
+	const v = hsv.v / 100;
+
+	i = Math.floor(h * 6);
+	f = h * 6 - i;
+	p = v * (1 - s);
+	q = v * (1 - f * s);
+	t = v * (1 - (1 - f) * s);
+	switch (i % 6) {
+	case 0: r = v, g = t, b = p; break;
+	case 1: r = q, g = v, b = p; break;
+	case 2: r = p, g = v, b = t; break;
+	case 3: r = p, g = q, b = v; break;
+	case 4: r = t, g = p, b = v; break;
+	case 5: r = v, g = p, b = q; break;
+	}
+	return {
+		r: Math.floor(r * 255),
+		g: Math.floor(g * 255),
+		b: Math.floor(b * 255),
+	};
+}
+
+const rgbToHsv = (rgb) => {
+	let rr; let gg; let bb;
+	const r = rgb.r / 255;
+	const g = rgb.g / 255;
+	const b = rgb.b / 255;
+	let h; let s;
+	const v = Math.max(r, g, b);
+	const diff = v - Math.min(r, g, b);
+	const diffc = function (c) {
+		return (v - c) / 6 / diff + 1 / 2;
+	};
+
+	if (diff == 0) {
+		h = s = 0;
+	} else {
+		s = diff / v;
+		rr = diffc(r);
+		gg = diffc(g);
+		bb = diffc(b);
+
+		if (r === v) {
+			h = bb - gg;
+		} else if (g === v) {
+			h = (1 / 3) + rr - bb;
+		} else if (b === v) {
+			h = (2 / 3) + gg - rr;
+		}
+		if (h < 0) {
+			h += 1;
+		} else if (h > 1) {
+			h -= 1;
+		}
+	}
+	return {
+		h: Math.round(h * 360),
+		s: Math.round(s * 100),
+		v: Math.round(v * 100),
+	};
+}
+
+const rgbToString = (rgb) => {
+	return `rgb(${rgb.r},${rgb.g},${rgb.b})`;
+}
+
+const stringToRgb = (rgbString) => {
+	rgbString = rgbString.substring(4, rgbString.length - 1);
+	const split = rgbString.split(',');
+	return {
+		r: parseInt(split[0]),
+		g: parseInt(split[1]),
+		b: parseInt(split[2]),
+	};
+}
+
+const hexToRgb = (hex) => {
+	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+	return result ? {
+		r: parseInt(result[1], 16),
+		g: parseInt(result[2], 16),
+		b: parseInt(result[3], 16),
+	} : null;
+}
+
+const rgbToHex = (rgb) => {
+	return `#${componentToHex(rgb.r)}${componentToHex(rgb.g)}${componentToHex(rgb.b)}`;
+}
+
+const componentToHex = (c) => {
+	const hex = c.toString(16);
+	return hex.length == 1 ? `0${hex}` : hex;
+}
+
+export default {
+	getColor,
+	hsvToRgb,
+	rgbToHsv,
+	rgbToString,
+	stringToRgb,
+	hexToRgb,
+	rgbToHex,
+};
diff --git a/src/utils/focus.js b/src/utils/focus.js
@@ -0,0 +1,78 @@
+import Vue      from 'vue';
+import uniqueId from 'lodash/uniqueId.js';
+
+import store    from '../store.js';
+
+Vue.directive('select-on-focus', {
+	inserted(el) {
+		el.addEventListener('focus', (evt) => {
+			el.select();
+		});
+	},
+});
+
+Vue.directive('focus-on-create', {
+	inserted(el, binding) {
+		if (binding.expression && binding.value || !binding.expression) {
+			el.focus();
+		}
+	},
+});
+
+Vue.directive('focus-on-bus', {
+	inserted(el, binding) {
+		bus.$on(binding.value, () => {
+			el.focus();
+		});
+	},
+});
+
+Vue.directive('select-on-bus', {
+	inserted(el, binding) {
+		bus.$on(binding.value, () => {
+			el.select();
+		});
+	},
+});
+
+Vue.directive('empty-if-zero', {
+	inserted(el) {
+		el.addEventListener('focus', (evt) => {
+			if (el.value === '0' || el.value === '0.00') {
+				el.dataset.originalValue = el.value;
+				el.value = '';
+			}
+		});
+
+		el.addEventListener('blur', (evt) => {
+			if (el.value === '') {
+				el.value = el.dataset.originalValue || '0';
+			}
+		});
+	},
+});
+
+Vue.directive('click-outside', {
+	inserted(el, binding) {
+		const handler = (evt) => {
+			if (el.contains(evt.target)) {
+				return;
+			}
+			if (binding && typeof binding.value === 'function') {
+				binding.value();
+			}
+		};
+
+		window.addEventListener('click', handler);
+
+		// Store handler to clean up later
+		el.dataset.clickoutside = uniqueId();
+		store.commit('addDirectiveInstance', { key: el.dataset.clickoutside, value: handler });
+	},
+	unbind(el) {
+		// clean up event handlers
+		const handler = store.state.directiveInstances[el.dataset.clickoutside];
+		store.commit('removeDirectiveInstance', el.dataset.clickoutside);
+		window.removeEventListener('click', handler);
+	},
+});
diff --git a/src/utils/mixin.js b/src/utils/mixin.js
@@ -0,0 +1,23 @@
+import weightUtils from './weight.js';
+
+export default {
+	data() {
+		return {
+
+		};
+	},
+	methods: {
+	},
+	filters: {
+		displayWeight(mg, unit) {
+			return weightUtils.MgToWeight(mg, unit) || 0;
+		},
+		displayPrice(price, symbol) {
+			let amount = '0.00';
+			if (typeof price === 'number') {
+				amount = price.toFixed(2);
+			}
+			return amount+' '+symbol;
+		},
+	},
+};
diff --git a/src/utils/utils.js b/src/utils/utils.js
@@ -0,0 +1,124 @@
+import assignIn from 'lodash/assignIn.js';
+
+class lpError extends Error {
+	constructor(response, statusCode = null) {
+		super();
+
+		this.message = 'An error occurred, please try again later.';
+		this.statusCode = statusCode;
+		this.errors = null;
+		this.id = null;
+		this.metadata = null;
+
+		if (response.message) {
+			this.message = response.message;
+		} else if (response.errors && response.errors instanceof Array && response.errors.length && response.errors[0].message) {
+			this.message = response.errors[0].message;
+		}
+
+		if (response.errors) {
+			this.errors = response.errors;
+		}
+	}
+}
+
+window.fetchJson = (url, options) => {
+	const fetchOptions = {
+		method: 'GET',
+		headers: {}
+	};
+
+	if (options) {
+		assignIn(fetchOptions, options);
+	}
+
+	if (!fetchOptions.body && !fetchOptions.headers['Content-Type']) {
+		fetchOptions.headers['Content-Type'] = 'application/json';
+	}
+
+	function parseJSON(response) {
+		return new Promise((resolve, reject) => {
+			response
+				.text()
+				.then((text) => {
+					let json;
+
+					try {
+						json = text ? JSON.parse(text) : {};
+					} catch (err) {
+						json = { message: response };
+					}
+
+					return resolve({
+						status: response.status,
+						ok: response.ok,
+						json,
+					});
+				})
+				.catch(err => reject(err));
+		});
+	}
+
+	return new Promise((resolve, reject) => {
+		fetch(url, fetchOptions)
+			.then(parseJSON)
+			.then((response) => {
+				if (response.ok) {
+					return resolve(response.json);
+				}
+				if (response.status && (response.status === 401 || response.status === 403)) {
+					bus.$emit('unauthorized');
+					return;
+				}
+
+				if (response.json) {
+					return reject(new lpError(response.json, response.status));
+				}
+
+				return reject(new lpError(response));
+			})
+			.catch((err) => {
+				if (err && err instanceof TypeError && err.message === 'Failed to fetch') {
+					err = {};
+				}
+
+				return reject(new lpError(err));
+			});
+	});
+};
+
+window.readCookie = function (name) {
+	const nameEQ = `${name}=`;
+	const ca = document.cookie.split(';');
+	for (let i = 0; i < ca.length; i++) {
+		let c = ca[i];
+		while (c.charAt(0) == ' ') c = c.substring(1, c.length);
+		if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
+	}
+	return null;
+};
+
+window.createCookie = function (name, value, days) {
+	if (days) {
+		const date = new Date();
+		date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
+		var expires = `; expires=${date.toGMTString()}`;
+	} else var expires = '';
+	document.cookie = `${name}=${value}${expires}; path=/`;
+};
+
+window.getElementIndex = function (node) {
+	let index = 0;
+	while ((node = node.previousElementSibling)) {
+		index++;
+	}
+	return index;
+};
+
+window.arrayMove = function (inputArray, oldIndex, newIndex) {
+	const array = inputArray.slice();
+	const element = array[oldIndex];
+	array.splice(oldIndex, 1);
+	array.splice(newIndex, 0, element);
+	return array;
+};
diff --git a/src/utils/weight.js b/src/utils/weight.js
@@ -0,0 +1,22 @@
+const  WeightToMg = (value, unit) => {
+	if (unit == 'g') {
+		return value * 1000;
+	} if (unit == 'kg') {
+		return value * 1000000;
+	}
+}
+
+const MgToWeight = (value, unit, display) => {
+	if (typeof display === 'undefined') display = false;
+
+	if (unit == 'g') {
+		return Math.round(100 * value / 1000.0) / 100;
+	} if (unit == 'kg') {
+		return Math.round(100 * value / 1000000.0, 2) / 100;
+	}
+}
+
+export default {
+		WeightToMg,
+		MgToWeight,
+};
diff --git a/src/view.js b/src/view.js
@@ -0,0 +1,103 @@
+import chart from './chart.js';
+
+window.addEventListener('DOMContentLoaded', (event) => {
+	const mgToWeight = (value, unit) => {
+		if (unit == 'g') {
+			return Math.round(100 * value / 1000.0) / 100;
+		} if (unit == 'kg') {
+			return Math.round(100 * value / 1000000.0, 2) / 100;
+		}
+	};
+
+	const addParents = (chartData, parent) => {
+		if (parent) chartData.parent = parent;
+		for (const i in chartData.points) {
+			addParents(chartData.points[i], chartData);
+		}
+	}
+
+	document.querySelector('.lpList').querySelectorAll('.lpUnitSelect').forEach((element) => {
+		element.addEventListener('click', (event) => {
+			event.stopPropagation();
+
+			element.classList.toggle('lpOpen');
+			const value = element.querySelector('.lpUnit').value;
+			element.getElementsByTagName('ul')[0].classList.remove('g');
+			element.getElementsByTagName('ul')[0].classList.remove('kg');
+			element.getElementsByTagName('ul')[0].classList.add(value);
+		});
+	});
+
+	document.querySelector('.lpList').querySelectorAll('.lpUnitSelect li').forEach((element) => {
+		element.addEventListener('click', (event) => {
+			const unitSelect = element.parentNode.parentNode
+			const unit       = element.textContent;
+
+			unitSelect.querySelector('.lpDisplay').textContent = unit;
+			unitSelect.querySelector('.lpUnit').value          = unit;
+
+			if ( unitSelect.parentNode.className === "lpTotalUnit" ) {
+				unitSelect.parentNode.parentNode.querySelector('.lpTotalValue').textContent = mgToWeight(parseFloat(unitSelect.querySelector('.lpMG').value), unit);
+				document.querySelectorAll('.lpDisplaySubtotal').forEach((element) => {
+					element.nextElementSibling.innerHTML = unit;
+					element.innerHTML                    = mgToWeight(element.getAttribute('mg'), unit);
+				});
+			} else {
+				document.querySelectorAll('.lpWeight').forEach((element) => {
+					const weightCell = element.parentNode;
+
+					element.textContent = mgToWeight(parseFloat(weightCell.querySelector('.lpMG').value), unit);
+					weightCell.querySelector('.lpDisplay').textContent = unit;
+				});
+			}
+		});
+	});
+
+	document.querySelector('.lpCategories').querySelectorAll('.lpItemImage').forEach((element) => {
+		element.addEventListener('click', (event) => {
+			const imageModal = document.getElementById('lpImageDialog');
+			const image      = document.createElement('img');
+			image.setAttribute('src', element.getAttribute('href'));
+
+			while (imageModal.lastChild) {
+				imageModal.removeChild(imageModal.lastChild);
+			}
+
+			imageModal.append(image);
+
+			image.addEventListener('load', () => {
+				imageModal.style.display = 'block';
+				document.getElementById('modalOverlay').style.display = 'block';
+			});
+		});
+	});
+
+	document.getElementById('modalOverlay').addEventListener('click', () => {
+		document.getElementById('modalOverlay').style.display = 'none';;
+		document.getElementById('lpImageDialog').style.display = 'none';;
+	});
+
+	document.addEventListener('click', () => {
+		document.querySelectorAll('.lpOpen').forEach((element) => {
+			element.classList.remove('lpOpen');
+		});
+	});
+
+	if (typeof chartData !== "undefined") {
+		chartData = JSON.parse(unescape(chartData));
+		addParents(chartData, false);
+		chart({
+			processedData: chartData,
+			container:     document.getElementById('chartContainer'),
+			hoverCallback: (chartItem) => {
+				document.querySelectorAll('.hover').forEach((element) => {
+					element.classList.remove('hover');
+				});
+			
+				if (chartItem && chartItem.id) {
+					document.getElementById(`total_${chartItem.id}`).classList.add('hover');
+				}
+			}
+		});
+	}
+});
diff --git a/src/views/editor.vue b/src/views/editor.vue
@@ -0,0 +1,179 @@
+<style lang="scss">
+@import "../css/_globals";
+
+#header {
+	align-items: baseline;
+	display: flex;
+	height: 60px;
+	margin: 0 -20px 20px; /* lpList padding */
+	position: relative;
+}
+
+#hamburger {
+	cursor: pointer;
+	display: inline-block;
+	opacity: 0.6;
+	transition: transform $transitionDurationSlow;
+
+	&:hover {
+		opacity: 1;
+	}
+
+	.lpHasSidebar & {
+		transform: rotate(90deg);
+	}
+}
+
+#lpListName {
+	font-size: 24px;
+	font-weight: 600;
+	padding: 12px 15px;
+}
+
+.headerItem {
+	flex: 0 0 auto;
+	height: 100%;
+	padding: 17px 16px;
+	position: relative;
+
+	&:first-child {
+		padding-left: 20px;
+	}
+
+	.lpPopover {
+		&:hover .lpTarget {
+			color: $blue1;
+		}
+	}
+
+	.lpTarget {
+		font-weight: 600;
+		padding: 17px 16px 15px;
+	}
+
+	&#lpListName {
+		flex: 1 0 auto;
+	}
+
+	&.hasPopover {
+		padding: 0;
+	}
+
+	&.signInRegisterButtons {
+		height: auto;
+		padding: 0 16px;
+	}
+}
+</style>
+
+<template>
+	<div v-if="isLoaded" id="main" :class="{lpHasSidebar: library.showSidebar}">
+		<sidebar />
+		<div class="lpList lpTransition">
+			<div id="header" class="clearfix">
+				<span class="headerItem">
+					<a id="hamburger" class="lpTransition" @click="toggleSidebar"><i class="lpSprite lpHamburger" /></a>
+				</span>
+				<input id="lpListName" :value="list.name" type="text" class="lpListName lpSilent headerItem" value="New List" placeholder="List Name" autocomplete="off" name="lastpass-disable-search" @input="updateListName">
+				<span>{{ lastSync }}</span>
+				<share />
+				<listSettings />
+				<moreDropdown />
+				<span class="clearfix" />
+			</div>
+
+			<list />
+
+			<div id="lpFooter">
+				<div class="lpSiteBy">
+					This frontend is based on the work of <a class="lpHref" href="https://github.com/galenmaly/lighterpack" target="_blank" rel="noopener noreferrer">Lighterpack</a>.
+				</div>
+			</div>
+		</div>
+
+		<globalAlerts />
+		<speedbump />
+		<copyList />
+		<importCsv />
+		<itemImage />
+		<itemViewImage />
+		<itemLink />
+		<help />
+		<changePassword />
+	</div>
+</template>
+
+<script>
+import globalAlerts    from '../components/globalAlerts.vue';
+import sidebar         from '../components/sidebar.vue';
+import share           from '../components/share.vue';
+import listSettings    from '../components/listSettings.vue';
+import moreDropdown    from '../components/moreDropdown.vue';
+import changePassword  from '../components/changePassword.vue';
+import help            from '../components/help.vue';
+import list            from '../components/list.vue';
+import itemImage       from '../components/itemImage.vue';
+import itemViewImage   from '../components/itemViewImage.vue';
+import itemLink        from '../components/itemLink.vue';
+import importCsv       from '../components/importCsv.vue';
+import copyList        from '../components/copyList.vue';
+import speedbump       from '../components/speedbump.vue';
+
+export default {
+	name: 'Dashboard',
+	components: {
+		sidebar,
+		share,
+		listSettings,
+		moreDropdown,
+		changePassword,
+		help,
+		list,
+		itemLink,
+		copyList,
+		importCsv,
+		itemImage,
+		itemViewImage,
+		speedbump,
+		globalAlerts,
+	},
+	mixins: [],
+	data() {
+		return {
+			isLoaded: false,
+			lastSync: '',
+		};
+	},
+	computed: {
+		library() {
+			return this.$store.state.library;
+		},
+		list() {
+			return this.library.getListById(this.library.defaultListId);
+		},
+		isSignedIn() {
+			return this.$store.state.loggedIn;
+		},
+	},
+	created() {
+		bus.$on('lastSyncDate', (lastSyncDate) => {
+			this.lastSync = 'Last sync: ' + lastSyncDate;
+		});
+	},
+	beforeMount() {
+		if (!this.$store.state.library) {
+			router.push('/login');
+		} else {
+			this.isLoaded = true;
+		}
+	},
+	methods: {
+		toggleSidebar() {
+			this.$store.commit('toggleSidebar');
+		},
+		updateListName(evt) {
+			this.$store.commit('updateListName', { id: this.list.id, name: evt.target.value });
+		},
+	},
+};
+</script>
diff --git a/src/views/login.vue b/src/views/login.vue
@@ -0,0 +1,110 @@
+<style lang="scss">
+</style>
+
+<template>
+	<div id="loginContainer">
+		<modal id="login" :shown="true">
+			<div class="lpModalHeader">
+				<h2>Password reqired!</h2>
+			</div>
+
+			<form @submit.prevent="login">
+				<p v-if="message" class="lpSuccess">
+					{{ message }}
+				</p>
+				<div class="lpFields">
+					<input v-model="password" v-select-on-bus="'focus-login-password'" type="password" placeholder="Password" name="password" class="password">
+				</div>
+
+				<errors :errors="errors" />
+
+				<div class="lpButtons">
+					<button class="lpButton">
+						Login
+						<spinner v-if="fetching" />
+					</button>
+				</div>
+			</form>
+		</modal>
+
+		<globalAlerts />
+	</div>
+</template>
+
+<script>
+import globalAlerts from '../components/globalAlerts.vue';
+import errors       from '../components/errors.vue';
+import spinner      from '../components/spinner.vue';
+import modal        from '../components/modal.vue';
+
+import dataTypes    from '../dataTypes.js';
+
+const Library = dataTypes.Library;
+
+export default {
+	name: 'Login',
+	components: {
+		errors,
+		spinner,
+		globalAlerts,
+		modal,
+	},
+	props: ['message'],
+	data() {
+		return {
+			fetching: false,
+			errors: [],
+			password: '',
+		};
+	},
+	beforeMount() {
+		if (this.$store.state.library) {
+			router.push('/editor');
+		}
+	},
+	mounted() {
+		bus.$emit('focus-login-password');
+	},
+	methods: {
+		login() {
+			this.errors = [];
+
+			if (!this.password) {
+				this.errors.push({ field: 'password', message: 'Please enter a password.' });
+			}
+
+			if (this.errors.length) {
+				return;
+			}
+
+			this.fetching = true;
+
+			return fetchJson('/getLibrary', {
+				method: 'POST',
+				headers: {
+					'Content-Type': 'application/json',
+				},
+				credentials: 'same-origin',
+				body: JSON.stringify({ password: this.password }),
+			})
+				.then((response) => {
+					this.$store.commit('setSyncToken', response.syncToken);
+					if (response.library !== "") {
+						this.$store.commit('loadLibraryData', response.library);
+					} else {
+						let freshLibrary = JSON.stringify(new Library().save());
+						this.$store.commit('loadLibraryData', freshLibrary);
+					}
+					this.$router.push('/editor');
+					this.fetching = false;
+				})
+				.catch((err) => {
+					this.errors = err;
+					bus.$emit('focus-login-password');
+					this.password = '';
+					this.fetching = false;
+				});
+		},
+	},
+};
+</script>
diff --git a/webpack.config.js b/webpack.config.js
@@ -0,0 +1,71 @@
+import {fileURLToPath}      from 'url';
+import path                 from 'path';
+import webpack              from 'webpack';
+import sass                 from 'sass';
+import MiniCssExtractPlugin from 'mini-css-extract-plugin';
+import VueLoaderPlugin      from 'vue-loader/lib/plugin.js';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname  = path.dirname(__filename);
+
+export default {
+	mode: 'production',
+	entry: {
+		app: [
+			'./src/css/app.scss',
+			'./src/app.js',
+		],
+		view: [
+			'./src/css/view.scss',
+			'./src/view.js',
+		],
+	},
+
+	output: {
+		path: path.resolve(__dirname, './public/dist'),
+		publicPath: '/dist',
+		filename: '[name].js',
+	},
+
+	module: {
+		rules: [
+			{
+				test: /\.vue$/,
+				loader: 'vue-loader',
+			},
+			{
+				test: /\.scss$/,
+				use: [
+					{
+						loader: MiniCssExtractPlugin.loader,
+					},
+					'css-loader',
+					{
+						loader: 'sass-loader',
+						options: {
+							implementation: sass,
+						},
+					},
+				],
+			},
+		],
+	},
+	resolve: {
+		alias: {
+			vue$: 'vue/dist/vue.esm.js',
+		},
+	},
+	performance: {
+		hints: false,
+	},
+	devtool: false,
+	plugins: [
+		new VueLoaderPlugin(),
+		new webpack.LoaderOptionsPlugin({
+			minimize: true,
+		}),
+		new MiniCssExtractPlugin({
+			filename: '[name].css',
+		}),
+	],
+};
diff --git a/yarn.lock b/yarn.lock
@@ -0,0 +1,1352 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@discoveryjs/json-ext@^0.5.0":
+  version "0.5.7"
+  resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
+  integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
+
+"@jridgewell/gen-mapping@^0.3.0":
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
+  integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
+  dependencies:
+    "@jridgewell/set-array" "^1.0.1"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/resolve-uri@3.1.0":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
+  integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
+
+"@jridgewell/set-array@^1.0.1":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
+  integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+
+"@jridgewell/source-map@^0.3.2":
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb"
+  integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==
+  dependencies:
+    "@jridgewell/gen-mapping" "^0.3.0"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10":
+  version "1.4.14"
+  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
+  integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
+
+"@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.9":
+  version "0.3.17"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985"
+  integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==
+  dependencies:
+    "@jridgewell/resolve-uri" "3.1.0"
+    "@jridgewell/sourcemap-codec" "1.4.14"
+
+"@types/eslint-scope@^3.7.0":
+  version "3.7.4"
+  resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
+  integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==
+  dependencies:
+    "@types/eslint" "*"
+    "@types/estree" "*"
+
+"@types/eslint@*":
+  version "8.4.9"
+  resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.9.tgz#f7371980148697f4b582b086630319b55324b5aa"
+  integrity sha512-jFCSo4wJzlHQLCpceUhUnXdrPuCNOjGFMQ8Eg6JXxlz3QaCKOb7eGi2cephQdM4XTYsNej69P9JDJ1zqNIbncQ==
+  dependencies:
+    "@types/estree" "*"
+    "@types/json-schema" "*"
+
+"@types/estree@*":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
+  integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==
+
+"@types/estree@^0.0.45":
+  version "0.0.45"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884"
+  integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==
+
+"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8":
+  version "7.0.11"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
+  integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
+
+"@types/node@*":
+  version "18.11.9"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4"
+  integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==
+
+"@vue/component-compiler-utils@^3.1.0":
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz#f9f5fb53464b0c37b2c8d2f3fbfe44df60f61dc9"
+  integrity sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==
+  dependencies:
+    consolidate "^0.15.1"
+    hash-sum "^1.0.2"
+    lru-cache "^4.1.2"
+    merge-source-map "^1.1.0"
+    postcss "^7.0.36"
+    postcss-selector-parser "^6.0.2"
+    source-map "~0.6.1"
+    vue-template-es2015-compiler "^1.9.0"
+  optionalDependencies:
+    prettier "^1.18.2 || ^2.0.0"
+
+"@webassemblyjs/ast@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.1.tgz#76c6937716d68bf1484c15139f5ed30b9abc8bb4"
+  integrity sha512-uMu1nCWn2Wxyy126LlGqRVlhdTOsO/bsBRI4dNq3+6SiSuRKRQX6ejjKgh82LoGAPSq72lDUiQ4FWVaf0PecYw==
+  dependencies:
+    "@webassemblyjs/helper-module-context" "1.9.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.9.1"
+    "@webassemblyjs/wast-parser" "1.9.1"
+
+"@webassemblyjs/floating-point-hex-parser@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.1.tgz#9eb0ff90a1cdeef51f36ba533ed9f06b5cdadd09"
+  integrity sha512-5VEKu024RySmLKTTBl9q1eO/2K5jk9ZS+2HXDBLA9s9p5IjkaXxWiDb/+b7wSQp6FRdLaH1IVGIfOex58Na2pg==
+
+"@webassemblyjs/helper-api-error@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.1.tgz#ad89015c4246cd7f5ed0556700237f8b9c2c752f"
+  integrity sha512-y1lGmfm38djrScwpeL37rRR9f1D6sM8RhMpvM7CYLzOlHVboouZokXK/G88BpzW0NQBSvCCOnW5BFhten4FPfA==
+
+"@webassemblyjs/helper-buffer@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.1.tgz#186e67ac25f9546ea7939759413987f157524133"
+  integrity sha512-uS6VSgieHbk/m4GSkMU5cqe/5TekdCzQso4revCIEQ3vpGZgqSSExi4jWpTWwDpAHOIAb1Jfrs0gUB9AA4n71w==
+
+"@webassemblyjs/helper-code-frame@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.1.tgz#aab177b7cc87a318a8f8664ad68e2c3828ebc42b"
+  integrity sha512-ZQ2ZT6Evk4DPIfD+92AraGYaFIqGm4U20e7FpXwl7WUo2Pn1mZ1v8VGH8i+Y++IQpxPbQo/UyG0Khs7eInskzA==
+  dependencies:
+    "@webassemblyjs/wast-printer" "1.9.1"
+
+"@webassemblyjs/helper-fsm@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.1.tgz#527e91628e84d13d3573884b3dc4c53a81dcb911"
+  integrity sha512-J32HGpveEqqcKFS0YbgicB0zAlpfIxJa5MjxDxhu3i5ltPcVfY5EPvKQ1suRguFPehxiUs+/hfkwPEXom/l0lw==
+
+"@webassemblyjs/helper-module-context@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.1.tgz#778670b3d471f7cf093d1e7c0dde431b54310e16"
+  integrity sha512-IEH2cMmEQKt7fqelLWB5e/cMdZXf2rST1JIrzWmf4XBt3QTxGdnnLvV4DYoN8pJjOx0VYXsWg+yF16MmJtolZg==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.1"
+
+"@webassemblyjs/helper-wasm-bytecode@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.1.tgz#563f59bcf409ccf469edde168b9426961ffbf6df"
+  integrity sha512-i2rGTBqFUcSXxyjt2K4vm/3kkHwyzG6o427iCjcIKjOqpWH8SEem+xe82jUk1iydJO250/CvE5o7hzNAMZf0dQ==
+
+"@webassemblyjs/helper-wasm-section@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.1.tgz#f7988f94c12b01b99a16120cb01dc099b00e4798"
+  integrity sha512-FetqzjtXZr2d57IECK+aId3D0IcGweeM0CbAnJHkYJkcRTHP+YcMb7Wmc0j21h5UWBpwYGb9dSkK/93SRCTrGg==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.1"
+    "@webassemblyjs/helper-buffer" "1.9.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.9.1"
+    "@webassemblyjs/wasm-gen" "1.9.1"
+
+"@webassemblyjs/ieee754@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.1.tgz#3b715871ca7d75784717cf9ceca9d7b81374b8af"
+  integrity sha512-EvTG9M78zP1MmkBpUjGQHZc26DzPGZSLIPxYHCjQsBMo60Qy2W34qf8z0exRDtxBbRIoiKa5dFyWer/7r1aaSQ==
+  dependencies:
+    "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/leb128@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.1.tgz#b2ecaa39f9e8277cc9c707c1ca8b2aa7b27d0b72"
+  integrity sha512-Oc04ub0vFfLnF+2/+ki3AE+anmW4sv9uNBqb+79fgTaPv6xJsOT0dhphNfL3FrME84CbX/D1T9XT8tjFo0IIiw==
+  dependencies:
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/utf8@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.1.tgz#d02d9daab85cda3211e43caf31dca74c260a73b0"
+  integrity sha512-llkYtppagjCodFjo0alWOUhAkfOiQPQDIc5oA6C9sFAXz7vC9QhZf/f8ijQIX+A9ToM3c9Pq85X0EX7nx9gVhg==
+
+"@webassemblyjs/wasm-edit@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.1.tgz#e27a6bdbf78e5c72fa812a2fc3cbaad7c3e37578"
+  integrity sha512-S2IaD6+x9B2Xi8BCT0eGsrXXd8UxAh2LVJpg1ZMtHXnrDcsTtIX2bDjHi40Hio6Lc62dWHmKdvksI+MClCYbbw==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.1"
+    "@webassemblyjs/helper-buffer" "1.9.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.9.1"
+    "@webassemblyjs/helper-wasm-section" "1.9.1"
+    "@webassemblyjs/wasm-gen" "1.9.1"
+    "@webassemblyjs/wasm-opt" "1.9.1"
+    "@webassemblyjs/wasm-parser" "1.9.1"
+    "@webassemblyjs/wast-printer" "1.9.1"
+
+"@webassemblyjs/wasm-gen@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.1.tgz#56a0787d1fa7994fdc7bea59004e5bec7189c5fc"
+  integrity sha512-bqWI0S4lBQsEN5FTZ35vYzfKUJvtjNnBobB1agCALH30xNk1LToZ7Z8eiaR/Z5iVECTlBndoRQV3F6mbEqE/fg==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.9.1"
+    "@webassemblyjs/ieee754" "1.9.1"
+    "@webassemblyjs/leb128" "1.9.1"
+    "@webassemblyjs/utf8" "1.9.1"
+
+"@webassemblyjs/wasm-opt@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.1.tgz#fbdf8943a825e6dcc4cd69c3e092289fa4aec96c"
+  integrity sha512-gSf7I7YWVXZ5c6XqTEqkZjVs8K1kc1k57vsB6KBQscSagDNbAdxt6MwuJoMjsE1yWY1tsuL+pga268A6u+Fdkg==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.1"
+    "@webassemblyjs/helper-buffer" "1.9.1"
+    "@webassemblyjs/wasm-gen" "1.9.1"
+    "@webassemblyjs/wasm-parser" "1.9.1"
+
+"@webassemblyjs/wasm-parser@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.1.tgz#5e8352a246d3f605312c8e414f7990de55aaedfa"
+  integrity sha512-ImM4N2T1MEIond0MyE3rXvStVxEmivQrDKf/ggfh5pP6EHu3lL/YTAoSrR7shrbKNPpeKpGesW1LIK/L4kqduw==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.1"
+    "@webassemblyjs/helper-api-error" "1.9.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.9.1"
+    "@webassemblyjs/ieee754" "1.9.1"
+    "@webassemblyjs/leb128" "1.9.1"
+    "@webassemblyjs/utf8" "1.9.1"
+
+"@webassemblyjs/wast-parser@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.1.tgz#e25ef13585c060073c1db0d6bd94340fdeee7596"
+  integrity sha512-2xVxejXSvj3ls/o2TR/zI6p28qsGupjHhnHL6URULQRcXmryn3w7G83jQMcT7PHqUfyle65fZtWLukfdLdE7qw==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.1"
+    "@webassemblyjs/floating-point-hex-parser" "1.9.1"
+    "@webassemblyjs/helper-api-error" "1.9.1"
+    "@webassemblyjs/helper-code-frame" "1.9.1"
+    "@webassemblyjs/helper-fsm" "1.9.1"
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/wast-printer@1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.1.tgz#b9f38e93652037d4f3f9c91584635af4191ed7c1"
+  integrity sha512-tDV8V15wm7mmbAH6XvQRU1X+oPGmeOzYsd6h7hlRLz6QpV4Ec/KKxM8OpLtFmQPLCreGxTp+HuxtH4pRIZyL9w==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.1"
+    "@webassemblyjs/wast-parser" "1.9.1"
+    "@xtuc/long" "4.2.2"
+
+"@webpack-cli/configtest@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.2.0.tgz#7b20ce1c12533912c3b217ea68262365fa29a6f5"
+  integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==
+
+"@webpack-cli/info@^1.5.0":
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.5.0.tgz#6c78c13c5874852d6e2dd17f08a41f3fe4c261b1"
+  integrity sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==
+  dependencies:
+    envinfo "^7.7.3"
+
+"@webpack-cli/serve@^1.7.0":
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1"
+  integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==
+
+"@xtuc/ieee754@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
+  integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
+
+"@xtuc/long@4.2.2":
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
+  integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
+
+acorn@^8.0.4, acorn@^8.5.0:
+  version "8.8.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73"
+  integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==
+
+ajv-keywords@^3.5.2:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
+  integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
+
+ajv@^6.12.4, ajv@^6.12.5:
+  version "6.12.6"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+  integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
+anymatch@~3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
+  integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
+atoa@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/atoa/-/atoa-1.0.0.tgz#0cc0e91a480e738f923ebc103676471779b34a49"
+  integrity sha512-VVE1H6cc4ai+ZXo/CRWoJiHXrA1qfA31DPnx6D20+kSI547hQN5Greh51LQ1baMRMfxO5K5M4ImMtZbZt2DODQ==
+
+big.js@^5.2.2:
+  version "5.2.2"
+  resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
+  integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
+
+binary-extensions@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
+bluebird@^3.1.1:
+  version "3.7.2"
+  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
+  integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
+
+braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+browserslist@^4.14.5:
+  version "4.21.4"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987"
+  integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==
+  dependencies:
+    caniuse-lite "^1.0.30001400"
+    electron-to-chromium "^1.4.251"
+    node-releases "^2.0.6"
+    update-browserslist-db "^1.0.9"
+
+buffer-from@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+camelcase@^5.3.1:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+caniuse-lite@^1.0.30001400:
+  version "1.0.30001429"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001429.tgz#70cdae959096756a85713b36dd9cb82e62325639"
+  integrity sha512-511ThLu1hF+5RRRt0zYCf2U2yRr9GPF6m5y90SBCWsvSoYoW7yAGlv/elyPaNfvGCkp6kj/KFZWU0BMA69Prsg==
+
+"chokidar@>=2.0.0 <4.0.0":
+  version "3.5.3"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+  dependencies:
+    anymatch "~3.1.2"
+    braces "~3.0.2"
+    glob-parent "~5.1.2"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.6.0"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+chrome-trace-event@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
+  integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
+
+clone-deep@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
+  integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
+  dependencies:
+    is-plain-object "^2.0.4"
+    kind-of "^6.0.2"
+    shallow-clone "^3.0.0"
+
+colorette@^2.0.14:
+  version "2.0.19"
+  resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
+  integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
+
+commander@^2.20.0:
+  version "2.20.3"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+commander@^7.0.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
+consolidate@^0.15.1:
+  version "0.15.1"
+  resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7"
+  integrity sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==
+  dependencies:
+    bluebird "^3.1.1"
+
+contra@1.9.4:
+  version "1.9.4"
+  resolved "https://registry.yarnpkg.com/contra/-/contra-1.9.4.tgz#f53bde42d7e5b5985cae4d99a8d610526de8f28d"
+  integrity sha512-N9ArHAqwR/lhPq4OdIAwH4e1btn6EIZMAz4TazjnzCiVECcWUPTma+dRAM38ERImEJBh8NiCCpjoQruSZ+agYg==
+  dependencies:
+    atoa "1.0.0"
+    ticky "1.0.1"
+
+cross-spawn@^7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+  dependencies:
+    path-key "^3.1.0"
+    shebang-command "^2.0.0"
+    which "^2.0.1"
+
+crossvent@1.5.5:
+  version "1.5.5"
+  resolved "https://registry.yarnpkg.com/crossvent/-/crossvent-1.5.5.tgz#ad20878e4921e9be73d9d6976f8b2ecd0f71a0b1"
+  integrity sha512-MY4xhBYEnVi+pmTpHCOCsCLYczc0PVtGdPBz6NXNXxikLaUZo4HdAeUb1UqAo3t3yXAloSelTmfxJ+/oUqkW5w==
+  dependencies:
+    custom-event "^1.0.0"
+
+css-loader@3.5.3:
+  version "3.5.3"
+  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.5.3.tgz#95ac16468e1adcd95c844729e0bb167639eb0bcf"
+  integrity sha512-UEr9NH5Lmi7+dguAm+/JSPovNjYbm2k3TK58EiwQHzOHH5Jfq1Y+XoP2bQO6TMn7PptMd0opxxedAWcaSTRKHw==
+  dependencies:
+    camelcase "^5.3.1"
+    cssesc "^3.0.0"
+    icss-utils "^4.1.1"
+    loader-utils "^1.2.3"
+    normalize-path "^3.0.0"
+    postcss "^7.0.27"
+    postcss-modules-extract-imports "^2.0.0"
+    postcss-modules-local-by-default "^3.0.2"
+    postcss-modules-scope "^2.2.0"
+    postcss-modules-values "^3.0.0"
+    postcss-value-parser "^4.0.3"
+    schema-utils "^2.6.6"
+    semver "^6.3.0"
+
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+custom-event@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
+  integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==
+
+de-indent@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
+  integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
+
+dragula@^3.7.2:
+  version "3.7.3"
+  resolved "https://registry.yarnpkg.com/dragula/-/dragula-3.7.3.tgz#909460fd0b4acba5409c6dbb1b64d24f5bc9efb6"
+  integrity sha512-/rRg4zRhcpf81TyDhaHLtXt6sEywdfpv1cRUMeFFy7DuypH2U0WUL0GTdyAQvXegviT4PJK4KuMmOaIDpICseQ==
+  dependencies:
+    contra "1.9.4"
+    crossvent "1.5.5"
+
+electron-to-chromium@^1.4.251:
+  version "1.4.284"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592"
+  integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==
+
+emojis-list@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
+  integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
+
+enhanced-resolve@^5.3.1:
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6"
+  integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==
+  dependencies:
+    graceful-fs "^4.2.4"
+    tapable "^2.2.0"
+
+envinfo@^7.7.3:
+  version "7.8.1"
+  resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475"
+  integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==
+
+escalade@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+eslint-scope@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+  integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
+  dependencies:
+    esrecurse "^4.3.0"
+    estraverse "^4.1.1"
+
+esrecurse@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
+  integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+  dependencies:
+    estraverse "^5.2.0"
+
+estraverse@^4.1.1:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+  integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+estraverse@^5.2.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
+  integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+events@^3.2.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+  integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
+fast-deep-equal@^3.1.1:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-json-stable-stringify@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fastest-levenshtein@^1.0.12:
+  version "1.0.16"
+  resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5"
+  integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+find-up@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+  integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+  dependencies:
+    locate-path "^5.0.0"
+    path-exists "^4.0.0"
+
+find-up@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
+  integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+  dependencies:
+    locate-path "^6.0.0"
+    path-exists "^4.0.0"
+
+fsevents@~2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+glob-parent@~5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
+glob-to-regexp@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
+  integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
+
+graceful-fs@^4.1.2, graceful-fs@^4.2.4:
+  version "4.2.10"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
+  integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
+hash-sum@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04"
+  integrity sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==
+
+he@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+  integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+
+icss-utils@^4.0.0, icss-utils@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467"
+  integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==
+  dependencies:
+    postcss "^7.0.14"
+
+import-local@^3.0.2:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4"
+  integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==
+  dependencies:
+    pkg-dir "^4.2.0"
+    resolve-cwd "^3.0.0"
+
+interpret@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
+  integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==
+
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
+is-core-module@^2.9.0:
+  version "2.11.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144"
+  integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==
+  dependencies:
+    has "^1.0.3"
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-glob@^4.0.1, is-glob@~4.0.1:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-plain-object@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+  integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+  dependencies:
+    isobject "^3.0.1"
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+isobject@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+  integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
+
+jest-worker@^27.4.5:
+  version "27.5.1"
+  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0"
+  integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==
+  dependencies:
+    "@types/node" "*"
+    merge-stream "^2.0.0"
+    supports-color "^8.0.0"
+
+json-parse-better-errors@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+  integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+json-schema-traverse@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json5@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
+  integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
+  dependencies:
+    minimist "^1.2.0"
+
+json5@^2.1.2:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
+  integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
+
+kind-of@^6.0.2:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+  integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+klona@^2.0.4:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc"
+  integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==
+
+loader-runner@^4.1.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1"
+  integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==
+
+loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
+  integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
+  dependencies:
+    big.js "^5.2.2"
+    emojis-list "^3.0.0"
+    json5 "^1.0.1"
+
+loader-utils@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.3.tgz#d4b15b8504c63d1fc3f2ade52d41bc8459d6ede1"
+  integrity sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==
+  dependencies:
+    big.js "^5.2.2"
+    emojis-list "^3.0.0"
+    json5 "^2.1.2"
+
+locate-path@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+  integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+  dependencies:
+    p-locate "^4.1.0"
+
+locate-path@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
+  integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+  dependencies:
+    p-locate "^5.0.0"
+
+lodash@4.17.20:
+  version "4.17.20"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
+  integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
+
+lru-cache@^4.1.2:
+  version "4.1.5"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
+  integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
+  dependencies:
+    pseudomap "^1.0.2"
+    yallist "^2.1.2"
+
+lru-cache@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+  dependencies:
+    yallist "^4.0.0"
+
+merge-source-map@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646"
+  integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==
+  dependencies:
+    source-map "^0.6.1"
+
+merge-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+  integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
+mime-db@1.52.0:
+  version "1.52.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+  integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.27:
+  version "2.1.35"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+  integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+  dependencies:
+    mime-db "1.52.0"
+
+mini-css-extract-plugin@1.3.3:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.3.tgz#7802e62b34199aa7d1a62e654395859a836486a0"
+  integrity sha512-7lvliDSMiuZc81kI+5/qxvn47SCM7BehXex3f2c6l/pR3Goj58IQxZh9nuPQ3AkGQgoETyXuIqLDaO5Oa0TyBw==
+  dependencies:
+    loader-utils "^2.0.0"
+    schema-utils "^3.0.0"
+    webpack-sources "^1.1.0"
+
+minimist@^1.2.0:
+  version "1.2.7"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
+  integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
+
+neo-async@^2.6.2:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
+  integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+
+node-releases@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
+  integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+p-limit@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+  dependencies:
+    p-try "^2.0.0"
+
+p-limit@^3.0.2:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+  integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+  dependencies:
+    yocto-queue "^0.1.0"
+
+p-locate@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+  integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+  dependencies:
+    p-limit "^2.2.0"
+
+p-locate@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
+  integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
+  dependencies:
+    p-limit "^3.0.2"
+
+p-try@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+path-exists@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+  integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-key@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+picocolors@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f"
+  integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==
+
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.0.4, picomatch@^2.2.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+pkg-dir@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+  integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+  dependencies:
+    find-up "^4.0.0"
+
+pkg-dir@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760"
+  integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==
+  dependencies:
+    find-up "^5.0.0"
+
+postcss-modules-extract-imports@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e"
+  integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==
+  dependencies:
+    postcss "^7.0.5"
+
+postcss-modules-local-by-default@^3.0.2:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0"
+  integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==
+  dependencies:
+    icss-utils "^4.1.1"
+    postcss "^7.0.32"
+    postcss-selector-parser "^6.0.2"
+    postcss-value-parser "^4.1.0"
+
+postcss-modules-scope@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee"
+  integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==
+  dependencies:
+    postcss "^7.0.6"
+    postcss-selector-parser "^6.0.0"
+
+postcss-modules-values@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10"
+  integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==
+  dependencies:
+    icss-utils "^4.0.0"
+    postcss "^7.0.6"
+
+postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
+  version "6.0.10"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
+  integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
+  dependencies:
+    cssesc "^3.0.0"
+    util-deprecate "^1.0.2"
+
+postcss-value-parser@^4.0.3, postcss-value-parser@^4.1.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
+  integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
+postcss@^7.0.14, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.36, postcss@^7.0.5, postcss@^7.0.6:
+  version "7.0.39"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309"
+  integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==
+  dependencies:
+    picocolors "^0.2.1"
+    source-map "^0.6.1"
+
+"prettier@^1.18.2 || ^2.0.0":
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
+  integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
+
+pseudomap@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+  integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==
+
+punycode@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+randombytes@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+  integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+  dependencies:
+    safe-buffer "^5.1.0"
+
+readdirp@~3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+  dependencies:
+    picomatch "^2.2.1"
+
+rechoir@^0.7.0:
+  version "0.7.1"
+  resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686"
+  integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==
+  dependencies:
+    resolve "^1.9.0"
+
+resolve-cwd@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
+  integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==
+  dependencies:
+    resolve-from "^5.0.0"
+
+resolve-from@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+  integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
+resolve@^1.9.0:
+  version "1.22.1"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
+  integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
+  dependencies:
+    is-core-module "^2.9.0"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
+
+safe-buffer@^5.1.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+sass-loader@10.1.0:
+  version "10.1.0"
+  resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.1.0.tgz#1727fcc0c32ab3eb197cda61d78adf4e9174a4b3"
+  integrity sha512-ZCKAlczLBbFd3aGAhowpYEy69Te3Z68cg8bnHHl6WnSCvnKpbM6pQrz957HWMa8LKVuhnD9uMplmMAHwGQtHeg==
+  dependencies:
+    klona "^2.0.4"
+    loader-utils "^2.0.0"
+    neo-async "^2.6.2"
+    schema-utils "^3.0.0"
+    semver "^7.3.2"
+
+sass@1.32.0:
+  version "1.32.0"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.0.tgz#10101a026c13080b14e2b374d4e15ee24400a4d3"
+  integrity sha512-fhyqEbMIycQA4blrz/C0pYhv2o4x2y6FYYAH0CshBw3DXh5D5wyERgxw0ptdau1orc/GhNrhF7DFN2etyOCEng==
+  dependencies:
+    chokidar ">=2.0.0 <4.0.0"
+
+schema-utils@^2.6.6:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
+  integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
+  dependencies:
+    "@types/json-schema" "^7.0.5"
+    ajv "^6.12.4"
+    ajv-keywords "^3.5.2"
+
+schema-utils@^3.0.0, schema-utils@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281"
+  integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==
+  dependencies:
+    "@types/json-schema" "^7.0.8"
+    ajv "^6.12.5"
+    ajv-keywords "^3.5.2"
+
+semver@^6.3.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+semver@^7.3.2:
+  version "7.3.8"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
+  integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
+  dependencies:
+    lru-cache "^6.0.0"
+
+serialize-javascript@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
+  integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
+  dependencies:
+    randombytes "^2.1.0"
+
+shallow-clone@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
+  integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
+  dependencies:
+    kind-of "^6.0.2"
+
+shebang-command@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+  integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+  dependencies:
+    shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+source-list-map@^2.0.0, source-list-map@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
+  integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
+
+source-map-support@~0.5.20:
+  version "0.5.21"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
+  integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+supports-color@^8.0.0:
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+  integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+  dependencies:
+    has-flag "^4.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+tapable@^2.1.1, tapable@^2.2.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
+  integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
+
+terser-webpack-plugin@^5.0.3:
+  version "5.3.6"
+  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz#5590aec31aa3c6f771ce1b1acca60639eab3195c"
+  integrity sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==
+  dependencies:
+    "@jridgewell/trace-mapping" "^0.3.14"
+    jest-worker "^27.4.5"
+    schema-utils "^3.1.1"
+    serialize-javascript "^6.0.0"
+    terser "^5.14.1"
+
+terser@^5.14.1:
+  version "5.15.1"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.15.1.tgz#8561af6e0fd6d839669c73b92bdd5777d870ed6c"
+  integrity sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==
+  dependencies:
+    "@jridgewell/source-map" "^0.3.2"
+    acorn "^8.5.0"
+    commander "^2.20.0"
+    source-map-support "~0.5.20"
+
+ticky@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/ticky/-/ticky-1.0.1.tgz#b7cfa71e768f1c9000c497b9151b30947c50e46d"
+  integrity sha512-RX35iq/D+lrsqhcPWIazM9ELkjOe30MSeoBHQHSsRwd1YuhJO5ui1K1/R0r7N3mFvbLBs33idw+eR6j+w6i/DA==
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+update-browserslist-db@^1.0.9:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3"
+  integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==
+  dependencies:
+    escalade "^3.1.1"
+    picocolors "^1.0.0"
+
+uri-js@^4.2.2:
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+  integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+  dependencies:
+    punycode "^2.1.0"
+
+util-deprecate@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
+vue-color-picker-wheel@0.4.3:
+  version "0.4.3"
+  resolved "https://registry.yarnpkg.com/vue-color-picker-wheel/-/vue-color-picker-wheel-0.4.3.tgz#d24bcf61c1c1139cd3e9a45511f6ce26a24ce09c"
+  integrity sha512-LBXGkWOCLSmt4m/klDjHHEk3sCsfTNSs9NBfbDU3TSEBb9vwfXD4tYsCO9lCO09iTDYt3rRbhUmxUBxYJX7Dcg==
+
+vue-hot-reload-api@^2.3.0:
+  version "2.3.4"
+  resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
+  integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==
+
+vue-loader@15.9.6:
+  version "15.9.6"
+  resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.6.tgz#f4bb9ae20c3a8370af3ecf09b8126d38ffdb6b8b"
+  integrity sha512-j0cqiLzwbeImIC6nVIby2o/ABAWhlppyL/m5oJ67R5MloP0hj/DtFgb0Zmq3J9CG7AJ+AXIvHVnJAPBvrLyuDg==
+  dependencies:
+    "@vue/component-compiler-utils" "^3.1.0"
+    hash-sum "^1.0.2"
+    loader-utils "^1.1.0"
+    vue-hot-reload-api "^2.3.0"
+    vue-style-loader "^4.1.0"
+
+vue-router@^3.4.9:
+  version "3.6.5"
+  resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.6.5.tgz#95847d52b9a7e3f1361cb605c8e6441f202afad8"
+  integrity sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==
+
+vue-style-loader@^4.1.0:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz#6d55863a51fa757ab24e89d9371465072aa7bc35"
+  integrity sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==
+  dependencies:
+    hash-sum "^1.0.2"
+    loader-utils "^1.0.2"
+
+vue-template-compiler@2.6.12:
+  version "2.6.12"
+  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.12.tgz#947ed7196744c8a5285ebe1233fe960437fcc57e"
+  integrity sha512-OzzZ52zS41YUbkCBfdXShQTe69j1gQDZ9HIX8miuC9C3rBCk9wIRjLiZZLrmX9V+Ftq/YEyv1JaVr5Y/hNtByg==
+  dependencies:
+    de-indent "^1.0.2"
+    he "^1.1.0"
+
+vue-template-es2015-compiler@^1.9.0:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
+  integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
+
+vue@2.6.12:
+  version "2.6.12"
+  resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.12.tgz#f5ebd4fa6bd2869403e29a896aed4904456c9123"
+  integrity sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==
+
+vuex@3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.0.tgz#95efa56a58f7607c135b053350833a09e01aa813"
+  integrity sha512-W74OO2vCJPs9/YjNjW8lLbj+jzT24waTo2KShI8jLvJW8OaIkgb3wuAMA7D+ZiUxDOx3ubwSZTaJBip9G8a3aQ==
+
+watchpack@^2.0.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
+  integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
+  dependencies:
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.1.2"
+
+webpack-cli@^4.3.0:
+  version "4.10.0"
+  resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.10.0.tgz#37c1d69c8d85214c5a65e589378f53aec64dab31"
+  integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==
+  dependencies:
+    "@discoveryjs/json-ext" "^0.5.0"
+    "@webpack-cli/configtest" "^1.2.0"
+    "@webpack-cli/info" "^1.5.0"
+    "@webpack-cli/serve" "^1.7.0"
+    colorette "^2.0.14"
+    commander "^7.0.0"
+    cross-spawn "^7.0.3"
+    fastest-levenshtein "^1.0.12"
+    import-local "^3.0.2"
+    interpret "^2.2.0"
+    rechoir "^0.7.0"
+    webpack-merge "^5.7.3"
+
+webpack-merge@^5.7.3:
+  version "5.8.0"
+  resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61"
+  integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==
+  dependencies:
+    clone-deep "^4.0.1"
+    wildcard "^2.0.0"
+
+webpack-sources@^1.1.0:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
+  integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==
+  dependencies:
+    source-list-map "^2.0.0"
+    source-map "~0.6.1"
+
+webpack-sources@^2.1.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.3.1.tgz#570de0af163949fe272233c2cefe1b56f74511fd"
+  integrity sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==
+  dependencies:
+    source-list-map "^2.0.1"
+    source-map "^0.6.1"
+
+webpack@5.11.1:
+  version "5.11.1"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.11.1.tgz#39b2b9daeb5c6c620e03b7556ec674eaed4016b4"
+  integrity sha512-tNUIdAmYJv+nupRs/U/gqmADm6fgrf5xE+rSlSsf2PgsGO7j2WG7ccU6AWNlOJlHFl+HnmXlBmHIkiLf+XA9mQ==
+  dependencies:
+    "@types/eslint-scope" "^3.7.0"
+    "@types/estree" "^0.0.45"
+    "@webassemblyjs/ast" "1.9.1"
+    "@webassemblyjs/helper-module-context" "1.9.1"
+    "@webassemblyjs/wasm-edit" "1.9.1"
+    "@webassemblyjs/wasm-parser" "1.9.1"
+    acorn "^8.0.4"
+    browserslist "^4.14.5"
+    chrome-trace-event "^1.0.2"
+    enhanced-resolve "^5.3.1"
+    eslint-scope "^5.1.1"
+    events "^3.2.0"
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.2.4"
+    json-parse-better-errors "^1.0.2"
+    loader-runner "^4.1.0"
+    mime-types "^2.1.27"
+    neo-async "^2.6.2"
+    pkg-dir "^5.0.0"
+    schema-utils "^3.0.0"
+    tapable "^2.1.1"
+    terser-webpack-plugin "^5.0.3"
+    watchpack "^2.0.0"
+    webpack-sources "^2.1.1"
+
+which@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+  dependencies:
+    isexe "^2.0.0"
+
+wildcard@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
+  integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
+
+yallist@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+  integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==
+
+yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yocto-queue@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+  integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==