diff --git a/.gitignore b/.gitignore
index 0bf57fa62..4695ea70a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,7 @@ node_modules
 /frontend/out/
 /frontend/.shadow-cljs
 /frontend/resources/public/*
+/frontend/resources/fonts/experiments
 /exporter/target
 /exporter/.shadow-cljs
 /docker/images/bundle*
diff --git a/backend/deps.edn b/backend/deps.edn
index 5659d1a62..316e1f981 100644
--- a/backend/deps.edn
+++ b/backend/deps.edn
@@ -47,7 +47,7 @@
   org.postgresql/postgresql {:mvn/version "42.2.19"}
   com.zaxxer/HikariCP {:mvn/version "4.0.3"}
 
-  funcool/datoteka {:mvn/version "1.2.0"}
+  funcool/datoteka {:mvn/version "2.0.0"}
   funcool/promesa {:mvn/version "6.0.0"}
   funcool/cuerdas {:mvn/version "2020.03.26-3"}
 
diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj
index ab8c96ebb..1dba68956 100644
--- a/backend/src/app/main.clj
+++ b/backend/src/app/main.clj
@@ -122,10 +122,15 @@
    :app.rlimits/image
    (cf/get :rlimits-image)
 
+   ;; RLimit definition for font processing
+   :app.rlimits/font
+   (cf/get :rlimits-font 2)
+
    ;; A collection of rlimits as hash-map.
    :app.rlimits/all
    {:password (ig/ref :app.rlimits/password)
-    :image    (ig/ref :app.rlimits/image)}
+    :image    (ig/ref :app.rlimits/image)
+    :font     (ig/ref :app.rlimits/font)}
 
    :app.rpc/rpc
    {:pool       (ig/ref :app.db/pool)
diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj
index 6d035a9c8..a1253dc65 100644
--- a/backend/src/app/media.clj
+++ b/backend/src/app/media.clj
@@ -5,7 +5,7 @@
 ;; Copyright (c) UXBOX Labs SL
 
 (ns app.media
-  "Media postprocessing."
+  "Media & Font postprocessing."
   (:require
    [app.common.data :as d]
    [app.common.exceptions :as ex]
@@ -13,20 +13,29 @@
    [app.common.spec :as us]
    [app.rlimits :as rlm]
    [app.rpc.queries.svg :as svg]
+   [clojure.java.io :as io]
+   [clojure.java.shell :as sh]
    [clojure.spec.alpha :as s]
    [cuerdas.core :as str]
    [datoteka.core :as fs])
   (:import
    java.io.ByteArrayInputStream
+   java.io.OutputStream
+   org.apache.commons.io.IOUtils
    org.im4java.core.ConvertCmd
    org.im4java.core.IMOperation
    org.im4java.core.Info))
 
-;; --- Generic specs
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; --- Utility functions
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(s/def ::image-content-type cm/valid-image-types)
+(s/def ::font-content-type cm/valid-font-types)
 
 (s/def :internal.http.upload/filename ::us/string)
 (s/def :internal.http.upload/size ::us/integer)
-(s/def :internal.http.upload/content-type cm/valid-media-types)
+(s/def :internal.http.upload/content-type ::us/string)
 (s/def :internal.http.upload/tempfile any?)
 
 (s/def ::upload
@@ -35,8 +44,44 @@
                    :internal.http.upload/tempfile
                    :internal.http.upload/content-type]))
 
+(defn validate-media-type
+  ([mtype] (validate-media-type mtype cm/valid-image-types))
+  ([mtype allowed]
+   (when-not (contains? allowed mtype)
+     (ex/raise :type :validation
+               :code :media-type-not-allowed
+               :hint "Seems like you are uploading an invalid media object"))))
 
+
+(defmulti process :cmd)
+(defmulti process-error class)
+
+(defmethod process :default
+  [{:keys [cmd] :as params}]
+  (ex/raise :type :internal
+            :code :not-implemented
+            :hint (str/fmt "No impl found for process cmd: %s" cmd)))
+
+(defmethod process-error :default
+  [error]
+  (ex/raise :type :internal :cause error))
+
+(defn run
+  [{:keys [rlimits] :as cfg} {:keys [rlimit] :or {rlimit :image} :as params}]
+  (us/assert map? rlimits)
+  (let [rlimit (get rlimits rlimit)]
+    (when-not rlimit
+      (ex/raise :type :internal
+                :code :rlimit-not-configured
+                :hint ":image rlimit not configured"))
+    (try
+      (rlm/execute rlimit (process params))
+      (catch Throwable e
+        (process-error e)))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;; --- Thumbnails Generation
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
 (s/def ::cmd keyword?)
 
@@ -77,8 +122,6 @@
              :size   (alength ^bytes thumbnail-data)
              :data   (ByteArrayInputStream. thumbnail-data)))))
 
-(defmulti process :cmd)
-
 (defmethod process :generic-thumbnail
   [{:keys [quality width height] :as params}]
   (us/assert ::thumbnail-params params)
@@ -161,33 +204,63 @@
          :height (.getPageHeight instance)
          :mtype  mtype}))))
 
-(defmethod process :default
-  [{:keys [cmd] :as params}]
-  (ex/raise :type :internal
-            :code :not-implemented
-            :hint (str "No impl found for process cmd:" cmd)))
+(defmethod process-error org.im4java.core.InfoException
+  [error]
+  (ex/raise :type :validation
+            :code :invalid-image
+            :cause error))
 
-(defn run
-  [{:keys [rlimits]} params]
-  (us/assert map? rlimits)
-  (let [rlimit (get rlimits :image)]
-    (when-not rlimit
-      (ex/raise :type :internal
-                :code :rlimit-not-configured
-                :hint ":image rlimit not configured"))
-    (try
-      (rlm/execute rlimit (process params))
-      (catch org.im4java.core.InfoException e
-        (ex/raise :type :validation
-                  :code :invalid-image
-                  :cause e)))))
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; --- Fonts Generation
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-;; --- Utility functions
+(def all-fotmats #{"font/woff2", "font/woff", "font/otf", "font/ttf"})
 
-(defn validate-media-type
-  ([mtype] (validate-media-type mtype cm/valid-media-types))
-  ([mtype allowed]
-   (when-not (contains? allowed mtype)
-     (ex/raise :type :validation
-               :code :media-type-not-allowed
-               :hint "Seems like you are uploading an invalid media object"))))
+(defmethod process :generate-fonts
+  [{:keys [input] :as params}]
+  (letfn [(ttf->otf [data]
+            (let [input-file  (fs/create-tempfile :prefix "penpot")
+                  output-file (fs/path (str input-file ".otf"))
+                  _           (with-open [out (io/output-stream input-file)]
+                                (IOUtils/writeChunked ^bytes data ^OutputStream out)
+                                (.flush ^OutputStream out))
+                  res         (sh/sh "fontforge" "-lang=ff" "-c"
+                                     (str/fmt "Open('%s'); Generate('%s')"
+                                              (str input-file)
+                                              (str output-file)))]
+              (when (zero? (:exit res))
+                (fs/slurp-bytes output-file))))
+
+
+          (ttf-or-otf->woff [data]
+            (let [input-file  (fs/create-tempfile :prefix "penpot" :suffix "")
+                  output-file (fs/path (str input-file ".woff"))
+                  _           (with-open [out (io/output-stream input-file)]
+                                (IOUtils/writeChunked ^bytes data ^OutputStream out)
+                                (.flush ^OutputStream out))
+                  res         (sh/sh "sfnt2woff" (str input-file))]
+              (when (zero? (:exit res))
+                (fs/slurp-bytes output-file))))
+
+          (ttf-or-otf->woff2 [data]
+            (let [input-file  (fs/create-tempfile :prefix "penpot" :suffix "")
+                  output-file (fs/path (str input-file ".woff2"))
+                  _           (with-open [out (io/output-stream input-file)]
+                                (IOUtils/writeChunked ^bytes data ^OutputStream out)
+                                (.flush ^OutputStream out))
+                  res         (sh/sh "woff2_compress" (str input-file))]
+              (when (zero? (:exit res))
+                (fs/slurp-bytes output-file))))]
+
+    (let [current (into #{} (keys input))]
+      (if (contains? current "font/ttf")
+        (-> input
+            (assoc "font/otf" (ttf->otf (get input "font/ttf")))
+            (assoc "font/woff" (ttf-or-otf->woff (get input "font/ttf")))
+            (assoc "font/woff2" (ttf-or-otf->woff2 (get input "font/ttf"))))
+
+        (-> input
+            ;; TODO: pending to implement
+            ;; (assoc "font/ttf" (otf->ttf (get input "font/ttf")))
+            (assoc "font/woff" (ttf-or-otf->woff (get input "font/otf")))
+            (assoc "font/woff2" (ttf-or-otf->woff2 (get input "font/otf"))))))))
diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj
index 18a8917c4..4989dd109 100644
--- a/backend/src/app/migrations.clj
+++ b/backend/src/app/migrations.clj
@@ -166,6 +166,9 @@
 
    {:name "0052-del-legacy-user-and-team"
     :fn (mg/resource "app/migrations/sql/0052-del-legacy-user-and-team.sql")}
+
+   {:name "0053-add-team-font-variant-table"
+    :fn (mg/resource "app/migrations/sql/0053-add-team-font-variant-table.sql")}
    ])
 
 
diff --git a/backend/src/app/migrations/sql/0053-add-team-font-variant-table.sql b/backend/src/app/migrations/sql/0053-add-team-font-variant-table.sql
new file mode 100644
index 000000000..423ed2556
--- /dev/null
+++ b/backend/src/app/migrations/sql/0053-add-team-font-variant-table.sql
@@ -0,0 +1,20 @@
+CREATE TABLE team_font_variant (
+  id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
+
+  team_id    uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE,
+  profile_id uuid NULL REFERENCES profile(id) ON DELETE SET NULL,
+
+  created_at timestamptz NOT NULL DEFAULT now(),
+  modified_at timestamptz NOT NULL DEFAULT now(),
+  deleted_at timestamptz NULL DEFAULT NULL,
+
+  font_id text NOT NULL,
+  font_family text NOT NULL,
+  font_weight smallint NOT NULL,
+  font_style text NOT NULL,
+
+  otf_file_id   uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL,
+  ttf_file_id   uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL,
+  woff1_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL,
+  woff2_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL
+);
diff --git a/backend/src/app/rlimits.clj b/backend/src/app/rlimits.clj
index 86a3903db..aaf813178 100644
--- a/backend/src/app/rlimits.clj
+++ b/backend/src/app/rlimits.clj
@@ -18,6 +18,7 @@
 
 (derive ::password ::instance)
 (derive ::image ::instance)
+(derive ::font ::instance)
 
 (defmethod ig/pre-init-spec ::instance [_]
   (s/spec int?))
diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj
index a0c1a4bda..2495d3227 100644
--- a/backend/src/app/rpc.clj
+++ b/backend/src/app/rpc.clj
@@ -120,6 +120,7 @@
                      'app.rpc.queries.profile
                      'app.rpc.queries.recent-files
                      'app.rpc.queries.viewer
+                     'app.rpc.queries.fonts
                      'app.rpc.queries.svg)
          (map (partial process-method cfg))
          (into {}))))
@@ -143,6 +144,7 @@
                      'app.rpc.mutations.teams
                      'app.rpc.mutations.management
                      'app.rpc.mutations.ldap
+                     'app.rpc.mutations.fonts
                      'app.rpc.mutations.verify-token)
          (map (partial process-method cfg))
          (into {}))))
diff --git a/backend/src/app/rpc/mutations/fonts.clj b/backend/src/app/rpc/mutations/fonts.clj
new file mode 100644
index 000000000..e5194174f
--- /dev/null
+++ b/backend/src/app/rpc/mutations/fonts.clj
@@ -0,0 +1,116 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.rpc.mutations.fonts
+  (:require
+   [app.common.spec :as us]
+   [app.common.uuid :as uuid]
+   [app.config :as cf]
+   [app.db :as db]
+   [app.media :as media]
+   [app.rpc.queries.teams :as teams]
+   [app.storage :as sto]
+   [app.util.services :as sv]
+   [app.util.time :as dt]
+   [app.worker :as wrk]
+   [clojure.spec.alpha :as s]
+   [cuerdas.core :as str]))
+
+(declare create-font-variant)
+
+(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
+(def valid-style #{"normal" "italic"})
+
+(s/def ::profile-id ::us/uuid)
+(s/def ::team-id ::us/uuid)
+(s/def ::name ::us/not-empty-string)
+(s/def ::weight valid-weight)
+(s/def ::style valid-style)
+(s/def ::font-id (s/and ::us/string #(str/starts-with? % "custom-")))
+(s/def ::content-type ::media/font-content-type)
+(s/def ::data (s/map-of ::us/string any?))
+
+(s/def ::create-font-variant
+  (s/keys :req-un [::profile-id ::team-id ::data
+                   ::font-id ::font-family ::font-weight ::font-style]))
+
+(sv/defmethod ::create-font-variant
+  [{:keys [pool] :as cfg} {:keys [team-id profile-id] :as params}]
+  (db/with-atomic [conn pool]
+    (let [cfg (assoc cfg :conn conn)]
+      (teams/check-edition-permissions! conn profile-id team-id)
+      (create-font-variant cfg params))))
+
+(defn create-font-variant
+  [{:keys [conn storage] :as cfg} {:keys [data] :as params}]
+  (let [data    (media/run cfg {:cmd :generate-fonts :input data :rlimit :font})
+        storage (assoc storage :conn conn)
+        otf     (when-let [fdata (get data "font/otf")]
+                  (sto/put-object storage {:content (sto/content fdata)
+                                           :content-type "font/otf"}))
+
+        ttf     (when-let [fdata (get data "font/ttf")]
+                  (sto/put-object storage {:content (sto/content fdata)
+                                           :content-type "font/ttf"}))
+
+        woff1   (when-let [fdata (get data "font/woff")]
+                  (sto/put-object storage {:content (sto/content fdata)
+                                           :content-type "font/woff"}))
+
+        woff2   (when-let [fdata (get data "font/woff2")]
+                  (sto/put-object storage {:content (sto/content fdata)
+                                           :content-type "font/woff2"}))]
+
+    (db/insert! conn :team-font-variant
+                {:id (uuid/next)
+                 :team-id (:team-id params)
+                 :font-id (:font-id params)
+                 :font-family (:font-family params)
+                 :font-weight (:font-weight params)
+                 :font-style (:font-style params)
+                 :woff1-file-id (:id woff1)
+                 :woff2-file-id (:id woff2)
+                 :otf-file-id (:id otf)
+                 :ttf-file-id (:id ttf)})))
+
+;; --- UPDATE FONT VARIANT
+
+(s/def ::update-font-variant
+  (s/keys :req-un [::profile-id ::team-id ::id ::font-family ::font-id]))
+
+(sv/defmethod ::update-font-variant
+  [{:keys [pool] :as cfg} {:keys [id team-id profile-id font-family font-id] :as params}]
+  (db/with-atomic [conn pool]
+    (teams/check-edition-permissions! conn profile-id team-id)
+    (db/update! conn :team-font-variant
+                {:font-family font-family
+                 :font-id font-id}
+                {:id id
+                 :team-id team-id})
+    nil))
+
+;; --- DELETE FONT VARIANT
+
+(s/def ::delete-font-variant
+  (s/keys :req-un [::profile-id ::team-id ::id]))
+
+(sv/defmethod ::delete-font-variant
+  [{:keys [pool] :as cfg} {:keys [id team-id profile-id font-family font-id] :as params}]
+  (db/with-atomic [conn pool]
+    (teams/check-edition-permissions! conn profile-id team-id)
+
+    ;; Schedule object deletion
+    (wrk/submit! {::wrk/task :delete-object
+                  ::wrk/delay cf/deletion-delay
+                  ::wrk/conn conn
+                  :id id
+                  :type :team-font-variant})
+
+    (db/update! conn :team-font-variant
+                {:deleted-at (dt/now)}
+                {:id id
+                 :team-id team-id})
+    nil))
diff --git a/backend/src/app/rpc/mutations/media.clj b/backend/src/app/rpc/mutations/media.clj
index 26dbac324..62241a48b 100644
--- a/backend/src/app/rpc/mutations/media.clj
+++ b/backend/src/app/rpc/mutations/media.clj
@@ -32,12 +32,15 @@
 (s/def ::file-id ::us/uuid)
 (s/def ::team-id ::us/uuid)
 
+
 ;; --- Create File Media object (upload)
 
 (declare create-file-media-object)
 (declare select-file)
 
-(s/def ::content ::media/upload)
+(s/def ::content-type ::media/image-content-type)
+(s/def ::content (s/and ::media/upload (s/keys :req-un [::content-type])))
+
 (s/def ::is-local ::us/boolean)
 
 (s/def ::upload-file-media-object
diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj
index 7e719b6c2..01fdb5243 100644
--- a/backend/src/app/rpc/mutations/profile.clj
+++ b/backend/src/app/rpc/mutations/profile.clj
@@ -401,7 +401,9 @@
 
 (declare update-profile-photo)
 
-(s/def ::file ::media/upload)
+(s/def ::content-type ::media/image-content-type)
+(s/def ::file (s/and ::media/upload (s/keys :req-un [::content-type])))
+
 (s/def ::update-profile-photo
   (s/keys :req-un [::profile-id ::file]))
 
diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj
index 701833162..c9adebfae 100644
--- a/backend/src/app/rpc/mutations/teams.clj
+++ b/backend/src/app/rpc/mutations/teams.clj
@@ -249,7 +249,9 @@
 
 (declare upload-photo)
 
-(s/def ::file ::media/upload)
+(s/def ::content-type ::media/image-content-type)
+(s/def ::file (s/and ::media/upload (s/keys :req-un [::content-type])))
+
 (s/def ::update-team-photo
   (s/keys :req-un [::profile-id ::team-id ::file]))
 
diff --git a/backend/src/app/rpc/queries/fonts.clj b/backend/src/app/rpc/queries/fonts.clj
new file mode 100644
index 000000000..b25c780f5
--- /dev/null
+++ b/backend/src/app/rpc/queries/fonts.clj
@@ -0,0 +1,29 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.rpc.queries.fonts
+  (:require
+   [app.common.spec :as us]
+   [app.db :as db]
+   [app.rpc.queries.teams :as teams]
+   [app.util.services :as sv]
+   [clojure.spec.alpha :as s]))
+
+;; --- Query: Team Font Variants
+
+(s/def ::team-id ::us/uuid)
+(s/def ::profile-id ::us/uuid)
+(s/def ::team-font-variants
+  (s/keys :req-un [::profile-id ::team-id]))
+
+(sv/defmethod ::team-font-variants
+  [{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
+  (with-open [conn (db/open pool)]
+    (teams/check-read-permissions! conn profile-id team-id)
+    (db/query conn :team-font-variant
+              {:team-id team-id
+               :deleted-at nil})))
+
diff --git a/backend/src/app/storage/impl.clj b/backend/src/app/storage/impl.clj
index 27a44cbed..a28184df5 100644
--- a/backend/src/app/storage/impl.clj
+++ b/backend/src/app/storage/impl.clj
@@ -145,8 +145,8 @@
     (make-output-stream [_ opts]
       (throw (UnsupportedOperationException. "not implemented")))
 
-      clojure.lang.Counted
-      (count [_] size)))
+    clojure.lang.Counted
+    (count [_] size)))
 
 (defn content
   ([data] (content data nil))
diff --git a/backend/src/app/tasks/delete_object.clj b/backend/src/app/tasks/delete_object.clj
index bcd6e4a48..8a8335bcd 100644
--- a/backend/src/app/tasks/delete_object.clj
+++ b/backend/src/app/tasks/delete_object.clj
@@ -10,6 +10,7 @@
    [app.common.data :as d]
    [app.common.spec :as us]
    [app.db :as db]
+   [app.storage :as sto]
    [app.util.logging :as l]
    [clojure.spec.alpha :as s]
    [integrant.core :as ig]))
@@ -24,7 +25,8 @@
   (fn [{:keys [props] :as task}]
     (us/verify ::props props)
     (db/with-atomic [conn pool]
-      (handle-deletion conn props))))
+      (let [cfg (assoc cfg :conn conn)]
+        (handle-deletion cfg props)))))
 
 (s/def ::type ::us/keyword)
 (s/def ::id ::us/uuid)
@@ -34,21 +36,32 @@
   (fn [_ props] (:type props)))
 
 (defmethod handle-deletion :default
-  [_conn {:keys [type]}]
+  [_cfg {:keys [type]}]
   (l/warn :hint "no handler found"
           :type (d/name type)))
 
 (defmethod handle-deletion :file
-  [conn {:keys [id] :as props}]
+  [{:keys [conn]} {:keys [id] :as props}]
   (let [sql "delete from file where id=? and deleted_at is not null"]
     (db/exec-one! conn [sql id])))
 
 (defmethod handle-deletion :project
-  [conn {:keys [id] :as props}]
+  [{:keys [conn]} {:keys [id] :as props}]
   (let [sql "delete from project where id=? and deleted_at is not null"]
     (db/exec-one! conn [sql id])))
 
 (defmethod handle-deletion :team
-  [conn {:keys [id] :as props}]
+  [{:keys [conn]} {:keys [id] :as props}]
   (let [sql "delete from team where id=? and deleted_at is not null"]
     (db/exec-one! conn [sql id])))
+
+(defmethod handle-deletion :team-font-variant
+  [{:keys [conn storage]} {:keys [id] :as props}]
+  (let [font    (db/get-by-id conn :team-font-variant id {:uncheked true})
+        storage (assoc storage :conn conn)]
+    (when (:deleted-at font)
+      (db/delete! conn :team-font-variant {:id id})
+      (some->> (:woff1-file-id font) (sto/del-object storage))
+      (some->> (:woff2-file-id font) (sto/del-object storage))
+      (some->> (:otf-file-id font)   (sto/del-object storage))
+      (some->> (:ttf-file-id font)   (sto/del-object storage)))))
diff --git a/backend/src/app/tasks/file_media_gc.clj b/backend/src/app/tasks/file_media_gc.clj
index d1bdc6751..8b8bc3d28 100644
--- a/backend/src/app/tasks/file_media_gc.clj
+++ b/backend/src/app/tasks/file_media_gc.clj
@@ -101,7 +101,10 @@
                :media-id (:media-id mobj)
                :thumbnail-id (:thumbnail-id mobj))
       ;; NOTE: deleting the file-media-object in the database
-      ;; automatically marks as toched the referenced storage objects.
+      ;; automatically marks as toched the referenced storage
+      ;; objects. The touch mechanism is needed because many files can
+      ;; point to the same storage objects and we can't just delete
+      ;; them.
       (db/delete! conn :file-media-object {:id (:id mobj)}))
 
     nil))
diff --git a/common/app/common/data.cljc b/common/app/common/data.cljc
index a46b306f2..12b58d619 100644
--- a/common/app/common/data.cljc
+++ b/common/app/common/data.cljc
@@ -261,6 +261,19 @@
       (recur (reduce-kv assoc! res (first maps))
              (next maps)))))
 
+(defn distinct-xf
+  [f]
+  (fn [rf]
+    (let [seen (volatile! #{})]
+      (fn
+        ([] (rf))
+        ([result] (rf result))
+        ([result input]
+         (let [input* (f input)]
+           (if (contains? @seen input*)
+             result
+             (do (vswap! seen conj input*)
+                 (rf result input)))))))))
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;; Data Parsing / Conversion
diff --git a/common/app/common/media.cljc b/common/app/common/media.cljc
index df3a556ca..108c21b10 100644
--- a/common/app/common/media.cljc
+++ b/common/app/common/media.cljc
@@ -9,10 +9,10 @@
    [clojure.spec.alpha :as s]
    [cuerdas.core :as str]))
 
-(def valid-media-types
-  #{"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"})
-
-(def str-media-types (str/join "," valid-media-types))
+(def valid-font-types #{"font/ttf" "font/woff", "font/otf"})
+(def valid-image-types #{"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"})
+(def str-image-types (str/join "," valid-image-types))
+(def str-font-types (str/join "," valid-font-types))
 
 (defn format->extension
   [format]
@@ -65,3 +65,38 @@
                    ::modified-at
                    ::uri]))
 
+
+(defn parse-font-weight
+  [variant]
+  (cond
+    (re-seq #"(?i)(?:hairline|thin)" variant)           100
+    (re-seq #"(?i)(?:extra light|ultra light)" variant) 200
+    (re-seq #"(?i)(?:light)" variant)                   300
+    (re-seq #"(?i)(?:normal|regular)" variant)          400
+    (re-seq #"(?i)(?:medium)" variant)                  500
+    (re-seq #"(?i)(?:semi bold|demi bold)" variant)     600
+    (re-seq #"(?i)(?:bold)" variant)                    700
+    (re-seq #"(?i)(?:extra bold|ultra bold)" variant)   800
+    (re-seq #"(?i)(?:black|heavy)" variant)             900
+    (re-seq #"(?i)(?:extra black|ultra black)" variant) 950
+    :else                                               400))
+
+(defn parse-font-style
+  [variant]
+  (if (re-seq #"(?i)(?:italic)" variant)
+    "italic"
+    "normal"))
+
+(defn font-weight->name
+  [weight]
+  (case weight
+    100 "Hairline"
+    200 "Extra Light"
+    300 "Light"
+    400 "Regular"
+    500 "Medium"
+    600 "Semi Bold"
+    700 "Bold"
+    800 "Extra Bold"
+    900 "Black"
+    950 "Extra Black"))
diff --git a/common/app/common/spec.cljc b/common/app/common/spec.cljc
index eabce91ff..00aebc82a 100644
--- a/common/app/common/spec.cljc
+++ b/common/app/common/spec.cljc
@@ -6,7 +6,7 @@
 
 (ns app.common.spec
   "Data manipulation and query helper functions."
-  (:refer-clojure :exclude [assert])
+  (:refer-clojure :exclude [assert bytes?])
   #?(:cljs (:require-macros [app.common.spec :refer [assert]]))
   (:require
    #?(:clj  [clojure.spec.alpha :as s]
@@ -108,6 +108,20 @@
 (s/def ::point gpt/point?)
 (s/def ::id ::uuid)
 
+(defn bytes?
+  "Test if a first parameter is a byte
+  array or not."
+  [x]
+  (if (nil? x)
+    false
+    #?(:clj (= (Class/forName "[B")
+               (.getClass ^Object x))
+       :cljs (or (instance? js/Uint8Array x)
+                 (instance? js/ArrayBuffer x)))))
+
+(s/def ::bytes bytes?)
+
+
 (s/def ::safe-integer
   #(and
     (int? %)
diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js
index 44596e08b..5a2068d1d 100644
--- a/frontend/gulpfile.js
+++ b/frontend/gulpfile.js
@@ -17,6 +17,7 @@ const mkdirp = require("mkdirp");
 const rimraf = require("rimraf");
 const sass = require("sass");
 const gettext = require("gettext-parser");
+const marked = require("marked");
 
 const mapStream = require("map-stream");
 const paths = {};
@@ -45,17 +46,35 @@ function readLocales() {
 
     for (let key of Object.keys(trdata)) {
       if (key === "") continue;
+      const comments = trdata[key].comments || {};
 
       if (l.isNil(result[key])) {
         result[key] = {};
       }
 
-      const msgstr = trdata[key].msgstr;
-      if (msgstr.length === 1) {
-        result[key][lang] = msgstr[0];
+      const isMarkdown = l.includes(comments.flag, "markdown");
+
+      const msgs = trdata[key].msgstr;
+      if (msgs.length === 1) {
+        let message = msgs[0];
+        if (isMarkdown) {
+          message = marked.parseInline(message);
+        }
+
+        result[key][lang] = message;
       } else {
-        result[key][lang] = msgstr;
+        result[key][lang] = msgs.map((item) => {
+          if (isMarkdown) {
+            return marked.parseInline(item);
+          } else {
+            return item;
+          }
+        });
       }
+      // if (key === "modals.delete-font.title") {
+      //   console.dir(trdata[key], {depth:10});
+      //   console.dir(result[key], {depth:10});
+      // }
     }
   }
 
diff --git a/frontend/package.json b/frontend/package.json
index dbc09258c..5fb09dbf2 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -27,12 +27,13 @@
     "gulp-sourcemaps": "^3.0.0",
     "gulp-svg-sprite": "^1.5.0",
     "map-stream": "0.0.7",
+    "marked": "^2.0.3",
     "mkdirp": "^1.0.4",
     "postcss": "^8.2.7",
     "postcss-clean": "^1.2.2",
     "rimraf": "^3.0.0",
     "sass": "^1.32.8",
-    "shadow-cljs": "^2.11.20"
+    "shadow-cljs": "2.12.5"
   },
   "dependencies": {
     "date-fns": "^2.21.1",
@@ -41,6 +42,7 @@
     "js-beautify": "^1.13.5",
     "luxon": "^1.26.0",
     "mousetrap": "^1.6.5",
+    "opentype.js": "^1.3.3",
     "randomcolor": "^0.6.2",
     "react": "~17.0.1",
     "react-dom": "~17.0.1",
diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss
index 0c9ee57b1..27c7c3610 100644
--- a/frontend/resources/styles/main-default.scss
+++ b/frontend/resources/styles/main-default.scss
@@ -63,6 +63,7 @@
 @import "main/partials/dashboard-sidebar";
 @import "main/partials/dashboard-team";
 @import "main/partials/dashboard-settings";
+@import "main/partials/dashboard-fonts";
 @import "main/partials/debug-icons-preview";
 @import "main/partials/editable-label";
 @import "main/partials/left-toolbar";
diff --git a/frontend/resources/styles/main/partials/dashboard-fonts.scss b/frontend/resources/styles/main/partials/dashboard-fonts.scss
new file mode 100644
index 000000000..864f17e87
--- /dev/null
+++ b/frontend/resources/styles/main/partials/dashboard-fonts.scss
@@ -0,0 +1,167 @@
+.dashboard-fonts {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+
+  .dashboard-installed-fonts {
+    max-width: 1000px;
+    width: 100%;
+    display: flex;
+    margin-top: $big;
+    flex-direction: column;
+
+    h3 {
+      font-size: $fs14;
+      color: $color-gray-30;
+      margin: $x-small;
+    }
+
+    .font-item {
+      color: $color-black;
+    }
+  }
+
+  .installed-fonts-header {
+    color: $color-gray-40;
+    display: flex;
+    height: 40px;
+    font-size: $fs12;
+    background-color: $color-white;
+    align-items: center;
+    padding: 0px $big;
+
+    > div {
+      width: 30%;
+    }
+
+    .search-input {
+      display: flex;
+      flex-grow: 1;
+      justify-content: flex-end;
+
+      input {
+        font-size: $fs12;
+        border: 1px solid $color-gray-30;
+        border-radius: $br-small;
+        width: 130px;
+        padding: $x-small;
+        margin: 0px;
+      }
+    }
+  }
+
+  .fonts-group {
+    margin-top: $big;
+  }
+
+  .font-item {
+    color: $color-gray-40;
+    font-size: $fs14;
+    background-color: $color-white;
+    display: flex;
+    min-width: 1000px;
+    width: 100%;
+    height: 97px;
+    align-items: center;
+    padding: $big;
+
+    &:not(:first-child) {
+      border-top: 1px solid $color-gray-10;
+    }
+
+    input {
+      border: 1px solid $color-gray-30;
+      border-radius: $br-small;
+      margin: 0px;
+      padding: $small;
+      font-size: $fs12;
+    }
+
+    > div {
+      width: 30%;
+    }
+
+    .variant {
+      font-size: $fs14;
+    }
+
+    .filenames {
+      display: flex;
+      flex-direction: column;
+      font-size: $fs12;
+    }
+
+    .options {
+      display: flex;
+      justify-content: flex-end;
+
+      .icon {
+        width: $big;
+        cursor: pointer;
+        display: flex;
+        margin-left: 10px;
+        justify-content: center;
+        align-items: center;
+        svg {
+          width: 16px;
+          height: 16px;
+        }
+
+        &.close {
+          svg {
+            transform: rotate(45deg);
+          }
+        }
+
+      }
+    }
+  }
+
+
+  .dashboard-fonts-upload {
+    max-width: 1000px;
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+
+
+    .upload-button {
+      width: 100px;
+    }
+  }
+
+  .dashboard-fonts-hero {
+    font-size: $fs14;
+
+    padding: $x-big;
+    background-color: $color-white;
+    margin-top: $x-big;
+    display: flex;
+    justify-content: space-between;
+
+    .banner {
+      background-color: unset;
+
+        display: flex;
+
+      .icon {
+        display: flex;
+        align-items: center;
+        padding-left: 0px;
+        padding-right: 10px;
+        svg {
+          fill: $color-info;
+        }
+      }
+    }
+
+    .desc {
+      h2 {
+        margin-bottom: $medium;
+        color: $color-black;
+      }
+      width: 80%;
+      color: $color-gray-40;
+    }
+  }
+}
diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs
index bb56ba551..a09a1f1d4 100644
--- a/frontend/src/app/main/data/dashboard.cljs
+++ b/frontend/src/app/main/data/dashboard.cljs
@@ -99,8 +99,6 @@
         (->> (rp/query :team-stats {:team-id id})
              (rx/map #(partial fetched %)))))))
 
-;; --- Fetch Projects
-
 (defn fetch-projects
   [{:keys [team-id] :as params}]
   (us/assert ::us/uuid team-id)
@@ -123,8 +121,6 @@
                   (ptk/watch (fetch-projects {:team-id id}) state stream)
                   (ptk/watch (du/fetch-users {:team-id id}) state stream))))))
 
-;; --- Search Files
-
 (s/def :internal.event.search-files/team-id ::us/uuid)
 (s/def :internal.event.search-files/search-term (s/nilable ::us/string))
 
@@ -149,8 +145,6 @@
         (->> (rp/query :search-files params)
              (rx/map #(partial fetched %)))))))
 
-;; --- Fetch Files
-
 (defn fetch-files
   [{:keys [project-id] :as params}]
   (us/assert ::us/uuid project-id)
@@ -162,8 +156,6 @@
         (->> (rp/query :files params)
              (rx/map #(partial fetched %)))))))
 
-;; --- Fetch Shared Files
-
 (defn fetch-shared-files
   [{:keys [team-id] :as params}]
   (us/assert ::us/uuid team-id)
@@ -175,8 +167,6 @@
         (->> (rp/query :shared-files {:team-id team-id})
              (rx/map #(partial fetched %)))))))
 
-;; --- Fetch recent files
-
 (declare recent-files-fetched)
 
 (defn fetch-recent-files
diff --git a/frontend/src/app/main/data/dashboard/fonts.cljs b/frontend/src/app/main/data/dashboard/fonts.cljs
new file mode 100644
index 000000000..c1b76c6ab
--- /dev/null
+++ b/frontend/src/app/main/data/dashboard/fonts.cljs
@@ -0,0 +1,94 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.main.data.dashboard.fonts
+  (:require
+   [app.common.exceptions :as ex]
+   [app.common.data :as d]
+   [app.common.media :as cm]
+   [app.common.spec :as us]
+   [app.common.uuid :as uuid]
+   [app.main.repo :as rp]
+   [app.util.time :as dt]
+   [app.util.timers :as ts]
+   [app.main.data.messages :as dm]
+   [app.util.webapi :as wa]
+   [app.util.object :as obj]
+   [app.util.transit :as t]
+   [beicon.core :as rx]
+   [cljs.spec.alpha :as s]
+   [cuerdas.core :as str]
+   [potok.core :as ptk]))
+
+(defn fetch-fonts
+  [{:keys [id] :as team}]
+  (ptk/reify ::fetch-fonts
+    ptk/WatchEvent
+    (watch [_ state stream]
+      (->> (rp/query! :team-font-variants {:team-id id})
+           (rx/map (fn [items]
+                     #(assoc % :dashboard-fonts (d/index-by :id items))))))))
+
+(defn add-font
+  [font]
+  (ptk/reify ::add-font
+    ptk/UpdateEvent
+    (update [_ state]
+      (update state :dashboard-fonts assoc (:id font) font))))
+
+
+(defn update-font
+  [{:keys [id font-family] :as font}]
+  (ptk/reify ::update-font
+    ptk/UpdateEvent
+    (update [_ state]
+      (let [font (assoc font :font-id (str "custom-" (str/slug font-family)))]
+        (update state :dashboard-fonts assoc id font)))
+
+    ptk/WatchEvent
+    (watch [_ state stream]
+      (let [font (get-in state [:dashboard-fonts id])]
+        (->> (rp/mutation! :update-font-variant font)
+             (rx/ignore))))))
+
+(defn delete-font
+  [{:keys [id] :as font}]
+  (ptk/reify ::delete-font
+    ptk/UpdateEvent
+    (update [_ state]
+      (update state :dashboard-fonts dissoc id))
+
+    ptk/WatchEvent
+    (watch [_ state stream]
+      (let [params (select-keys font [:id :team-id])]
+        (->> (rp/mutation! :delete-font-variant params)
+             (rx/ignore))))))
+
+;; (defn upload-font
+;;   [{:keys [id] :as font}]
+;;   (ptk/reify ::upload-font
+;;     ptk/WatchEvent
+;;     (watch [_ state stream]
+;;       (let [{:keys [on-success on-error]
+;;              :or {on-success identity
+;;                   on-error rx/throw}} (meta params)]
+;;         (->> (rp/mutation! :create-font-variant font)
+;;              (rx/tap on-success)
+;;              (rx/catch on-error))))))
+
+;; (defn add-font
+;;   "Add fonts to the state in a pending to upload state."
+;;   [font]
+;;   (ptk/reify ::add-font
+;;     ptk/UpdateEvent
+;;     (update [_ state]
+;;       (let [id   (uuid/next)
+;;             font (-> font
+;;                      (assoc :created-at (dt/now))
+;;                      (assoc :id id)
+;;                      (assoc :status :draft))]
+;;         (js/console.log (clj->js font))
+;;         (assoc-in state [:dashboard-fonts id] font)))))
diff --git a/frontend/src/app/main/data/media.cljs b/frontend/src/app/main/data/media.cljs
index 27794fd77..695807cc2 100644
--- a/frontend/src/app/main/data/media.cljs
+++ b/frontend/src/app/main/data/media.cljs
@@ -51,7 +51,7 @@
     (ex/raise :type :validation
               :code :media-too-large
               :hint (str/fmt "media size is large than 5mb (size: %s)" (.-size file))))
-  (when-not (contains? cm/valid-media-types (.-type file))
+  (when-not (contains? cm/valid-image-types (.-type file))
     (ex/raise :type :validation
               :code :media-type-not-allowed
               :hint (str/fmt "media type %s is not supported" (.-type file))))
diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs
index 7212dbc32..3b8554006 100644
--- a/frontend/src/app/main/refs.cljs
+++ b/frontend/src/app/main/refs.cljs
@@ -43,6 +43,9 @@
 (def dashboard-local
   (l/derived :dashboard-local st/state))
 
+(def dashboard-fonts
+  (l/derived :dashboard-fonts st/state))
+
 (def dashboard-selected-project
   (l/derived (fn [state]
                (get-in state [:dashboard-local :selected-project]))
diff --git a/frontend/src/app/main/snap.cljs b/frontend/src/app/main/snap.cljs
index e73f4c777..021d2528a 100644
--- a/frontend/src/app/main/snap.cljs
+++ b/frontend/src/app/main/snap.cljs
@@ -284,7 +284,7 @@
   ([matches other]
    (let [merge-coord
          (fn [matches other]
-           
+
            (let [matches (into {} matches)
                  other (into {} other)
                  keys (set/union (keys matches) (keys other))]
@@ -305,7 +305,7 @@
           (if (< (mth/abs cur-val) (mth/abs other-val))
             current
             other))
-        
+
         min-match-coord
         (fn [matches]
           (if (and (seq matches) (not (empty? matches)))
diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs
index 5e0d543c3..020131834 100644
--- a/frontend/src/app/main/ui.cljs
+++ b/frontend/src/app/main/ui.cljs
@@ -90,6 +90,8 @@
     ["/settings"             :dashboard-team-settings]
     ["/projects"             :dashboard-projects]
     ["/search"               :dashboard-search]
+    ["/fonts"                :dashboard-fonts]
+    ["/fonts/providers"      :dashboard-font-providers]
     ["/libraries"            :dashboard-libraries]
     ["/projects/:project-id" :dashboard-files]]
 
@@ -135,12 +137,11 @@
       :dashboard-projects
       :dashboard-files
       :dashboard-libraries
+      :dashboard-fonts
+      :dashboard-font-providers
       :dashboard-team-members
       :dashboard-team-settings)
-     [:*
-      #_[:div.modal-wrapper
-         [:& app.main.ui.onboarding/release-notes-modal {:version "1.4"}]]
-      [:& dashboard {:route route}]]
+     [:& dashboard {:route route}]
 
      :viewer
      (let [index   (get-in route [:query-params :index])
diff --git a/frontend/src/app/main/ui/components/context_menu.cljs b/frontend/src/app/main/ui/components/context_menu.cljs
index 50cfbfbde..075d99544 100644
--- a/frontend/src/app/main/ui/components/context_menu.cljs
+++ b/frontend/src/app/main/ui/components/context_menu.cljs
@@ -11,7 +11,6 @@
    [app.main.ui.components.dropdown :refer [dropdown']]
    [app.main.ui.icons :as i]
    [app.common.uuid :as uuid]
-   [app.util.data :refer [classnames]]
    [app.util.dom :as dom]
    [app.util.object :as obj]))
 
@@ -22,18 +21,18 @@
   (assert (boolean? (gobj/get props "show")) "missing `show` prop")
   (assert (vector? (gobj/get props "options")) "missing `options` prop")
 
-  (let [open? (gobj/get props "show")
-        on-close (gobj/get props "on-close")
-        options (gobj/get props "options")
+  (let [open?         (gobj/get props "show")
+        on-close      (gobj/get props "on-close")
+        options       (gobj/get props "options")
         is-selectable (gobj/get props "selectable")
-        selected (gobj/get props "selected")
-        top (gobj/get props "top" 0)
-        left (gobj/get props "left" 0)
-        fixed? (gobj/get props "fixed?" false)
-        min-width? (gobj/get props "min-width?" false)
+        selected      (gobj/get props "selected")
+        top           (gobj/get props "top" 0)
+        left          (gobj/get props "left" 0)
+        fixed?        (gobj/get props "fixed?" false)
+        min-width?    (gobj/get props "min-width?" false)
 
-        local (mf/use-state {:offset 0
-                             :levels nil})
+        local         (mf/use-state {:offset 0
+                                     :levels nil})
 
         on-local-close
         (mf/use-callback
@@ -81,13 +80,13 @@
 
     (when (and open? (some? (:levels @local)))
       [:> dropdown' props
-       [:div.context-menu {:class (classnames :is-open open?
-                                              :fixed fixed?
-                                              :is-selectable is-selectable)
+       [:div.context-menu {:class (dom/classnames :is-open open?
+                                                  :fixed fixed?
+                                                  :is-selectable is-selectable)
                            :style {:top (+ top (:offset @local))
                                    :left left}}
         (let [level (-> @local :levels peek)]
-          [:ul.context-menu-items {:class (classnames :min-width min-width?)
+          [:ul.context-menu-items {:class (dom/classnames :min-width min-width?)
                                    :ref check-menu-offscreen}
            (when-let [parent-option (:parent-option level)]
              [:*
@@ -103,8 +102,7 @@
                (if (= option-name :separator)
                  [:li.separator]
                  [:li.context-menu-item
-                  {:class (classnames :is-selected (and selected
-                                                        (= option-name selected)))
+                  {:class (dom/classnames :is-selected (and selected (= option-name selected)))
                    :key option-name}
                   (if-not sub-options
                     [:a.context-menu-action {:on-click option-handler}
diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs
index 105ec5663..83a148a72 100644
--- a/frontend/src/app/main/ui/dashboard.cljs
+++ b/frontend/src/app/main/ui/dashboard.cljs
@@ -18,6 +18,7 @@
    [app.main.ui.dashboard.files :refer [files-section]]
    [app.main.ui.dashboard.libraries :refer [libraries-page]]
    [app.main.ui.dashboard.projects :refer [projects-section]]
+   [app.main.ui.dashboard.fonts :refer [fonts-page font-providers-page]]
    [app.main.ui.dashboard.search :refer [search-page]]
    [app.main.ui.dashboard.sidebar :refer [sidebar]]
    [app.main.ui.dashboard.team :refer [team-settings-page team-members-page]]
@@ -65,6 +66,12 @@
      :dashboard-projects
      [:& projects-section {:team team :projects projects}]
 
+     :dashboard-fonts
+     [:& fonts-page {:team team}]
+
+     :dashboard-font-providers
+     [:& font-providers-page {:team team}]
+
      :dashboard-files
      (when project
        [:& files-section {:team team :project project}])
@@ -121,17 +128,19 @@
        [:& (mf/provider ctx/current-page-id) {:value nil}
 
         [:section.dashboard-layout
-         [:& sidebar {:team team
-                      :projects projects
-                      :project project
-                      :profile profile
-                      :section section
-                      :search-term search-term}]
+         [:& sidebar
+          {:team team
+           :projects projects
+           :project project
+           :profile profile
+           :section section
+           :search-term search-term}]
          (when (and team (seq projects))
-           [:& dashboard-content {:projects projects
-                                  :profile profile
-                                  :project project
-                                  :section section
-                                  :search-term search-term
-                                  :team team}])]]]]]))
+           [:& dashboard-content
+            {:projects projects
+             :profile profile
+             :project project
+             :section section
+             :search-term search-term
+             :team team}])]]]]]))
 
diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs
new file mode 100644
index 000000000..73fe59a93
--- /dev/null
+++ b/frontend/src/app/main/ui/dashboard/fonts.cljs
@@ -0,0 +1,353 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.main.ui.dashboard.fonts
+  (:require
+   ["opentype.js" :as ot]
+   [app.common.media :as cm]
+   [app.common.uuid :as uuid]
+   [app.main.data.dashboard :as dd]
+   [app.main.data.dashboard.fonts :as df]
+   [app.main.data.modal :as modal]
+   [app.main.ui.components.file-uploader :refer [file-uploader]]
+   [app.main.ui.components.context-menu :refer [context-menu]]
+   [app.main.store :as st]
+   [app.main.repo :as rp]
+   [app.main.refs :as refs]
+   [app.main.ui.icons :as i]
+   [app.util.dom :as dom]
+   [app.util.i18n :as i18n :refer [tr]]
+   [app.util.logging :as log]
+   [app.util.keyboard :as kbd]
+   [app.util.router :as rt]
+   [app.util.webapi :as wa]
+   [cuerdas.core :as str]
+   [beicon.core :as rx]
+   [okulary.core :as l]
+   [rumext.alpha :as mf]))
+
+(log/set-level! :trace)
+
+(defn- use-set-page-title
+  [team section]
+  (mf/use-effect
+   (mf/deps team)
+   (fn []
+     (when team
+       (let [tname (if (:is-default team)
+                     (tr "dashboard.your-penpot")
+                     (:name team))]
+         (case section
+           :fonts (dom/set-html-title (tr "title.dashboard.fonts" tname))
+           :providers (dom/set-html-title (tr "title.dashboard.font-providers" tname))))))))
+
+(mf/defc header
+  {::mf/wrap [mf/memo]}
+  [{:keys [section team] :as props}]
+  (let [go-fonts
+        (mf/use-callback
+         (mf/deps team)
+         (st/emitf (rt/nav :dashboard-fonts {:team-id (:id team)})))
+
+        go-providers
+        (mf/use-callback
+         (mf/deps team)
+         (st/emitf (rt/nav :dashboard-font-providers {:team-id (:id team)})))]
+
+    (use-set-page-title team section)
+
+    [:header.dashboard-header
+     [:div.dashboard-title
+      [:h1 (tr "labels.fonts")]]
+     [:nav
+      [:ul
+       [:li {:class (when (= section :fonts) "active")}
+        [:a {:on-click go-fonts} (tr "labels.custom-fonts")]]
+       [:li {:class (when (= section :providers) "active")}
+        [:a {:on-click go-providers} (tr "labels.font-providers")]]]]
+
+     [:div]]))
+
+(defn- prepare-fonts
+  [blobs]
+  (letfn [(prepare [{:keys [font type name data] :as params}]
+            (let [family  (or (.getEnglishName ^js font "preferredFamily")
+                              (.getEnglishName ^js font "fontFamily"))
+                  variant (or (.getEnglishName ^js font "preferredSubfamily")
+                              (.getEnglishName ^js font "fontSubfamily"))]
+              {:content {:data (js/Uint8Array. data)
+                         :name name
+                         :type type}
+               :font-id (str "custom-" (str/slug family))
+               :font-family family
+               :font-weight (cm/parse-font-weight variant)
+               :font-style  (cm/parse-font-style variant)}))
+
+          (parse-mtype [mtype]
+            (case mtype
+              "application/vnd.oasis.opendocument.formula-template" "font/otf"
+              mtype))
+
+          (parse-font [{:keys [data] :as params}]
+            (try
+              (assoc params :font (ot/parse data))
+              (catch :default e
+                (log/warn :msg (str/fmt "skiping file %s, unsupported format" (:name params)))
+                nil)))
+
+          (read-blob [blob]
+            (->> (wa/read-file-as-array-buffer blob)
+                 (rx/map (fn [data]
+                           {:data data
+                            :name (.-name blob)
+                            :type (parse-mtype (.-type blob))}))))]
+
+    (->> (rx/from blobs)
+         (rx/mapcat read-blob)
+         (rx/map parse-font)
+         (rx/filter some?)
+         (rx/map prepare))))
+
+(mf/defc fonts-upload
+  [{:keys [team] :as props}]
+  (let [fonts     (mf/use-state {})
+        input-ref (mf/use-ref)
+
+        uploading (mf/use-state #{})
+
+        on-click
+        (mf/use-callback #(dom/click (mf/ref-val input-ref)))
+
+        font-key-fn
+        (mf/use-callback (juxt :font-family :font-weight :font-style))
+
+        on-selected
+        (mf/use-callback
+         (mf/deps team)
+         (fn [blobs]
+           (->> (prepare-fonts blobs)
+                (rx/subs (fn [{:keys [content] :as font}]
+                           (let [key (font-key-fn font)]
+                             (swap! fonts update key
+                                    (fn [val]
+                                      (-> (or val font)
+                                          (assoc :team-id (:id team))
+                                          (update :id #(or % (uuid/next)))
+                                          (update :data assoc (:type content) (:data content))
+                                          (update :names (fnil conj #{}) (:name content))
+                                          (dissoc :content))))))
+                         (fn [error]
+                           (js/console.error "error" error))))))
+
+        on-upload
+        (mf/use-callback
+         (mf/deps team)
+         (fn [item]
+           (let [key (font-key-fn item)]
+             (swap! uploading conj (:id item))
+             (->> (rp/mutation! :create-font-variant item)
+                  (rx/delay-at-least 2000)
+                  (rx/subs (fn [font]
+                             (swap! fonts dissoc key)
+                             (swap! uploading disj (:id item))
+                             (st/emit! (df/add-font font)))
+                           (fn [error]
+                             (js/console.log "error" error)))))))
+
+        on-delete
+        (mf/use-callback
+         (mf/deps team)
+         (fn [item]
+           (swap! fonts dissoc (font-key-fn item))))]
+
+    [:div.dashboard-fonts-upload
+     [:div.dashboard-fonts-hero
+      [:div.desc
+       [:h2 (tr "labels.upload-custom-fonts")]
+       [:& i18n/tr-html {:label "dashboard.fonts.hero-text1"}]
+
+       [:div.banner
+        [:div.icon i/msg-info]
+        [:div.content
+         [:& i18n/tr-html {:tag-name "span"
+                           :label "dashboard.fonts.hero-text2"}]]]]
+
+      [:div.btn-primary
+       {:on-click on-click}
+       [:span "Add custom font"]
+       [:& file-uploader {:input-id "font-upload"
+                          :accept cm/str-font-types
+                          :multi true
+                          :input-ref input-ref
+                          :on-selected on-selected}]]]
+
+     [:*
+      (for [item (sort-by :font-family (vals @fonts))]
+        (let [uploading? (contains? @uploading (:id item))]
+          [:div.font-item.table-row {:key (:id item)}
+           [:div.table-field.family
+            [:input {:type "text"
+                     :default-value (:font-family item)}]]
+           [:div.table-field.variant
+            [:span (cm/font-weight->name (:font-weight item))]
+            (when (not= "normal" (:font-style item))
+              [:span " " (str/capital (:font-style item))])]
+           [:div.table-field.filenames
+            (for [item (:names item)]
+              [:span item])]
+
+           [:div.table-field.options
+            [:button.btn-primary.upload-button
+             {:on-click #(on-upload item)
+              :class (dom/classnames :disabled uploading?)
+              :disabled uploading?}
+             (if uploading?
+               (tr "labels.uploading")
+               (tr "labels.upload"))]
+            [:span.icon.close {:on-click #(on-delete item)} i/close]]]))]]))
+
+(mf/defc installed-font
+  [{:keys [font] :as props}]
+  (let [open-menu? (mf/use-state false)
+        edit?      (mf/use-state false)
+        state      (mf/use-var (:font-family font))
+
+        on-change
+        (mf/use-callback
+         (mf/deps font)
+         (fn [event]
+           (reset! state (dom/get-target-val event))))
+
+        on-save
+        (mf/use-callback
+         (mf/deps font)
+         (fn [event]
+           (let [font (assoc font :font-family @state)]
+             (st/emit! (df/update-font font))
+             (reset! edit? false))))
+
+        on-key-down
+        (mf/use-callback
+         (mf/deps font)
+         (fn [event]
+           (when (kbd/enter? event)
+             (on-save event))))
+
+        on-cancel
+        (mf/use-callback
+         (mf/deps font)
+         (fn [event]
+           (reset! edit? false)
+           (reset! state (:font-family font))))
+
+        delete-fn
+        (mf/use-callback
+         (mf/deps font)
+         (st/emitf (df/delete-font font)))
+
+        on-delete
+        (mf/use-callback
+         (mf/deps font)
+         (st/emitf (modal/show
+                    {:type :confirm
+                     :title (tr "modals.delete-font.title")
+                     :message (tr "modals.delete-font.message")
+                     :accept-label (tr "labels.delete")
+                     :on-accept delete-fn})))]
+
+
+    [:div.font-item.table-row {:key (:id font)}
+     [:div.table-field.family
+      (if @edit?
+        [:input {:type "text"
+                 :default-value @state
+                 :on-key-down on-key-down
+                 :on-change on-change}]
+        [:span (:font-family font)])]
+
+     [:div.table-field.variant
+      [:span (cm/font-weight->name (:font-weight font))]
+      (when (not= "normal" (:font-style font))
+        [:span " " (str/capital (:font-style font))])]
+
+     [:div]
+
+     (if @edit?
+       [:div.table-field.options
+        [:button.btn-primary
+         {:disabled (str/blank? @state)
+          :on-click on-save
+          :class (dom/classnames :btn-disabled (str/blank? @state))}
+          "Save"]
+        [:span.icon.close {:on-click on-cancel} i/close]]
+
+       [:div.table-field.options
+        [:span.icon {:on-click #(reset! open-menu? true)} i/actions]
+        [:& context-menu
+         {:on-close #(reset! open-menu? false)
+          :show @open-menu?
+          :fixed? false
+          :top -15
+          :left -115
+          :options [[(tr "labels.edit") #(reset! edit? true)]
+                    [(tr "labels.delete") on-delete]]}]])]))
+
+
+(mf/defc installed-fonts
+  [{:keys [team fonts] :as props}]
+  (let [sterm (mf/use-state "")
+
+        matches?
+        #(str/includes? (str/lower (:font-family %)) @sterm)
+
+        on-change
+        (mf/use-callback
+         (fn [event]
+           (let [val (dom/get-target-val event)]
+             (reset! sterm val))))]
+
+    [:div.dashboard-installed-fonts
+     [:h3 (tr "labels.installed-fonts")]
+     [:div.installed-fonts-header
+      [:div.table-field.family (tr "labels.font-family")]
+      [:div.table-field.variant (tr "labels.font-variant")]
+      [:div]
+      [:div.table-field.search-input
+       [:input {:placeholder (tr "labels.search-font")
+                :default-value ""
+                :on-change on-change
+                }]]]
+     (for [[font-id fonts] (->> fonts
+                                (filter matches?)
+                                (group-by :font-id))]
+       [:div.fonts-group
+        (for [font (sort-by (juxt :font-weight :font-style) fonts)]
+          [:& installed-font {:key (:id font) :font font}])])]))
+
+
+(mf/defc fonts-page
+  [{:keys [team] :as props}]
+  (let [fonts-map (mf/deref refs/dashboard-fonts)
+        fonts     (vals fonts-map)]
+
+    (mf/use-effect
+     (mf/deps team)
+     (st/emitf (df/fetch-fonts team)))
+
+    [:*
+     [:& header {:team team :section :fonts}]
+     [:section.dashboard-container.dashboard-fonts
+      [:& fonts-upload {:team team}]
+
+      (when fonts
+        [:& installed-fonts {:team team
+                             :fonts fonts}])]]))
+(mf/defc font-providers-page
+  [{:keys [team] :as props}]
+  [:*
+   [:& header {:team team :section :providers}]
+   [:section.dashboard-container
+    [:span "hello world font providers"]]])
diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs
index f2b331a0e..459b7949d 100644
--- a/frontend/src/app/main/ui/dashboard/sidebar.cljs
+++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs
@@ -28,7 +28,7 @@
    [app.util.avatars :as avatars]
    [app.util.dom :as dom]
    [app.util.dom.dnd :as dnd]
-   [app.util.i18n :as i18n :refer [t tr]]
+   [app.util.i18n :as i18n :refer [tr]]
    [app.util.keyboard :as kbd]
    [app.util.object :as obj]
    [app.util.router :as rt]
@@ -47,10 +47,11 @@
         selected-project (:selected-project dstate)
         edit-id          (:project-for-edit dstate)
 
-        local   (mf/use-state {:menu-open false
-                               :menu-pos nil
-                               :edition? (= (:id item) edit-id)
-                               :dragging? false})
+        local            (mf/use-state
+                          {:menu-open false
+                           :menu-pos nil
+                           :edition? (= (:id item) edit-id)
+                           :dragging? false})
 
         on-click
         (mf/use-callback
@@ -60,11 +61,13 @@
                                                :project-id (:id item)}))))
 
         on-menu-click
-        (mf/use-callback (fn [event]
-                           (let [position (dom/get-client-position event)]
-                             (dom/prevent-default event)
-                             (swap! local assoc :menu-open true
-                                                :menu-pos position))))
+        (mf/use-callback
+         (fn [event]
+           (let [position (dom/get-client-position event)]
+             (dom/prevent-default event)
+             (swap! local assoc
+                    :menu-open true
+                    :menu-pos position))))
 
         on-menu-close
         (mf/use-callback #(swap! local assoc :menu-open false))
@@ -139,7 +142,7 @@
                        :on-menu-close on-menu-close}]]))
 
 (mf/defc sidebar-search
-  [{:keys [search-term team-id locale] :as props}]
+  [{:keys [search-term team-id] :as props}]
   (let [search-term (or search-term "")
         focused?    (mf/use-state false)
         emit!       (mf/use-memo #(f/debounce st/emit! 500))
@@ -183,7 +186,7 @@
       {:key :images-search-box
        :id "search-input"
        :type "text"
-       :placeholder (t locale "dashboard.search-placeholder")
+       :placeholder (tr "dashboard.search-placeholder")
        :default-value search-term
        :auto-complete "off"
        :on-focus on-search-focus
@@ -201,7 +204,7 @@
         i/search])]))
 
 (mf/defc teams-selector-dropdown
-  [{:keys [team profile locale] :as props}]
+  [{:keys [team profile] :as props}]
   (let [show-dropdown? (mf/use-state false)
         teams          (mf/deref refs/teams)
 
@@ -216,11 +219,11 @@
             (st/emit! (rt/nav :dashboard-projects {:team-id team-id}))))]
 
     [:ul.dropdown.teams-dropdown
-     [:li.title (t locale "dashboard.switch-team")]
+     [:li.title (tr "dashboard.switch-team")]
      [:hr]
      [:li.team-name {:on-click (partial team-selected (:default-team-id profile))}
       [:span.team-icon i/logo-icon]
-      [:span.team-text (t locale "dashboard.your-penpot")]]
+      [:span.team-text (tr "dashboard.your-penpot")]]
 
      (for [team (remove :is-default (vals teams))]
        [:* {:key (:id team)}
@@ -231,7 +234,7 @@
 
      [:hr]
      [:li.action {:on-click on-create-clicked}
-      (t locale "dashboard.create-new-team")]]))
+      (tr "dashboard.create-new-team")]]))
 
 (s/def ::member-id ::us/uuid)
 (s/def ::leave-modal-form
@@ -292,7 +295,7 @@
 
 
 (mf/defc team-options-dropdown
-  [{:keys [team locale profile] :as props}]
+  [{:keys [team profile] :as props}]
   (let [members (mf/use-state [])
 
         go-members
@@ -341,9 +344,9 @@
          (mf/deps team)
          (st/emitf (modal/show
                     {:type :confirm
-                     :title (t locale "modals.leave-confirm.title")
-                     :message (t locale "modals.leave-confirm.message")
-                     :accept-label (t locale "modals.leave-confirm.accept")
+                     :title (tr "modals.leave-confirm.title")
+                     :message (tr "modals.leave-confirm.message")
+                     :accept-label (tr "modals.leave-confirm.accept")
                      :on-accept leave-fn})))
 
         on-leave-as-owner-clicked
@@ -366,9 +369,9 @@
          (mf/deps team)
          (st/emitf (modal/show
                     {:type :confirm
-                     :title (t locale "modals.delete-team-confirm.title")
-                     :message (t locale "modals.delete-team-confirm.message")
-                     :accept-label (t locale "modals.delete-team-confirm.accept")
+                     :title (tr "modals.delete-team-confirm.title")
+                     :message (tr "modals.delete-team-confirm.message")
+                     :accept-label (tr "modals.delete-team-confirm.accept")
                      :on-accept delete-fn})))]
 
     (mf/use-layout-effect
@@ -378,25 +381,25 @@
             (rx/subs #(reset! members %)))))
 
     [:ul.dropdown.options-dropdown
-     [:li {:on-click go-members} (t locale "labels.members")]
-     [:li {:on-click go-settings} (t locale "labels.settings")]
+     [:li {:on-click go-members} (tr "labels.members")]
+     [:li {:on-click go-settings} (tr "labels.settings")]
      [:hr]
-     [:li {:on-click on-rename-clicked} (t locale "labels.rename")]
+     [:li {:on-click on-rename-clicked} (tr "labels.rename")]
 
      (cond
        (:is-owner team)
-       [:li {:on-click on-leave-as-owner-clicked} (t locale "dashboard.leave-team")]
+       [:li {:on-click on-leave-as-owner-clicked} (tr "dashboard.leave-team")]
 
        (> (count @members) 1)
-       [:li {:on-click on-leave-clicked}  (t locale "dashboard.leave-team")])
+       [:li {:on-click on-leave-clicked}  (tr "dashboard.leave-team")])
 
 
      (when (:is-owner team)
-       [:li {:on-click on-delete-clicked} (t locale "dashboard.delete-team")])]))
+       [:li {:on-click on-delete-clicked} (tr "dashboard.delete-team")])]))
 
 
 (mf/defc sidebar-team-switch
-  [{:keys [team profile locale] :as props}]
+  [{:keys [team profile] :as props}]
   (let [show-dropdown? (mf/use-state false)
 
         show-team-opts-ddwn? (mf/use-state false)
@@ -408,7 +411,7 @@
        (if (:is-default team)
          [:div.team-name
           [:span.team-icon i/logo-icon]
-          [:span.team-text (t locale "dashboard.default-team-name")]]
+          [:span.team-text (tr "dashboard.default-team-name")]]
          [:div.team-name
           [:span.team-icon
            [:img {:src (cfg/resolve-team-photo-url team)}]]
@@ -425,23 +428,22 @@
      [:& dropdown {:show @show-teams-ddwn?
                    :on-close #(reset! show-teams-ddwn? false)}
       [:& teams-selector-dropdown {:team team
-                                   :profile profile
-                                   :locale locale}]]
+                                   :profile profile}]]
 
      [:& dropdown {:show @show-team-opts-ddwn?
                    :on-close #(reset! show-team-opts-ddwn? false)}
       [:& team-options-dropdown {:team team
-                                 :profile profile
-                                 :locale locale}]]]))
+                                 :profile profile}]]]))
 
 (mf/defc sidebar-content
-  [{:keys [locale projects profile section team project search-term] :as props}]
+  [{:keys [projects profile section team project search-term] :as props}]
   (let [default-project-id
         (->> (vals projects)
              (d/seek :is-default)
              (:id))
 
         projects?   (= section :dashboard-projects)
+        fonts?      (= section :dashboard-fonts)
         libs?       (= section :dashboard-libraries)
         drafts?     (and (= section :dashboard-files)
                          (= (:id project) default-project-id))
@@ -451,6 +453,11 @@
          (mf/deps team)
          (st/emitf (rt/nav :dashboard-projects {:team-id (:id team)})))
 
+        go-fonts
+        (mf/use-callback
+         (mf/deps team)
+         (st/emitf (rt/nav :dashboard-fonts {:team-id (:id team)})))
+
         go-drafts
         (mf/use-callback
          (mf/deps team default-project-id)
@@ -469,29 +476,36 @@
              (filter :is-pinned))]
 
     [:div.sidebar-content
-     [:& sidebar-team-switch {:team team :profile profile :locale locale}]
+     [:& sidebar-team-switch {:team team :profile profile}]
      [:hr]
      [:& sidebar-search {:search-term search-term
-                         :team-id (:id team)
-                         :locale locale}]
+                         :team-id (:id team)}]
      [:div.sidebar-content-section
       [:ul.sidebar-nav.no-overflow
        [:li.recent-projects
         {:on-click go-projects
          :class-name (when projects? "current")}
-        [:span.element-title (t locale "labels.projects")]]
+        [:span.element-title (tr "labels.projects")]]
 
        [:li {:on-click go-drafts
              :class-name (when drafts? "current")}
-        [:span.element-title (t locale "labels.drafts")]]
+        [:span.element-title (tr "labels.drafts")]]
 
 
        [:li {:on-click go-libs
              :class-name (when libs? "current")}
-        [:span.element-title (t locale "labels.shared-libraries")]]]]
+        [:span.element-title (tr "labels.shared-libraries")]]]]
 
      [:hr]
 
+     [:div.sidebar-content-section
+      [:ul.sidebar-nav.no-overflow
+       [:li.recent-projects
+        {:on-click go-fonts
+         :class-name (when fonts? "current")}
+        [:span.element-title (tr "labels.fonts")]]]]
+
+     [:hr]
      [:div.sidebar-content-section
       (if (seq pinned-projects)
         [:ul.sidebar-nav
@@ -504,11 +518,11 @@
              :selected? (= (:id item) (:id project))}])]
         [:div.sidebar-empty-placeholder
          [:span.icon i/pin]
-         [:span.text (t locale "dashboard.no-projects-placeholder")]])]]))
+         [:span.text (tr "dashboard.no-projects-placeholder")]])]]))
 
 
 (mf/defc profile-section
-  [{:keys [profile locale team] :as props}]
+  [{:keys [profile team] :as props}]
   (let [show  (mf/use-state false)
         photo (cfg/resolve-profile-photo-url profile)
 
@@ -530,18 +544,18 @@
       [:ul.dropdown
        [:li {:on-click (partial on-click :settings-profile)}
         [:span.icon i/user]
-        [:span.text (t locale "labels.profile")]]
+        [:span.text (tr "labels.profile")]]
        [:li {:on-click (partial on-click :settings-password)}
         [:span.icon i/lock]
-        [:span.text (t locale "labels.password")]]
+        [:span.text (tr "labels.password")]]
        [:li {:on-click (partial on-click (da/logout))}
         [:span.icon i/exit]
-        [:span.text (t locale "labels.logout")]]
+        [:span.text (tr "labels.logout")]]
 
        (when cfg/feedback-enabled
          [:li.feedback {:on-click (partial on-click :settings-feedback)}
           [:span.icon i/msg-info]
-          [:span.text (t locale "labels.give-feedback")]
+          [:span.text (tr "labels.give-feedback")]
           [:span.primary-badge "ALPHA"]])]]]
 
      (when (and team profile)
@@ -552,15 +566,11 @@
   {::mf/wrap-props false
    ::mf/wrap [mf/memo]}
   [props]
-  (let [locale  (mf/deref i18n/locale)
-        team    (obj/get props "team")
-        profile (obj/get props "profile")
-        props   (-> (obj/clone props)
-                    (obj/set! "locale" locale))]
+  (let [team    (obj/get props "team")
+        profile (obj/get props "profile")]
     [:div.dashboard-sidebar
      [:div.sidebar-inside
       [:> sidebar-content props]
       [:& profile-section
        {:profile profile
-        :team team
-        :locale locale}]]]))
+        :team team}]]]))
diff --git a/frontend/src/app/main/ui/messages.cljs b/frontend/src/app/main/ui/messages.cljs
index e1848c969..70aecb9b2 100644
--- a/frontend/src/app/main/ui/messages.cljs
+++ b/frontend/src/app/main/ui/messages.cljs
@@ -18,33 +18,33 @@
 (mf/defc banner
   [{:keys [type position status controls content actions on-close] :as props}]
   [:div.banner {:class (dom/classnames
-                         :warning  (= type :warning)
-                         :error    (= type :error)
-                         :success  (= type :success)
-                         :info     (= type :info)
-                         :fixed    (= position :fixed)
-                         :floating (= position :floating)
-                         :inline   (= position :inline)
-                         :hide     (= status :hide))}
+                        :warning  (= type :warning)
+                        :error    (= type :error)
+                        :success  (= type :success)
+                        :info     (= type :info)
+                        :fixed    (= position :fixed)
+                        :floating (= position :floating)
+                        :inline   (= position :inline)
+                        :hide     (= status :hide))}
    [:div.wrapper
-     [:div.icon (case type
-                  :warning i/msg-warning
-                  :error i/msg-error
-                  :success i/msg-success
-                  :info i/msg-info
-                  i/msg-error)]
-     [:div.content {:class (dom/classnames
-                             :inline-actions (= controls :inline-actions)
-                             :bottom-actions (= controls :bottom-actions))}
-      content
-      (when (or (= controls :bottom-actions) (= controls :inline-actions))
-        [:div.actions
-          (for [action actions]
-            [:div.btn-secondary.btn-small {:key (uuid/next)
-                                           :on-click (:callback action)}
-             (:label action)])])]
-   (when (= controls :close)
-     [:div.btn-close {:on-click on-close} i/close])]])
+    [:div.icon (case type
+                 :warning i/msg-warning
+                 :error i/msg-error
+                 :success i/msg-success
+                 :info i/msg-info
+                 i/msg-error)]
+    [:div.content {:class (dom/classnames
+                           :inline-actions (= controls :inline-actions)
+                           :bottom-actions (= controls :bottom-actions))}
+     content
+     (when (or (= controls :bottom-actions) (= controls :inline-actions))
+       [:div.actions
+        (for [action actions]
+          [:div.btn-secondary.btn-small {:key (uuid/next)
+                                         :on-click (:callback action)}
+           (:label action)])])]
+    (when (= controls :close)
+      [:div.btn-close {:on-click on-close} i/close])]])
 
 (mf/defc notifications
   []
diff --git a/frontend/src/app/main/ui/workspace/left_toolbar.cljs b/frontend/src/app/main/ui/workspace/left_toolbar.cljs
index f948e8237..a2284e4ed 100644
--- a/frontend/src/app/main/ui/workspace/left_toolbar.cljs
+++ b/frontend/src/app/main/ui/workspace/left_toolbar.cljs
@@ -50,7 +50,7 @@
         [:*
          i/image
          [:& file-uploader {:input-id "image-upload"
-                            :accept cm/str-media-types
+                            :accept cm/str-image-types
                             :multi true
                             :input-ref ref
                             :on-selected on-files-selected}]]]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs
index 059c816bb..c92773181 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs
@@ -212,7 +212,7 @@
           (fn [path]
             (fn [event]
               (dom/stop-propagation event)
-              (swap! state update :folded-groups 
+              (swap! state update :folded-groups
                      toggle-folded-group path))))
 
         on-group
@@ -400,7 +400,7 @@
           (fn [path]
             (fn [event]
               (dom/stop-propagation event)
-              (swap! state update :folded-groups 
+              (swap! state update :folded-groups
                      toggle-folded-group path))))
 
         on-group
@@ -426,7 +426,7 @@
       (when local?
         [:div.assets-button {:on-click add-graphic}
          i/plus
-         [:& file-uploader {:accept cm/str-media-types
+         [:& file-uploader {:accept cm/str-image-types
                             :multi true
                             :input-ref input-ref
                             :on-selected on-file-selected}]])]
diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs
index 9f7e24b27..f9c4f18ca 100644
--- a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs
@@ -116,7 +116,6 @@
         snap-lines (->> (into (process-snap-lines @state :x)
                               (process-snap-lines @state :y))
                         (into #{}))]
-
     (mf/use-effect
      (fn []
        (let [sub (->> subject
diff --git a/frontend/src/app/util/i18n.cljs b/frontend/src/app/util/i18n.cljs
index ac66402b0..88b7cc807 100644
--- a/frontend/src/app/util/i18n.cljs
+++ b/frontend/src/app/util/i18n.cljs
@@ -10,6 +10,7 @@
    [app.config :as cfg]
    [app.util.globals :as globals]
    [app.util.storage :refer [storage]]
+   [app.util.object :as obj]
    [app.util.transit :as t]
    [beicon.core :as rx]
    [cuerdas.core :as str]
@@ -136,6 +137,13 @@
   ([code] (t @locale code))
   ([code & args] (apply t @locale code args)))
 
+(mf/defc tr-html
+  {::mf/wrap-props false}
+  [props]
+  (let [label    (obj/get props "label")
+        tag-name (obj/get props "tag-name" "p")]
+    [:> tag-name {:dangerouslySetInnerHTML #js {:__html (tr label)}}]))
+
 ;; DEPRECATED
 (defn use-locale
   []
diff --git a/frontend/src/app/util/webapi.cljs b/frontend/src/app/util/webapi.cljs
index 9e289c83e..9664de3a8 100644
--- a/frontend/src/app/util/webapi.cljs
+++ b/frontend/src/app/util/webapi.cljs
@@ -29,6 +29,10 @@
   [file]
   (file-reader #(.readAsText %1 file)))
 
+(defn read-file-as-array-buffer
+  [file]
+  (file-reader #(.readAsArrayBuffer %1 file)))
+
 (defn read-file-as-data-url
   [file]
   (file-reader #(.readAsDataURL ^js %1 file)))
diff --git a/frontend/translations/en.po b/frontend/translations/en.po
index eb0eee9d9..422b472e6 100644
--- a/frontend/translations/en.po
+++ b/frontend/translations/en.po
@@ -1,25 +1,68 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR Free Software Foundation, Inc.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-# 
-#, fuzzy
 msgid ""
 msgstr ""
-"Project-Id-Version: PACKAGE VERSION\n"
-"PO-Revision-Date: 2021-04-22 13:43+0200\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
+"PO-Revision-Date: 2021-04-14 13:44+0000\n"
+"Last-Translator: Andrey Antukh <niwi@niwi.nz>\n"
+"Language-Team: Spanish "
+"<https://hosted.weblate.org/projects/penpot/frontend/en/>\n"
+"Language: es\n"
 "MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=iso-8859-1\n"
+"Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.6-dev\n"
+
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "title.dashboard.fonts"
+msgstr "Fonts - %s - Penpot"
+
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "title.dashboard.font-providers"
+msgstr "Font Providers - %s - Penpot"
+
+msgid "labels.upload"
+msgstr "Upload"
+
+msgid "labels.uploading"
+msgstr "Uploading..."
+
+msgid "modals.delete-font.title"
+msgstr "Deleting font"
+
+msgid "modals.delete-font.message"
+msgstr "Are you sure you want to delete this font? It will not load if is used in a file."
+
+msgid "labels.fonts"
+msgstr "Fonts"
+
+msgid "labels.installed-fonts"
+msgstr "Installed fonts"
+
+msgid "labels.font-family"
+msgstr "Font Family"
+
+msgid "labels.font-variant"
+msgstr "Style"
+
+msgid "labels.custom-fonts"
+msgstr "Custom fonts"
+
+msgid "labels.search-font"
+msgstr "Search font"
+
+msgid "labels.font-providers"
+msgstr "Font providers"
+
+msgid "labels.upload-custom-fonts"
+msgstr "Upload custom fonts"
+
+#, markdown
+msgid "dashboard.fonts.hero-text1"
+msgstr "Any web font you upload here will be added to the font family list available at the text properties of the files of this team. Fonts with the same font family name will be grouped as a **single font family**. You can upload fonts with the following formats: **TTF, OTF and WOFF** (only one will be needed)."
+
+#, markdown
+msgid "dashboard.fonts.hero-text2"
+msgstr "You should only upload fonts you own or have license to use in Penpot. Find out more in the Content rights section of [Penpot's Terms of Service](https://penpot.app/terms.html). You also might want to read about [font licensing](2)."
 
-# ~ msgid ""
-# ~ msgstr ""
-# ~ "Language: en\n"
-# ~ "MIME-Version: 1.0\n"
-# ~ "Content-Type: text/plain; charset=utf-8\n"
-# ~ "Content-Transfer-Encoding: 8bit\n"
-# ~ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 #: src/app/main/ui/auth/register.cljs
 msgid "auth.already-have-account"
 msgstr "Already have an account?"
diff --git a/frontend/translations/es.po b/frontend/translations/es.po
index 46bacd4eb..371fdd653 100644
--- a/frontend/translations/es.po
+++ b/frontend/translations/es.po
@@ -11,6 +11,68 @@ msgstr ""
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
 "X-Generator: Weblate 4.6-dev\n"
 
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "title.dashboard.fonts"
+msgstr "Fuentes - %s - Penpot"
+
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "title.dashboard.font-providers"
+msgstr "Proveedores de fuentes - %s - Penpot"
+
+msgid "labels.upload"
+msgstr "Subir"
+
+msgid "labels.uploading"
+msgstr "Subiendo..."
+
+msgid "modals.delete-font.title"
+msgstr "Eliminando fuente"
+
+msgid "modals.delete-font.message"
+msgstr "Are you sure you want to delete this font? It will not load if is used in a file."
+msgstr "¿Estas seguro que quieres eliminar esta fuente? La fuente dejara de cargar si es usada en algun fichero."
+
+msgid "labels.fonts"
+msgstr "Fuentes"
+
+msgid "labels.installed-fonts"
+msgstr "Fuentes instaladas"
+
+msgid "labels.font-family"
+msgstr "Familia de fuente"
+
+msgid "labels.font-variant"
+msgstr "Estilo"
+
+msgid "labels.custom-fonts"
+msgstr "Fuentes personalizadas"
+
+msgid "labels.search-font"
+msgstr "Buscar fuente"
+
+msgid "labels.font-providers"
+msgstr "Proveedores de fuentes"
+
+msgid "labels.upload-custom-fonts"
+msgstr "Subir fuente"
+
+#, markdown
+msgid "dashboard.fonts.hero-text1"
+msgstr "Any web font you upload here will be added to the font family list available at the text properties of the files of this team. Fonts with the same font family name will be grouped as a **single font family**. You can upload fonts with the following formats: **TTF, OTF and WOFF** (only one will be needed)."
+
+#, markdown
+msgid "dashboard.fonts.hero-text2"
+msgstr "You should only upload fonts you own or have license to use in Penpot. Find out more in the Content rights section of [Penpot's Terms of Service](https://penpot.app/terms.html). You also might want to read about [font licensing](2)."
+
+
+
+
+
+
+
+
+
+
 #: src/app/main/ui/auth/register.cljs
 msgid "auth.already-have-account"
 msgstr "¿Tienes ya una cuenta?"
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 021d9aff6..43a22f305 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -3120,6 +3120,11 @@ map-visit@^1.0.0:
   dependencies:
     object-visit "^1.0.0"
 
+marked@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.3.tgz#3551c4958c4da36897bda2a16812ef1399c8d6b0"
+  integrity sha512-5otztIIcJfPc2qGTN8cVtOJEjNJZ0jwa46INMagrYfk0EvqtRuEHLsEe0LrFS0/q+ZRKT0+kXK7P2T1AN5lWRA==
+
 matchdep@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e"
@@ -3644,6 +3649,14 @@ one-time@^1.0.0:
   dependencies:
     fn.name "1.x.x"
 
+opentype.js@^1.3.3:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-1.3.3.tgz#65b8645b090a1ad444065b784d442fa19d1061f6"
+  integrity sha512-/qIY/+WnKGlPIIPhbeNjynfD2PO15G9lA/xqlX2bDH+4lc3Xz5GCQ68mqxj3DdUv6AJqCeaPvuAoH8mVL0zcuA==
+  dependencies:
+    string.prototype.codepointat "^0.2.1"
+    tiny-inflate "^1.0.3"
+
 ordered-read-streams@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e"
@@ -4364,9 +4377,9 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
     inherits "^2.0.1"
 
 rxjs@~7.0.0-beta.12:
-  version "7.0.0-rc.1"
-  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.0.0-rc.1.tgz#11f368e740e2b3cfe805891be127d07391673654"
-  integrity sha512-FVFOeT+eGdbcPe+uH+cWnEElrU4LiDMrlstNSUpI3MPErICLtVoUCbKrF+n+8DYemHDe7wPqYtuNEYTM3ur3xw==
+  version "7.0.0-rc.2"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.0.0-rc.2.tgz#bd5b18ff9b60ca28ea4b3a824419035007064fdf"
+  integrity sha512-81+TFxK8hUK3tmJ9TPon07bgun2ASgZ8OXumUuWSAnktSAzTvubw4NCJTr0Tc0lO9IfTThi5z3GDVlmjY3n5ug==
   dependencies:
     tslib "~2.1.0"
 
@@ -4480,7 +4493,7 @@ shadow-cljs-jar@1.3.2:
   resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b"
   integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg==
 
-shadow-cljs@^2.11.20:
+shadow-cljs@2.12.5:
   version "2.12.5"
   resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.12.5.tgz#d3cf29fc1f1e02dd875939549419979e0feadbf4"
   integrity sha512-o3xo3coRgnlkI/iI55ccHjj6AU3F1+ovk3hhK86e3P2JGGOpNTAwsGNxUpMC5JAwS9Nz0v6sSk73hWjEOnm6fQ==
@@ -4796,6 +4809,11 @@ string-width@^3.0.0, string-width@^3.1.0:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^5.1.0"
 
+string.prototype.codepointat@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc"
+  integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==
+
 string.prototype.trimend@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
@@ -5021,6 +5039,11 @@ timers-ext@^0.1.7:
     es5-ext "~0.10.46"
     next-tick "1"
 
+tiny-inflate@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
+  integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
+
 to-absolute-glob@^2.0.0:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b"