From 55d2abefb4a64853d93244f56789f35b3e799ee2 Mon Sep 17 00:00:00 2001 From: Swrup Date: Wed, 29 May 2024 19:16:48 +0200 Subject: [PATCH] big squish --- .ocamlformat | 2 +- README.md | 15 + doc/dune | 3 - doc/index.mld | 19 - dune-project | 48 +- example/dune | 3 - example/main.ml | 1 - permap.opam | 42 +- src/api.ml | 334 ++ src/app.ml | 94 - src/{content => }/assets/css/leaflet.css | 0 src/assets/css/style.css | 411 ++ src/{content => }/assets/img/atom.svg | 0 .../assets/img/default_avatar.png | Bin src/{content => }/assets/img/favicon.png | Bin src/{content => }/assets/img/layers-2x.png | Bin src/{content => }/assets/img/layers.png | Bin .../assets/img/marker-icon-2x.png | Bin src/{content => }/assets/img/marker-icon.png | Bin .../assets/img/marker-shadow.png | Bin src/assets/js/dune | 8 + src/babillard.ml | 425 -- src/babillard_page.eml.html | 31 - src/caqti_db.ml | 48 + src/caqti_db.mli | 17 + src/catalog_page.eml.html | 8 - src/client/client_types.ml | 207 + src/client/db.ml | 129 + src/client/dune | 17 + src/client/events.ml | 8 + src/client/form_kind.ml | 50 + src/client/html.ml | 366 ++ src/client/html_form.ml | 391 ++ src/client/html_post.ml | 335 ++ src/client/html_thread.ml | 156 + src/client/html_util.ml | 80 + src/client/leaflet_map.ml | 199 + src/client/main.ml | 24 + src/client/model.ml | 303 + src/client/navigation.ml | 98 + src/client/network.ml | 214 + src/client/page.ml | 193 + src/client/storage.ml | 43 + src/client/util.ml | 89 + src/comment/ast.ml | 9 + src/comment/comment.ml | 85 + src/comment/dune | 9 + src/comment/lexer.ml | 30 + src/comment/parser.mly | 30 + src/content/about.md | 23 - src/content/assets/css/bootstrap.min.css | 6 - src/content/assets/css/style.css | 169 - src/content/assets/js/bootstrap.bundle.min.js | 6 - src/content/assets/js/dune | 26 - src/content/emoji-test.txt | 4991 ----------------- src/content/register.md | 19 - src/db.ml | 48 - src/db.mli | 13 - src/db_image.ml | 162 + src/db_image.mli | 28 + src/db_post.ml | 269 + src/db_post.mli | 28 + src/db_user.ml | 125 + src/db_user.mli | 25 + src/delete_page.eml.html | 20 - src/discuss.ml | 129 - src/dune | 161 +- src/emojid.ml | 86 - src/emojid.mli | 5 - src/err.ml | 46 + src/html.ml | 35 + src/html.mli | 7 + src/image.ml | 240 +- src/image.mli | 5 + src/js/babillard.ml | 124 - src/js/catalog.ml | 0 src/js/dune | 51 - src/js/map.ml | 99 - src/js/newthread.ml | 3 - src/js/post_form.ml | 52 - src/js/pretty_post.ml | 203 - src/js/thread.ml | 0 src/js/utils.ml | 18 - src/json_data.ml | 364 ++ src/json_data.mli | 68 + src/login.eml.html | 20 - src/moderation.ml | 107 + src/permap.ml | 553 +- src/post.ml | 84 + src/post_form.eml.html | 34 - src/pp_babillard.ml | 364 -- src/register.eml.html | 16 - src/report_page.eml.html | 22 - src/syntax.ml | 81 +- src/template.eml.html | 84 - src/thread_page.eml.html | 16 - src/types.mli | 104 + src/user.ml | 427 +- src/user_account.eml.html | 28 - src/user_profile.eml.html | 40 - src/util.ml | 4 + src/utils.ml | 32 - src/validate_str.ml | 114 + src/validate_str.mli | 32 + src/virtual/config.mli | 51 + src/virtual/config_serv.mli | 11 + src/virtual/dir.mli | 4 + src/virtual/dune | 28 + src/virtual/impl/dir.ml | 13 + src/virtual/impl/dune | 39 + src/virtual/impl/make_config_impl.ml | 1 + src/virtual/make_config_lib.ml | 217 + test/config.scfg | 25 + test/dune | 25 +- test/img/fff.exif.png | Bin 0 -> 356 bytes test/img/fff.gif | Bin 0 -> 41 bytes test/img/fff.jpeg | Bin 0 -> 694 bytes test/img/fff.png | Bin 0 -> 109 bytes test/main.ml | 1 - test/test.ml | 419 ++ test/util.ml | 46 + test/virtual/dir.ml | 9 + test/virtual/dune | 44 + test/virtual/make_config_test.ml | 1 + 124 files changed, 6931 insertions(+), 8393 deletions(-) delete mode 100644 doc/dune delete mode 100644 doc/index.mld delete mode 100644 example/dune delete mode 100644 example/main.ml create mode 100644 src/api.ml delete mode 100644 src/app.ml rename src/{content => }/assets/css/leaflet.css (100%) create mode 100644 src/assets/css/style.css rename src/{content => }/assets/img/atom.svg (100%) rename src/{content => }/assets/img/default_avatar.png (100%) rename src/{content => }/assets/img/favicon.png (100%) rename src/{content => }/assets/img/layers-2x.png (100%) rename src/{content => }/assets/img/layers.png (100%) rename src/{content => }/assets/img/marker-icon-2x.png (100%) rename src/{content => }/assets/img/marker-icon.png (100%) rename src/{content => }/assets/img/marker-shadow.png (100%) create mode 100644 src/assets/js/dune delete mode 100644 src/babillard.ml delete mode 100644 src/babillard_page.eml.html create mode 100644 src/caqti_db.ml create mode 100644 src/caqti_db.mli delete mode 100644 src/catalog_page.eml.html create mode 100644 src/client/client_types.ml create mode 100644 src/client/db.ml create mode 100644 src/client/dune create mode 100644 src/client/events.ml create mode 100644 src/client/form_kind.ml create mode 100644 src/client/html.ml create mode 100644 src/client/html_form.ml create mode 100644 src/client/html_post.ml create mode 100644 src/client/html_thread.ml create mode 100644 src/client/html_util.ml create mode 100644 src/client/leaflet_map.ml create mode 100644 src/client/main.ml create mode 100644 src/client/model.ml create mode 100644 src/client/navigation.ml create mode 100644 src/client/network.ml create mode 100644 src/client/page.ml create mode 100644 src/client/storage.ml create mode 100644 src/client/util.ml create mode 100644 src/comment/ast.ml create mode 100644 src/comment/comment.ml create mode 100644 src/comment/dune create mode 100644 src/comment/lexer.ml create mode 100644 src/comment/parser.mly delete mode 100644 src/content/about.md delete mode 100644 src/content/assets/css/bootstrap.min.css delete mode 100644 src/content/assets/css/style.css delete mode 100644 src/content/assets/js/bootstrap.bundle.min.js delete mode 100644 src/content/assets/js/dune delete mode 100644 src/content/emoji-test.txt delete mode 100644 src/content/register.md delete mode 100644 src/db.ml delete mode 100644 src/db.mli create mode 100644 src/db_image.ml create mode 100644 src/db_image.mli create mode 100644 src/db_post.ml create mode 100644 src/db_post.mli create mode 100644 src/db_user.ml create mode 100644 src/db_user.mli delete mode 100644 src/delete_page.eml.html delete mode 100644 src/discuss.ml delete mode 100644 src/emojid.ml delete mode 100644 src/emojid.mli create mode 100644 src/err.ml create mode 100644 src/html.ml create mode 100644 src/html.mli create mode 100644 src/image.mli delete mode 100644 src/js/babillard.ml delete mode 100644 src/js/catalog.ml delete mode 100644 src/js/dune delete mode 100644 src/js/map.ml delete mode 100644 src/js/newthread.ml delete mode 100644 src/js/post_form.ml delete mode 100644 src/js/pretty_post.ml delete mode 100644 src/js/thread.ml delete mode 100644 src/js/utils.ml create mode 100644 src/json_data.ml create mode 100644 src/json_data.mli delete mode 100644 src/login.eml.html create mode 100644 src/moderation.ml create mode 100644 src/post.ml delete mode 100644 src/post_form.eml.html delete mode 100644 src/pp_babillard.ml delete mode 100644 src/register.eml.html delete mode 100644 src/report_page.eml.html delete mode 100644 src/template.eml.html delete mode 100644 src/thread_page.eml.html create mode 100644 src/types.mli delete mode 100644 src/user_account.eml.html delete mode 100644 src/user_profile.eml.html create mode 100644 src/util.ml delete mode 100644 src/utils.ml create mode 100644 src/validate_str.ml create mode 100644 src/validate_str.mli create mode 100644 src/virtual/config.mli create mode 100644 src/virtual/config_serv.mli create mode 100644 src/virtual/dir.mli create mode 100644 src/virtual/dune create mode 100644 src/virtual/impl/dir.ml create mode 100644 src/virtual/impl/dune create mode 100644 src/virtual/impl/make_config_impl.ml create mode 100644 src/virtual/make_config_lib.ml create mode 100644 test/config.scfg create mode 100644 test/img/fff.exif.png create mode 100644 test/img/fff.gif create mode 100644 test/img/fff.jpeg create mode 100644 test/img/fff.png delete mode 100644 test/main.ml create mode 100644 test/test.ml create mode 100644 test/util.ml create mode 100644 test/virtual/dir.ml create mode 100644 test/virtual/dune create mode 100644 test/virtual/make_config_test.ml diff --git a/.ocamlformat b/.ocamlformat index c54116a..eb9f4e0 100644 --- a/.ocamlformat +++ b/.ocamlformat @@ -1,4 +1,4 @@ -version=0.24.1 +version=0.27.0 assignment-operator=end-line break-cases=fit break-fun-decl=wrap diff --git a/README.md b/README.md index f53a111..9d6529a 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,18 @@ that can resolve to a geographical position. [AGPL-or-later] [AGPL-or-later]: ./LICENSE.md + +# TODO + +- post nb limit + +- make it a web-app instead + +- rework style, full screen map +- dream -> drame +- good moderation/admin system +- all of [issues](https://git.zapashcanon.fr/zapashcanon/permap/issues) +- imagemagick: WARNING: The convert command is deprecated in IMv7 + +- db error on login fail instead of clean login error msg +- login fail on login with email? diff --git a/doc/dune b/doc/dune deleted file mode 100644 index bae3535..0000000 --- a/doc/dune +++ /dev/null @@ -1,3 +0,0 @@ -(documentation - (package permap) - (mld_files index)) diff --git a/doc/index.mld b/doc/index.mld deleted file mode 100644 index 726e6fb..0000000 --- a/doc/index.mld +++ /dev/null @@ -1,19 +0,0 @@ -{0 permap} - -{{:https://TODO} permap} is an {{:https://ocaml.org} OCaml} library/executable to TODO. - -{1:api API} - - -{!modules: -Permap -} - - -{1:private_api Private API} - -You shouldn't have to use any of these modules, they're used internally only. - -{!modules: -TODO -} diff --git a/dune-project b/dune-project index c159b95..0cebf64 100644 --- a/dune-project +++ b/dune-project @@ -1,4 +1,7 @@ -(lang dune 2.8) +(lang dune 3.0) +(using menhir 2.1) + +(generate_opam_files true) (implicit_transitive_deps false) @@ -6,9 +9,13 @@ (license AGPL-3.0-or-later) -(authors "swrup " "Léo Andrès ") +(authors + "swrup " + "Léo Andrès ") -(maintainers "Léo Andrès ") +(maintainers + "swrup " + "Léo Andrès ") (source (uri git+https://git.zapashcanon.fr/zapashcanon/permap.git)) @@ -19,43 +26,40 @@ (documentation https://doc.zapashcanon.fr/permap) -(generate_opam_files true) - (package (name permap) (synopsis "OCaml library/executable to TODO") (description "permap is an OCaml library/executable to TODO.") (tags - (permap forum map local-knownledge ecology permaculture plant)) + (imageboard forum map leaflet single-page-application functional-reactive-programming)) (depends - dream - lwt - yojson - brr - leaflet - js_of_ocaml - uuidm - scfg - crunch - safepass - omd - lambdasoup bos + brr caqti caqti-driver-sqlite3 conan conan-database + crunch + data-encoding + digestif directories dream dream-pure emile + fmt fpath - lambdasoup - omd + htmlit + js_of_ocaml + leaflet + lwt + note safepass scfg uri uuidm - yojson + (alcotest :with-test) + (re :with-test) + (ocamlformat :with-dev-setup) + prelude (ocaml - (>= 4.08)))) + (>= 5.1)))) diff --git a/example/dune b/example/dune deleted file mode 100644 index e36b142..0000000 --- a/example/dune +++ /dev/null @@ -1,3 +0,0 @@ -(executable - (name main) - (modules main)) diff --git a/example/main.ml b/example/main.ml deleted file mode 100644 index 16c9b9f..0000000 --- a/example/main.ml +++ /dev/null @@ -1 +0,0 @@ -let () = Format.printf "TODO@." diff --git a/permap.opam b/permap.opam index 368de3a..f495dab 100644 --- a/permap.opam +++ b/permap.opam @@ -2,47 +2,51 @@ opam-version: "2.0" synopsis: "OCaml library/executable to TODO" description: "permap is an OCaml library/executable to TODO." -maintainer: ["Léo Andrès "] +maintainer: ["swrup " "Léo Andrès "] authors: ["swrup " "Léo Andrès "] license: "AGPL-3.0-or-later" tags: [ - "permap" "forum" "map" "local-knownledge" "ecology" "permaculture" "plant" + "imageboard" + "forum" + "map" + "leaflet" + "single-page-application" + "functional-reactive-programming" ] homepage: "https://git.zapashcanon.fr/zapashcanon/permap" doc: "https://doc.zapashcanon.fr/permap" bug-reports: "https://git.zapashcanon.fr/zapashcanon/permap/issues" depends: [ - "dune" {>= "2.8"} - "dream" - "lwt" - "yojson" - "brr" - "leaflet" - "js_of_ocaml" - "uuidm" - "scfg" - "crunch" - "safepass" - "omd" - "lambdasoup" + "dune" {>= "3.0"} "bos" + "brr" "caqti" "caqti-driver-sqlite3" "conan" "conan-database" + "crunch" + "data-encoding" + "digestif" "directories" "dream" "dream-pure" "emile" + "fmt" "fpath" - "lambdasoup" - "omd" + "htmlit" + "js_of_ocaml" + "leaflet" + "lwt" + "note" "safepass" "scfg" "uri" "uuidm" - "yojson" - "ocaml" {>= "4.08"} + "alcotest" {with-test} + "re" {with-test} + "ocamlformat" {with-dev-setup} + "prelude" + "ocaml" {>= "5.1"} "odoc" {with-doc} ] build: [ diff --git a/src/api.ml b/src/api.ml new file mode 100644 index 0000000..47f80db --- /dev/null +++ b/src/api.ml @@ -0,0 +1,334 @@ +open Syntax +open Err + +(* TODO server/client shared routes and types *) +(* used to get url param/session field and convert to int if needed + not actually useful *) +type _ t = + | User_id : string t + | Thread_id : int t + | Post_id : int t + | User_image_id : string t + | Post_image_id : int t + +let str : type a. a t -> string = function + | User_id -> "user_id" + | Thread_id -> "thread_id" + | Post_id -> "post_id" + | User_image_id -> "image_id" + | Post_image_id -> "image_id" + +let url_param : type a. Dream.request -> a t -> a Err.result = + let to_int s = int_of_string_opt s |> Option.to_result ~none:Err.Not_found in + fun request kind -> + let s = Dream.param request (str kind) in + match kind with + | User_id -> Ok s + | User_image_id -> Ok s + | Thread_id -> to_int s + | Post_id -> to_int s + | Post_image_id -> to_int s + +let session_user_id : Dream.request -> string option = + fun request -> Dream.session_field request (str User_id) + +let set_session_user_id : Dream.request -> string -> unit Lwt.t = + fun request v -> Dream.set_session_field request (str User_id) v + +let handle_invalid_form form = + match form with + | `Expired _ | `Wrong_session _ -> Error Bad_form + | `Ok _ | `Many_tokens _ | `Missing_token _ | `Invalid_token _ + | `Wrong_content_type -> + (* usually indicate either bugs or attacks *) + Error Bad_form_suspicious + +let get_logged_user request = + match session_user_id request with + | None -> Error Unauthorized + | Some id -> User.get_user id + +(* TODO + better system for permissions? *) +let check_is_admin request = + let* user = get_logged_user request in + if user.user_is_admin then Ok user else Error Forbidden + +let write_session request = + let open Types in + let+ user_private = + match session_user_id request with + | None -> Ok None + | Some user_id -> Result.map Option.some (User.get_user_private user_id) + in + let valid_for = float_of_int Config.csrf_lifetime in + let session = + { user_private + ; csrf_token = Dream.csrf_token ~valid_for request + ; csrf_time_limit = Unix.time () +. valid_for + } + in + Json_data.Write.session session + +let handle_login ~login ~password request = + let*! u = User.login ~login ~password in + let%lwt () = Dream.invalidate_session request in + let%lwt () = set_session_user_id request u.user_id in + Lwt.return @@ write_session request + +module GET = struct + let catalog _request = + let+ catalog = Post.get_catalog () in + Json_data.Write.catalog catalog + + let thread_w_reply request = + let* thread_id = url_param request Thread_id in + let+ v = Post.get_thread_w_reply thread_id in + Json_data.Write.thread_w_reply v + + let post request = + let* post_id = url_param request Post_id in + let+ v = Post.get_post post_id in + Json_data.Write.post v + + let admin request = + let* _user = check_is_admin request in + let+ reports = Moderation.get_reports_all () in + Json_data.Write.reports reports + + let user_page request = + let* user_id = url_param request User_id in + let+ user = User.get_user user_id in + Json_data.Write.user user + + let session request = write_session request +end + +module POST = struct + let new_thread request = + let*! user = get_logged_user request in + let%lwt form = Dream.multipart request in + Lwt.return + @@ + match form with + | `Ok + [ ("alt", [ (_, alt) ]) + ; ("comment", [ (_, comment) ]) + ; ("file", file) + ; ("lat-input", [ (_, lat) ]) + ; ("lng-input", [ (_, lng) ]) + ; ("subject", [ (_, subject) ]) + ] -> ( + match (Float.of_string_opt lat, Float.of_string_opt lng) with + | None, _ | _, None -> Error Bad_form + | Some lat, Some lng -> + let* image_data = + match file with + | [] -> Ok None + | _ :: _ :: _ -> Error Bad_form + | [ (image_name, image_content) ] -> + let image_data = (image_name, alt, image_content) in + Ok (Some image_data) + in + let+ v = + Post.make_thread ~comment ~image_data ~subject ~lat ~lng user + in + Json_data.Write.thread_w_reply v ) + | form -> handle_invalid_form form + + let reply request = + let*! user = get_logged_user request in + let%lwt form = Dream.multipart request in + Lwt.return + @@ + match form with + | `Ok + [ ("alt", [ (_, alt) ]); ("comment", [ (_, comment) ]); ("file", file) ] + -> + let* parent_thread = + let* thread_id = url_param request Thread_id in + Post.get_thread thread_id + in + let* image_data = + match file with + | [] -> Ok None + | _ :: _ :: _ -> Error Bad_form + | [ (image_name, image_content) ] -> + let image_data = (image_name, alt, image_content) in + Ok (Some image_data) + in + let* post = Post.make_post ~comment ~image_data ~parent_thread user in + let+ v = Post.get_thread_w_reply post.Types.parent_t_id in + Json_data.Write.thread_w_reply v + | form -> handle_invalid_form form + + let login request = + let%lwt form = Dream.multipart request in + match form with + | `Ok [ ("login", [ (_, login) ]); ("password", [ (_, password) ]) ] -> ( + (* TODO move all check like this to User/Post *) + let*! b = Moderation.is_banished login in + match b with + | true -> Lwt.return (Error (Unprocessable "YOU ARE BANISHED")) + | false -> handle_login ~login ~password request ) + | form -> Lwt.return @@ handle_invalid_form form + + let logout request = + let*! _user = get_logged_user request in + let%lwt form = Dream.multipart request in + match form with + | `Ok [] -> + let%lwt () = Dream.invalidate_session request in + Lwt.return @@ write_session request + | form -> Lwt.return @@ handle_invalid_form form + + let delete request = + let*! user = get_logged_user request in + let%lwt form = Dream.multipart request in + Lwt.return + @@ + match form with + | `Ok [] -> + let* post_id = url_param request Post_id in + let* post = Post.get_post post_id in + let+ () = Post.delete ~user post_id in + Json_data.Write.post post + | form -> handle_invalid_form form + + let report request = + let*! user = get_logged_user request in + let%lwt form = Dream.multipart request in + Lwt.return + @@ + match form with + | `Ok [ ("reason", [ (_, reason) ]) ] -> + let* post_id = url_param request Post_id in + let* () = + Moderation.make_report ~reporter_user_id:user.user_id ~reason post_id + in + let+ my_reports = + match user.user_is_admin with + | false -> Moderation.get_reports_made_by user.user_id + | true -> Moderation.get_reports_all () + in + Json_data.Write.reports my_reports + | form -> handle_invalid_form form + + let admin_ignore request = + let*! _user = check_is_admin request in + let%lwt form = Dream.multipart request in + Lwt.return + @@ + match form with + | `Ok [] -> + let* post_id = url_param request Post_id in + let* () = Moderation.delete_report post_id in + let+ v = Moderation.get_reports_all () in + Json_data.Write.reports v + | form -> handle_invalid_form form + + let admin_delete request = + let*! user = check_is_admin request in + let%lwt form = Dream.multipart request in + Lwt.return + @@ + match form with + | `Ok [] -> + let* post_id = url_param request Post_id in + let* post = Post.get_post post_id in + let+ () = Post.delete ~user post_id in + Json_data.Write.post post + | form -> handle_invalid_form form + + let admin_banish request = + let*! _user = check_is_admin request in + let%lwt form = Dream.multipart request in + Lwt.return + @@ + match form with + | `Ok [] -> + let* evil_id = url_param request User_id in + let* evil = User.get_user evil_id in + let+ () = Moderation.banish evil_id in + Json_data.Write.user evil + | form -> handle_invalid_form form + + let profile request = + let*! user = get_logged_user request in + let user_id = user.user_id in + let%lwt form = Dream.multipart request in + Lwt.return + @@ + match form with + | `Ok [ ("bio", [ (_, bio) ]) ] -> + let* () = User.update_bio user_id bio in + write_session request + | `Ok [ ("nick", [ (_, nick) ]) ] -> + let* () = User.update_nick user_id nick in + write_session request + | `Ok [ ("delete-avatar", _) ] -> + let* () = User.delete_avatar user_id in + write_session request + | `Ok + [ ("alt", [ (None, alt) ]); ("file", [ (image_name, image_content) ]) ] + -> + let image_data = (image_name, alt, image_content) in + let* () = User.upload_avatar user_id image_data in + write_session request + | form -> handle_invalid_form form + + (*TODO re-ask for password for account settings *) + let account request = + let*! user = get_logged_user request in + let user_id = user.user_id in + let%lwt form = Dream.multipart request in + match form with + | `Ok [ ("delete-account", _) ] -> ( + (* TODO ask for confirmation *) + match User.delete_user user_id with + | Error _ as e -> Lwt.return e + | Ok () -> + let%lwt () = Dream.invalidate_session request in + Lwt.return + @@ + (*let msg = "Your account was deleted" in*) + write_session request ) + | `Ok [ ("email", [ (_, email) ]) ] -> + Lwt.return + @@ + (*let msg = "Your email was updated!" in*) + let* () = User.update_email user_id email in + write_session request + | `Ok + [ ("confirm-new-password", [ (_, confirm_password) ]) + ; ("new-password", [ (_, password) ]) + ] -> + Lwt.return + @@ + let* () = + if String.equal password confirm_password then + User.update_password user_id password + (*let msg = "Your password was updated!" in*) + else Error (Unprocessable "Password confirmation does not match") + in + write_session request + | form -> Lwt.return @@ handle_invalid_form form + + let register request = + let*! () = + (* TODO move all check like this to User/Post *) + if Config.open_registration then Ok () + else Error (Unprocessable "registration is not open") + in + let%lwt form = Dream.multipart request in + match form with + | `Ok + [ ("email", [ (_, email) ]) + ; ("nick", [ (_, nick) ]) + ; ("password", [ (_, password) ]) + ] -> ( + match User.register ~email ~nick ~password with + | Error _ as e -> Lwt.return e + | Ok () -> handle_login ~login:email ~password request ) + | form -> Lwt.return @@ handle_invalid_form form +end diff --git a/src/app.ml b/src/app.ml deleted file mode 100644 index ca8e223..0000000 --- a/src/app.ml +++ /dev/null @@ -1,94 +0,0 @@ -module App_id = struct - let qualifier = "org" - - let organization = "Permap" - - let application = "permap" -end - -module Project_dirs = Directories.Project_dirs (App_id) - -let data_dir = - match Project_dirs.data_dir with - | None -> failwith "can't compute data directory" - | Some data_dir -> data_dir - -let config_dir = - match Project_dirs.config_dir with - | None -> failwith "can't compute configuration directory" - | Some config_dir -> config_dir - -let config = - let filename = Filename.concat config_dir "config.scfg" in - if not @@ Sys.file_exists filename then - failwith - @@ Format.sprintf "configuration file `%s` does not exist, please create it" - filename; - Dream.log "config file: %s" filename; - match Scfg.Parse.from_file filename with - | Error e -> failwith e - | Ok config -> config - -let open_registration = - match Scfg.Query.get_dir "open_registration" config with - | None -> true - | Some open_registration -> ( - match Scfg.Query.get_param 0 open_registration with - | Error e -> failwith e - | Ok "true" -> true - | Ok "false" -> false - | Ok _unknown -> - failwith "invalid `open_registration` value in configuration file" ) - -let () = Dream.log "open_registration: %b" open_registration - -let port = - match Scfg.Query.get_dir "port" config with - | None -> 8080 - | Some port -> ( - match Scfg.Query.get_param 0 port with - | Error e -> failwith e - | Ok n -> ( - try - let n = int_of_string n in - if n < 0 then raise (Invalid_argument "negative port number"); - n - with Invalid_argument _msg -> - failwith "invalid `port` value in configuration file" ) ) - -let () = Dream.log "port: %d" port - -let hostname = - match Scfg.Query.get_dir "hostname" config with - | None -> Format.sprintf "localhost:%d" port - | Some hostname -> - Result.fold ~error:failwith ~ok:Fun.id (Scfg.Query.get_param 0 hostname) - -let () = Dream.log "hostname: %s" hostname - -let log = - match Scfg.Query.get_dir "log" config with - | None -> true - | Some log -> ( - match Scfg.Query.get_param 0 log with - | Error e -> failwith e - | Ok "true" -> true - | Ok "false" -> false - | Ok _unknown -> failwith "invalid `log` value in configuration file" ) - -let () = Dream.log "log: %b" log - -let get_dirs name = - let dirs = Scfg.Query.get_dirs name config in - List.map - (fun dir -> - Result.fold ~error:failwith ~ok:Fun.id (Scfg.Query.get_param 0 dir) ) - dirs - -let admins = get_dirs "admin" - -let categories = List.sort_uniq compare (get_dirs "category") - -let random_state = Random.State.make_self_init () - -let () = Random.set_state random_state diff --git a/src/content/assets/css/leaflet.css b/src/assets/css/leaflet.css similarity index 100% rename from src/content/assets/css/leaflet.css rename to src/assets/css/leaflet.css diff --git a/src/assets/css/style.css b/src/assets/css/style.css new file mode 100644 index 0000000..0bf056f --- /dev/null +++ b/src/assets/css/style.css @@ -0,0 +1,411 @@ +/* TODO + * - nice color palette + * - nice fonts + * ... */ +:root { + --bg: #e8eaf6; + --bg-nav: #feafd6; + --bg-nav-hover: color-mix(in srgb, var(--bg-nav), black 15%); + --heavy-text: black; + --text: #333333; + --light-text: #5a5a5a; + --quote: #FFB300; + --bg-post: #C5E1A5; + --border-post: #9dd162; + --bg-post-highlight: #9dd162; + --border-form: #FFB300; + --bg-form: #FCE4EC; + --bg-error-popup: red; + --border-error-popup: red; + --bg-id: DodgerBlue; + --bg-id-hover: red; + --bg-id-remote: gray; + --bg-id-remote-loading: #FCE4EC; + --bg-id-remote-not-found: black; + --bg-id-remote-ready: blue; +} + + +/* unset default */ +ul { + list-style-type: none; + padding: 0; + margin-block: 0; +} + +body { + margin: 0; + color: var(--light-text); + background-color: var(--bg); + font-size: 18px; + display: grid; + grid-template-rows: 3fr 97fr; + height: 100vh; +} + +body * { + max-height: 100%; +} + +nav { + background-color: var(--bg-nav); + display: flex; + justify-content: space-between; +} + +nav div { + display: flex; + flex-direction: row; +} + +.logout-btn { + all: unset; + outline: revert; +} + +.logout-btn:hover { + cursor: pointer; +} + +nav a, nav button, +.sub-nav a, .sub-nav button { + display: flex; + align-items: center; + text-decoration: none; + color: var(--heavy-text); + transition: all 0.3s ease; + padding-inline: 2vw; +} + +nav a:hover, nav button:hover, +.sub-nav a:hover, .sub-nav button:hover { + background-color: var(--bg-nav-hover); +} + +/* todo: use sub-grid? */ +.home-page { + display: grid; + grid-template-columns: 60fr 40fr ; + width: 100%; + height: 100%; +} + +.home-left, .home-right { + position: relative; + width: 100%; + height: 100%; +} + +#map { + position: sticky !important; + height: 100vh; + width: 100%; + top: 0; + left: 0; +} + +.home-left-navigation-div { + display: flex; + margin-bottom: 7vh; + justify-content: flex-end; + margin-right: 7vw; +} + +.map-btn-div { + display: flex; + position: absolute; + bottom: 0; + margin-bottom: 7vh; + justify-content: flex-end; + right: 0; + margin-right: 7vw; + z-index: 999; +} + +.new-thread-view { + margin-inline: 1vw; + margin-block: 3vw; +} + +/* css trick to have a dropdown menu on click */ +.dropdown { + position: relative; + display: grid; + grid-template-rows: 1fr; + grid-auto-rows: 0; + + .dropdown-content, .dropdown-content-placeholder { + visibility: hidden; + display: flex; + flex-direction: column; + } + .dropdown-content { + transition: 0.2s ease-out; + z-index: 100000; + position: absolute; + top: 100%; + left: 0; + } + li { + background-color: var(--bg); + } + &:focus-within .dropdown-content { + visibility: visible; + } + .dropdown-arrow { + /* need to be block element to apply transform */ + display: inline-block; + transition: 0.2s ease-out; + } + &:focus-within .dropdown-arrow { + transform: rotate(90deg); + } + .dropdown-open-btn, .dropdown-close-btn { + all: unset; + cursor: pointer; + user-select: none; + transition: 0.2s ease-out; + } + .dropdown-open-btn { + /* gap to add space between arrow and label */ + display: flex; + gap: 3ch; + align-items: center; + } + .dropdown-open-btn:hover, .dropdown-open-btn:focus { + color: var(--bg-id-hover); + } + .dropdown-close-btn { + display: none; + position: absolute; + top: 0; + left: 0; + opacity: 0; + z-index: 99; + } + &:focus-within .dropdown-close-btn:not(:focus) { + display: inline-block; + min-width: 100%; + } +} + +.thread-view { + display: flex; + flex-direction: column; + height: 100%; +} + +.sub-nav { + display: flex; + flex-direction: row; + justify-content: space-between; + border-bottom: 1px solid black; +} + +#bottom { + margin-top: auto; +} + +.thread { + margin-inline: 1vw; + margin-block: 3vw; +} + +.thread-subject { + color: var(--light-text); + font-size: 25px; + padding-left: 3vw; + padding-bottom: 1vh; +} + +.thread-replies { + color: var(--light-text); + font-size: 20px; + display: flex; + flex-direction: column; + gap: 0.7vh; +} + +.post { + background-color: var(--bg-post); + border: 1px solid var(--border-post); + border-top: none; + border-left: none; + padding: 5px; + padding-left: 10px; + width: fit-content; + max-width: 100%; +} + +.post-info { + display: flex; + flex-direction: row; + gap: 0.2em; + align-items: center; + margin-bottom: 5px; +} + +.post-info * { + text-align: center +} + +.post-replies { + display: flex; + gap: 0.2em; +} + +.post-id, .post-id-quote { + all: unset; + cursor: revert; + /* revert default focus ring */ + outline: revert; + outline-offset: 3px; + + background-color: var(--bg-id); + padding: 2px; + text-align: center; + transition: 0.2s ease-out; + display: inline-block; + height: calc(1lh - 2px); + border-radius: 6px; +} +.post-id-quote { + border-radius: 12px; +} +.post-id:hover, .post-id-quote:hover, +.post-id:focus, .post-id-quote:focus { + background-color: var(--bg-id-hover); +} + +.post-id-quote.remote { + background-color: var(--bg-id-remote); +} +.post-id-quote.remote.loading { + background-color: var(--bg-id-remote-loading); +} +.post-id-quote.remote.not-found { + background-color: var(--bg-id-remote-not-found); +} +.post-id-quote.remote.ready { + background-color: var(--bg-id-remote-ready); +} + +.post-author-nick, .post-link-to-self { + text-decoration: none; + color: unset; + font-style: italic; + transition: 0.2s ease-out; +} +.post-author-nick:hover, .post-link-to-self:hover, +.post-author-nick:focus, .post-link-to-self:focus { + color: var(--bg-id-hover); +} + +.post-content { + display: flex; + gap: 10px; +} + +/* TODO use image dim? better max-size? */ +.post-image-div { +} + +.post-image { + max-width: 90vw; + height: auto; +} + +.post-image-small { + max-width: 30vw; + max-height: 30vh; +} + +.post-comment { + color: var(--text); + padding-top: 10px; + overflow-wrap: break-word; +} +.line-quote { + color: var(--quote); +} + +.selected, .highlighted { + background-color: var(--bg-post-highlight); +} + +.open-reply-popup-btn { + /* TODO */ +} + +.reply-popup { + display: table; + position: fixed; + right: 1vw; + top: 40vh; + background-color: var(--bg-form); + border: 2px solid var(--border-form); + padding: 5px; + z-index: 999990; +} +.reply-popup-dragzone { + display: flex; + justify-content: end; + cursor: move; +} +.close-reply-popup-btn { + line-height: 0.5lh; +} +.reply-popup-content { + display: flex; + flex-direction: column; + gap: 1em; + label { + display: block; + } + textarea { + width: 40ch; + height: 15ch; + } + & > div:last-child { + display: flex; + justify-content: center; + margin-top: 1em; + } +} + +.error-popup { + display: table; + position: fixed; + right: 1vw; + bottom: 1vh; + padding: 5px; + z-index: 999999; + background-color: var(--bg-error-popup); + border: 2px solid var(--border-error-popup); + border-radius: 12px; +} +.error-popup-dragzone { + display: flex; + justify-content: end; + cursor: move; +} +.close-error-popup-btn { + /* TODO + * - always have good contrast + * - better style + * same for reply-form */ + line-height: 0.5lh; +} +.error-popup-content { + padding: 2vw; + font-size: 18px; + color: white; +} + +.hidden { + visibility: hidden; +} + +.off { + display: none; +} diff --git a/src/content/assets/img/atom.svg b/src/assets/img/atom.svg similarity index 100% rename from src/content/assets/img/atom.svg rename to src/assets/img/atom.svg diff --git a/src/content/assets/img/default_avatar.png b/src/assets/img/default_avatar.png similarity index 100% rename from src/content/assets/img/default_avatar.png rename to src/assets/img/default_avatar.png diff --git a/src/content/assets/img/favicon.png b/src/assets/img/favicon.png similarity index 100% rename from src/content/assets/img/favicon.png rename to src/assets/img/favicon.png diff --git a/src/content/assets/img/layers-2x.png b/src/assets/img/layers-2x.png similarity index 100% rename from src/content/assets/img/layers-2x.png rename to src/assets/img/layers-2x.png diff --git a/src/content/assets/img/layers.png b/src/assets/img/layers.png similarity index 100% rename from src/content/assets/img/layers.png rename to src/assets/img/layers.png diff --git a/src/content/assets/img/marker-icon-2x.png b/src/assets/img/marker-icon-2x.png similarity index 100% rename from src/content/assets/img/marker-icon-2x.png rename to src/assets/img/marker-icon-2x.png diff --git a/src/content/assets/img/marker-icon.png b/src/assets/img/marker-icon.png similarity index 100% rename from src/content/assets/img/marker-icon.png rename to src/assets/img/marker-icon.png diff --git a/src/content/assets/img/marker-shadow.png b/src/assets/img/marker-shadow.png similarity index 100% rename from src/content/assets/img/marker-shadow.png rename to src/assets/img/marker-shadow.png diff --git a/src/assets/js/dune b/src/assets/js/dune new file mode 100644 index 0000000..0d98433 --- /dev/null +++ b/src/assets/js/dune @@ -0,0 +1,8 @@ +(rule + (target client.js) + (deps + (file ../../client/main.bc.js)) + (action + (with-stdout-to + %{target} + (cat ../../client/main.bc.js)))) diff --git a/src/babillard.ml b/src/babillard.ml deleted file mode 100644 index c139782..0000000 --- a/src/babillard.ml +++ /dev/null @@ -1,425 +0,0 @@ -open Syntax -open Caqti_request.Infix -open Caqti_type - -type moderation_action = - | Ignore - | Delete - | Banish - -let moderation_action_to_string = function - | Ignore -> "ignore" - | Delete -> "delete" - | Banish -> "banish" - -let moderation_action_from_string = function - | "ignore" -> Some Ignore - | "delete" -> Some Delete - | "banish" -> Some Banish - | _ -> None - -type thread_data = - { subject : string - ; lng : float - ; lat : float - } - -type post = - { id : string - ; emojid : string - ; parent_id : string - ; date : float - ; user_id : string - ; nick : string - ; comment : string - ; image_info : (string * string) option - ; tags : string list - ; replies : string list - ; citations : string list - } - -type t = - | Op of thread_data * post - | Post of post - -let () = - let tables = - [| (unit ->. unit) - "CREATE TABLE IF NOT EXISTS post_user (post_id TEXT, user_id TEXT, \ - PRIMARY KEY(post_id), FOREIGN KEY(user_id) REFERENCES user(user_id) \ - ON DELETE CASCADE)" - ; (* one row for each thread, with thread's data *) - (unit ->. unit) - "CREATE TABLE IF NOT EXISTS thread_info (thread_id TEXT, subject \ - TEXT, lat FLOAT, lng FLOAT, FOREIGN KEY(thread_id) REFERENCES \ - post_user(post_id) ON DELETE CASCADE)" - ; (* map thread and reply to the thread *) - (unit ->. unit) - "CREATE TABLE IF NOT EXISTS thread_post (thread_id TEXT, post_id \ - TEXT, FOREIGN KEY(thread_id) REFERENCES post_user(post_id) ON DELETE \ - CASCADE, FOREIGN KEY(post_id) REFERENCES post_user(post_id) ON \ - DELETE CASCADE)" - ; (unit ->. unit) - "CREATE TABLE IF NOT EXISTS post_replies (post_id TEXT, reply_id \ - TEXT, FOREIGN KEY(post_id) REFERENCES post_user(post_id) ON DELETE \ - CASCADE, FOREIGN KEY(reply_id) REFERENCES post_user(post_id) ON \ - DELETE CASCADE)" - ; (unit ->. unit) - "CREATE TABLE IF NOT EXISTS post_citations (post_id TEXT, cited_id \ - TEXT, FOREIGN KEY(post_id) REFERENCES post_user(post_id) ON DELETE \ - CASCADE, FOREIGN KEY(cited_id) REFERENCES post_user(post_id) ON \ - DELETE CASCADE)" - ; (unit ->. unit) - "CREATE TABLE IF NOT EXISTS post_date (post_id TEXT, date FLOAT, \ - FOREIGN KEY(post_id) REFERENCES post_user(post_id) ON DELETE \ - CASCADE)" - ; (unit ->. unit) - "CREATE TABLE IF NOT EXISTS post_comment (post_id TEXT, comment TEXT, \ - FOREIGN KEY(post_id) REFERENCES post_user(post_id) ON DELETE \ - CASCADE)" - ; (unit ->. unit) - "CREATE TABLE IF NOT EXISTS post_tags (post_id TEXT, tag TEXT, \ - FOREIGN KEY(post_id) REFERENCES post_user(post_id) ON DELETE \ - CASCADE)" - ; (unit ->. unit) - "CREATE TABLE IF NOT EXISTS report (user_id TEXT, reason TEXT, date \ - FLOAT,post_id TEXT, FOREIGN KEY(post_id) REFERENCES \ - post_user(post_id) ON DELETE CASCADE, FOREIGN KEY(user_id) \ - REFERENCES user(user_id) ON DELETE CASCADE)" - |] - in - if - Array.exists Result.is_error - (Array.map (fun query -> Db.exec query ()) tables) - then Dream.error (fun log -> log "can't create babillard's tables") - -module Q = struct - let upload_report = - Db.exec - @@ (tup4 string string float string ->. unit) - "INSERT INTO report VALUES (?,?,?,?)" - - let get_reports = - Db.collect_list - @@ (unit ->* tup4 string string float string) "SELECT * FROM report" - - let upload_post_id = - Db.exec - @@ (tup2 string string ->. unit) "INSERT INTO post_user VALUES (?,?)" - - let upload_thread_info = - Db.exec - @@ (tup4 string string float float ->. unit) - "INSERT INTO thread_info VALUES (?,?,?,?)" - - let upload_thread_post = - Db.exec - @@ (tup2 string string ->. unit) "INSERT INTO thread_post VALUES (?,?)" - - let upload_post_reply = - Db.exec - @@ (tup2 string string ->. unit) "INSERT INTO post_replies VALUES (?,?)" - - let upload_post_comment = - Db.exec - @@ (tup2 string string ->. unit) "INSERT INTO post_comment VALUES (?,?)" - - let upload_post_tag = - Db.exec - @@ (tup2 string string ->. unit) "INSERT INTO post_tags VALUES (?,?)" - - let upload_post_date = - Db.exec @@ (tup2 string float ->. unit) "INSERT INTO post_date VALUES (?,?)" - - let get_post_user_id = - Db.find - @@ (string ->! string) "SELECT user_id FROM post_user WHERE post_id=?" - - let get_post_comment = - Db.find - @@ (string ->! string) "SELECT comment FROM post_comment WHERE post_id=?" - - let get_post_tags = - Db.collect_list - @@ (string ->* string) "SELECT tag FROM post_tags WHERE post_id=?" - - let get_post_date = - Db.find @@ (string ->! float) "SELECT date FROM post_date WHERE post_id=?" - - let get_post_citations = - Db.collect_list - @@ (string ->* string) "SELECT post_id FROM post_citations WHERE post_id=?" - - let get_post_replies = - Db.collect_list - @@ (string ->* string) "SELECT reply_id FROM post_replies WHERE post_id=?" - - let get_thread_posts = - Db.collect_list - @@ (string ->* string) "SELECT post_id FROM thread_post WHERE thread_id=?" - - let count_thread_posts = - Db.find - @@ (string ->! int) - "SELECT COUNT(post_id) FROM thread_post WHERE thread_id=?" - - let get_is_post = - Db.find - @@ (string ->! string) - "SELECT post_id FROM post_user WHERE post_id=? LIMIT 1" - - let get_post_thread = - Db.find - @@ (string ->! string) - "SELECT thread_id FROM thread_post WHERE post_id=? LIMIT 1" - - let get_thread_info = - Db.find - @@ (string ->! tup3 string float float) - "SELECT subject,lat,lng FROM thread_info WHERE thread_id=?" - - let get_threads = - Db.collect_list @@ (unit ->* string) "SELECT thread_id FROM thread_info" - - let delete_post = - Db.exec @@ (string ->. unit) "DELETE FROM post_user WHERE post_id=?" -end - -let ignore_report = - Db.exec @@ (string ->. unit) "DELETE FROM report WHERE post_id=?" - -(*TODO switch to markdown !*) -(* insert html into the comment, and keep tracks of citations : - -wraps lines starting with ">" with a - -make raw posts uuid into links - (*TODO fix bad link if post is in other thread*) - -keeps tracks of every post cited in this comment - - add
at each line *) -let parse_comment comment = - let citations = ref [] in - - let pp_word fmt w = - let trim_w = String.trim w in - (* '>' is '>' after html_escape *) - if String.length trim_w >= 8 then - let sub_w = String.sub trim_w 8 (String.length trim_w - 8) in - if - String.starts_with ~prefix:{|>>|} trim_w - && Option.is_some (Uuidm.of_string sub_w) - then begin - citations := sub_w :: !citations; - Format.fprintf fmt {|%s|} sub_w w - end - else Format.pp_print_string fmt w - else Format.pp_print_string fmt w - in - let pp_line fmt l = - let trim_w = String.trim l in - (*insert quote*) - let words = String.split_on_char ' ' l in - if - String.starts_with ~prefix:{|>|} trim_w - && not (String.starts_with ~prefix:{|>>|} trim_w) - then - Format.fprintf fmt {|%a|} - (Format.pp_print_list ~pp_sep:Format.pp_print_space pp_word) - words - else Format.pp_print_list ~pp_sep:Format.pp_print_space pp_word fmt words - in - - let comment = String.trim comment in - let lines = String.split_on_char '\n' comment in - (*insert
*) - let comment = - Format.asprintf "%a" - (Format.pp_print_list - ~pp_sep:(fun fmt () -> Format.fprintf fmt "@\n
") - pp_line ) - lines - in - (* remove duplicate cited_id *) - let citations = List.sort_uniq String.compare !citations in - (comment, citations) - -let upload_post ~image post = - let thread_data, reply = - match post with - | Op (thread_data, reply) -> (Some thread_data, reply) - | Post reply -> (None, reply) - in - let { id; parent_id; date; user_id; comment; tags; citations; _ } = reply in - - let* () = Q.upload_post_id (id, user_id) in - let* () = Q.upload_post_comment (id, comment) in - let* () = Q.upload_post_date (id, date) in - let* () = Q.upload_thread_post (parent_id, id) in - let* () = - match image with None -> Ok () | Some image -> Image.upload image id - in - match unwrap_list (fun tag -> Q.upload_post_tag (id, tag)) tags with - | Error _e as e -> e - | Ok _ -> ( - match - unwrap_list (fun cited_id -> Q.upload_post_reply (cited_id, id)) citations - with - | Error _e as e -> e - | Ok _ -> - let* () = - match thread_data with - | None -> Ok () - | Some { subject; lng; lat } -> - Q.upload_thread_info (id, subject, lat, lng) - in - Ok id ) - -let build_reply ~comment ~image_info ~tag_list ?parent_id user_id = - let comment = Dream.html_escape comment in - let id = Uuidm.to_string (Uuidm.v4_gen App.random_state ()) in - (* parent_id is None if this reply is supposed to be a new thread *) - let parent_id = Option.value parent_id ~default:id in - if Option.is_none (Uuidm.of_string parent_id) then Error "invalid thread id" - else if String.length comment > 10000 then Error "invalid comment" - else if List.length tag_list > 30 then Error "too much tags" - else if List.exists (fun tag -> String.length tag > 100) tag_list then - Error "tag too long" - else if Option.is_none image_info && String.length (String.trim comment) = 0 - then Error "Your post must contain an image or a comment" - else - let tag_list = - List.map String.lowercase_ascii - @@ List.sort_uniq String.compare - @@ List.filter (( <> ) "") - @@ List.map String.trim - @@ List.map Dream.html_escape tag_list - in - let date = Unix.time () in - let comment, citations = parse_comment comment in - let* nick = User.get_nick user_id in - let* emojid = Emojid.make id in - let reply = - { id - ; emojid - ; parent_id - ; date - ; user_id - ; nick - ; comment - ; image_info - ; tags = tag_list - ; replies = [] - ; citations - } - in - Ok reply - -let build_op ~comment ~image_info ~tag_list ~categories ~subject ~lat ~lng - user_id = - let subject = Dream.html_escape subject in - if List.exists (fun s -> not (List.mem s App.categories)) categories then - Error "Invalid category" - else - let tag_list = categories @ tag_list in - (* TODO latlng validation? *) - let is_valid_latlng = true in - if not is_valid_latlng then Error "Invalid coordinate" - else if String.length subject > 600 then Error "Invalid subject" - else - let* reply = build_reply ~comment ~image_info ~tag_list user_id in - Ok ({ subject; lng; lat }, reply) - -let make_post ~comment ?image_input ~tags ~op_or_reply_data user_id = - let tag_list = String.split_on_char ',' tags in - let* image, image_info = - match image_input with - | None -> Ok (None, None) - | Some image_input -> - let* image = Image.make_image image_input in - Ok (Some image, Some (image.name, image.alt)) - in - let* post = - match op_or_reply_data with - | `Reply_data parent_id -> - let* reply = - build_reply ~comment ~image_info ~tag_list ~parent_id user_id - in - Ok (Post reply) - | `Op_data (categories, subject, lat, lng) -> - let* thread_data, reply = - build_op ~comment ~image_info ~tag_list ~categories ~subject ~lat ~lng - user_id - in - Ok (Op (thread_data, reply)) - in - upload_post ~image post - -(* true if post is an op too *) -let post_exist id = Result.is_ok (Q.get_is_post id) - -let get_post id = - let* emojid = Emojid.get id in - let* parent_id = Q.get_post_thread id in - let* user_id = Q.get_post_user_id id in - let* nick = User.get_nick user_id in - let* comment = Q.get_post_comment id in - let* date = Q.get_post_date id in - let* image_info = Image.get_info id in - - let* tags = Q.get_post_tags id in - let* replies = Q.get_post_replies id in - let* citations = Q.get_post_citations id in - let reply = - { id - ; emojid - ; parent_id - ; date - ; user_id - ; nick - ; comment - ; image_info - ; tags - ; replies - ; citations - } - in - Ok reply - -let get_thread_data id = - let* subject, lat, lng = Q.get_thread_info id in - Ok { subject; lat; lng } - -let get_op id = - let* thread_data = get_thread_data id in - let* post = get_post id in - Ok (thread_data, post) - -let get_posts ids = unwrap_list get_post ids - -let get_ops ids = unwrap_list get_op ids - -let try_delete_post ~user_id id = - let* post = get_post id in - if post.user_id = user_id || User.is_admin user_id then Q.delete_post id - else Error "You can only delete your posts" - -let report ~user_id ~reason id = - if not (post_exist id) then Error "This post exists not" - else if String.length reason > 2000 then Error "Your reason is too long.." - else - let reason = Dream.html_escape reason in - let date = Unix.time () in - Q.upload_report (user_id, reason, date, id) - -let get_reports () = - let* reports = Q.get_reports () in - let* posts = - unwrap_list (fun (_reporter_id, _reason, _date, id) -> get_post id) reports - in - (* add reporter_nick to reports so we can display it *) - let* reports = - unwrap_list - (fun (reporter_id, reason, date, id) -> - let* reporter_nick = User.get_nick reporter_id in - Ok (reporter_id, reporter_nick, reason, date, id) ) - reports - in - Ok (posts, reports) diff --git a/src/babillard_page.eml.html b/src/babillard_page.eml.html deleted file mode 100644 index 933113d..0000000 --- a/src/babillard_page.eml.html +++ /dev/null @@ -1,31 +0,0 @@ -let f request = - -% let new_thread_button = -% if Option.is_none @@ Dream.session "nick" request then -% Format.sprintf -% {|New Thread|} (Dream.to_percent_encoded "/") -% else {||} -% in - -

Babillard is love ❤️

-
-
-
-
-
- - - <%s! new_thread_button %> -
-
-
-
-

New thread

- - Click the map and make a new thread: - -
- <%s! Post_form.f None request %> -
-
-
diff --git a/src/caqti_db.ml b/src/caqti_db.ml new file mode 100644 index 0000000..64950ba --- /dev/null +++ b/src/caqti_db.ml @@ -0,0 +1,48 @@ +open Caqti_request.Infix + +let map_err = function + | Error e -> Error (Err.Internal (Db (Caqti_error.show e))) + | Ok _ as ok -> ok + +module Db = struct + module Db = + (val Caqti_blocking.connect Config_serv.db_uri |> Caqti_blocking.or_fail) + + let exec q v = Db.exec q v |> map_err + + let find q v = Db.find q v |> map_err + + let find_opt q v = Db.find_opt q v |> map_err + + let collect_list q v = Db.collect_list q v |> map_err + + let exec_unsafe q v = + match Db.exec q v with + | Error e -> + Dream.error (fun log -> log "%s" (Caqti_error.show e)); + exit 1 + | Ok () -> () + + let do_transaction f = + let open Syntax in + let* () = Db.start () |> map_err in + match f () with + | Error _ as error -> + let* () = Db.rollback () |> map_err in + error + | Ok v -> + let* () = Db.commit () |> map_err in + Ok v +end + +let set_foreign_keys_on = Caqti_type.(unit ->. unit) "PRAGMA foreign_keys = ON" + +let create_dream_session = + Caqti_type.(unit ->. unit) + "CREATE TABLE IF NOT EXISTS dream_session (id TEXT PRIMARY KEY, label TEXT \ + NOT NULL, expires_at REAL NOT NULL, payload TEXT NOT NULL)" + +let () = + Db.exec_unsafe set_foreign_keys_on (); + Db.exec_unsafe create_dream_session (); + () diff --git a/src/caqti_db.mli b/src/caqti_db.mli new file mode 100644 index 0000000..2512122 --- /dev/null +++ b/src/caqti_db.mli @@ -0,0 +1,17 @@ +open Err + +module Db : sig + val exec : ('a, unit, [< `Zero ]) Caqti_request.t -> 'a -> unit result + + val find : ('a, 'b, [< `One ]) Caqti_request.t -> 'a -> 'b result + + val find_opt : + ('a, 'b, [< `One | `Zero ]) Caqti_request.t -> 'a -> 'b option result + + val collect_list : + ('a, 'b, [ `Many | `One | `Zero ]) Caqti_request.t -> 'a -> 'b list result + + val exec_unsafe : ('a, unit, [< `Zero ]) Caqti_request.t -> 'a -> unit + + val do_transaction : (unit -> 'a result) -> 'a result +end diff --git a/src/catalog_page.eml.html b/src/catalog_page.eml.html deleted file mode 100644 index 87b85c8..0000000 --- a/src/catalog_page.eml.html +++ /dev/null @@ -1,8 +0,0 @@ -let f content = - - -

Catalog:

-
-
- <%s! content %> -
diff --git a/src/client/client_types.ml b/src/client/client_types.ml new file mode 100644 index 0000000..d7af0f6 --- /dev/null +++ b/src/client/client_types.ml @@ -0,0 +1,207 @@ +open Types + +type ('a, 'b) wrap = ('a, 'b) Page.wrap + +module Fragment = struct + type t = + | Empty + | Top + | Bottom + | Id of (int, int) wrap + + let unwrap_id = function Page.Loading v | Not_found v -> v | Ready v -> v + + let to_string = function + | Empty -> "" + | Top -> "top" + | Bottom -> "bottom" + | Id v -> + let id = unwrap_id v in + string_of_int id + + let of_string s = + match s with + | "" -> Ok Empty + | "top" -> Ok Top + | "bottom" -> Ok Bottom + | s -> ( + match int_of_string_opt s with + | None -> Fmt.error "invalid fragment format `%s`" s + | Some id -> Ok (Id (Loading id)) ) + + let get_ready_value v = + match v with + | Empty | Top | Bottom -> Some (to_string v) + | Id (Loading _) | Id (Not_found _) -> None + | Id (Ready _) -> Some (to_string v) +end + +module Post_form_data = struct + (* TODO (?) have a more genral thing for every form *) + (* store input data of reply and new thread form + both form share the same data: + text in comment on reply form will show up on new thread form too + wraped in module because record field conflict *) + type t = + { subject : string + ; comment : string + ; file : string option + ; alt : string option + ; is_open : bool + ; latlng : (float * float) option + } + + let empty = + { subject = "" + ; comment = "" + ; file = None + ; alt = None + ; is_open = false + ; latlng = None + } +end + +type meth = + | GET + | POST + +type response = + { meth : meth + ; url : string + ; status : int + ; status_text : string + ; body : string + } + +(* https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch#exceptions *) +(* https://developer.mozilla.org/en-US/docs/Web/API/Response/text#exceptions *) +type network_error = + | Fetch_err of string + | Body_err of string + | Read_err of string * response + +(* error type for interactions with server *) +type error = + | Network_err of network_error + | Err_response of Err.t + +type map_action = + | Move_end of (float * float * int) + | Zoom_end of (float * float * int) + | Click_latlng of (float * float) + | Click_marker of Types.post_id + | Geoloc_start + | Geoloc_pos of Brr_io.Geolocation.Pos.t + | Geoloc_err of Brr_io.Geolocation.Error.t + +type form_action = + | Form_open + | Form_close + | Form_insert_quote of post_id + | Form_comment of string + | Form_file of string option + | Form_alt of string option + | Form_subject of string + | Form_latlng of (float * float) option + | Form_reset + +(* post-quote's (x,y,w,h) + needed to compute quickview position *) +type rect = float * float * float * float + +type action = + | Navigation_event of (Page.t option * Fragment.t) + | Post_form_change of form_action + | Map_input of map_action + | Submit_event of (Form_kind.wrapped * Brr.El.t) + | Quickview_change of (rect * post_id) option + | Image_click of post_id + | Clear_error + +type data_update = + | Post_update of post + | Thread_update of Thread_w_reply.t + | Catalog_update of thread list + | User_update of user + | Reports_update of report list + | Session_update of session + +(* printer/util *) + +let pp_meth fmt = function GET -> Fmt.pf fmt "GET" | POST -> Fmt.pf fmt "POST" + +let pp_response fmt r = + Fmt.pf fmt + {|{ meth: `%a`; url: `%s`; status code: `%d`; status text: `%s`; body:`%s`}@.|} + pp_meth r.meth r.url r.status r.status_text r.body + +let pp_network_error fmt err = + match err with + | Fetch_err s -> Fmt.pf fmt "network fetch error `%s`" s + | Body_err s -> Fmt.pf fmt "network read body error `%s`" s + | Read_err (s, r) -> + Fmt.pf fmt "network read error `%s` on response `%a`" s pp_response r + +let pp_error fmt err = + match err with + | Network_err e -> pp_network_error fmt e + | Err_response e -> Err.pp fmt e + +let pp_map_action fmt a = + Fmt.pf fmt "map "; + match a with + | Move_end (lat, lng, zoom) -> + Fmt.pf fmt "move end `(%f, %f, %d)`" lat lng zoom + | Zoom_end (lat, lng, zoom) -> + Fmt.pf fmt "zoom end `(%f, %f, %d)`" lat lng zoom + | Click_latlng (lat, lng) -> Fmt.pf fmt "click latlng `(%f, %f)`" lat lng + | Click_marker post_id -> Fmt.pf fmt "click marker `%d`" post_id + | Geoloc_start -> Fmt.pf fmt "geoloc start" + | Geoloc_pos pos -> + let open Brr_io.Geolocation.Pos in + Fmt.pf fmt "geoloc pos `(%f, %f)`" (latitude pos) (longitude pos) + | Geoloc_err err -> + let open Brr_io.Geolocation.Error in + Fmt.pf fmt "geoloc error, code `%d` message `%s`" (code err) + (message err |> Jstr.to_string) + +let pp_form_action fmt a = + Fmt.pf fmt "form "; + match a with + | Form_open -> Fmt.pf fmt "open" + | Form_close -> Fmt.pf fmt "close" + | Form_insert_quote post_id -> Fmt.pf fmt "insert quote `%d`" post_id + | Form_comment s -> Fmt.pf fmt "comment `%s`" s + | Form_file o -> Fmt.pf fmt "file `%s`" (Option.value ~default:"none" o) + | Form_alt o -> Fmt.pf fmt "alt `%s`" (Option.value ~default:"none" o) + | Form_subject s -> Fmt.pf fmt "subject `%s`" s + | Form_latlng o -> ( + match o with + | None -> Fmt.pf fmt "latlng `none`" + | Some (lat, lng) -> Fmt.pf fmt "latlng `(%f, %f)`" lat lng ) + | Form_reset -> Fmt.pf fmt "reset" + +let pp_action fmt = function + | Navigation_event (opt, frag) -> + let s = + match opt with + | None -> "none" + | Some p -> p |> Page.to_uri |> Brr.Uri.to_jstr |> Jstr.to_string + in + Fmt.pf fmt "navigation event `(%s, %s)`" s (Fragment.to_string frag) + | Post_form_change a -> Fmt.pf fmt "post form change `%a`" pp_form_action a + | Map_input a -> Fmt.pf fmt "map input `%a`" pp_map_action a + | Submit_event (W kind, _el) -> + Fmt.pf fmt "submit event `%s`" (Form_kind.name kind) + | Quickview_change _opt -> Fmt.pf fmt "quickview change" + | Image_click post_id -> Fmt.pf fmt "image click `%d`" post_id + | Clear_error -> Fmt.pf fmt "clear error" + +let pp_data_update fmt a = + match a with + | Post_update v -> Fmt.pf fmt "post update `%d`" v.id + | Thread_update v -> Fmt.pf fmt "thread update `%d`" v.op.id + | Catalog_update _l -> Fmt.pf fmt "catalog update" + | User_update u -> Fmt.pf fmt "user update `%s`" u.user_id + | Reports_update _l -> Fmt.pf fmt "report update" + | Session_update _session -> Fmt.pf fmt "session update" diff --git a/src/client/db.ml b/src/client/db.ml new file mode 100644 index 0000000..f2a3589 --- /dev/null +++ b/src/client/db.ml @@ -0,0 +1,129 @@ +open Types + +let session : session option ref = ref None + +let update_session (v : session) = + session := Some v; + () + +let get_session () = + match !session with + | None -> Fmt.failwith "called get_session with uninitialized session" + | Some v -> v + +let post_db : (post_id, post) Hashtbl.t = Hashtbl.create 0x1000 + +let add_post (v : post) = + Hashtbl.replace post_db v.id v; + () + +let find_post id = + match Hashtbl.find_opt post_db id with None -> None | Some v -> Some v + +let post_db_404 : (post_id, unit) Hashtbl.t = Hashtbl.create 0x100 + +let post_is_404 id = Hashtbl.mem post_db_404 id + +let thread_is_404 id = Hashtbl.mem post_db_404 id + +let user_db_404 : (user_id, unit) Hashtbl.t = Hashtbl.create 0x100 + +let user_is_404 id = Hashtbl.mem user_db_404 id + +let catalog : thread list ref = ref [] + +let update_catalog (v : thread list) = + catalog := v; + () + +let get_catalog () = !catalog + +let thread_w_reply : Thread_w_reply.t option ref = ref None + +let update_thread_w_reply (o : Thread_w_reply.t option) = + Hashtbl.clear post_db; + thread_w_reply := o; + Option.iter (fun v -> List.iter add_post v.Thread_w_reply.reply_l) o; + () + +let find_thread_w_reply id = + match !thread_w_reply with + | None -> None + | Some v -> ( match v.op.id = id with false -> None | true -> Some v ) + +let reports : report list ref = ref [] + +let update_reports (v : report list) = + reports := v; + () + +let get_reports () = !reports + +let user : user option ref = ref None + +let update_user (v : user option) = + user := v; + () + +let find_user id = + match !user with + | None -> None + | Some v -> ( + match String.equal v.user_id id with false -> None | true -> Some v ) + +let clear () = + session := None; + update_catalog []; + update_thread_w_reply None; + update_reports []; + Hashtbl.clear post_db; + Hashtbl.clear post_db_404; + update_user None; + () + +let add_post_404 id = + (* in case post is a thread we have to remove id + potential reply_l *) + let to_delete_l = + match find_thread_w_reply id with + | Some v -> List.map (fun p -> p.id) v.reply_l + | None -> [ id ] + in + let filter get_id l = + (* O(n^2) ~~ *) + List.filter (fun v -> not @@ List.mem (get_id v) to_delete_l) l + in + + update_catalog (get_catalog () |> filter (fun v -> v.op.id)); + + update_thread_w_reply + ( match find_thread_w_reply id with + | Some _ -> None + | None -> ( + match !thread_w_reply with + | None -> None + | Some v -> + let v = { v with reply_l = filter (fun v -> v.id) v.reply_l } in + Some v ) ); + + update_reports (!reports |> filter (fun r -> r.reported_post.id)); + + Hashtbl.remove post_db id; + Hashtbl.add post_db_404 id (); + () + +let add_thread_404 = add_post_404 + +let add_user_404 id = + let session = get_session () in + let session = + match session.user_private with + | Some u when String.equal u.user_id id -> + (* dead session here *) + { session with user_private = None } + | _ -> session + in + update_session session; + begin + match find_user id with Some _ -> update_user None | None -> () + end; + () diff --git a/src/client/dune b/src/client/dune new file mode 100644 index 0000000..3869945 --- /dev/null +++ b/src/client/dune @@ -0,0 +1,17 @@ +(executable + (name main) + (modules :standard) + (libraries + config_impl ; virtual + shared + comment + leaflet + note + note.brr + brr + fmt + unix + prelude) + (modes js) + (flags + (:standard -open Prelude))) diff --git a/src/client/events.ml b/src/client/events.ml new file mode 100644 index 0000000..b61a02d --- /dev/null +++ b/src/client/events.ml @@ -0,0 +1,8 @@ +open Note +open Client_types + +let (actions : action event), send_action = E.create () + +let (data_updates : data_update event), send_data_update = E.create () + +let (errors : error event), send_error = E.create () diff --git a/src/client/form_kind.ml b/src/client/form_kind.ml new file mode 100644 index 0000000..fae38e5 --- /dev/null +++ b/src/client/form_kind.ml @@ -0,0 +1,50 @@ +(* TODO server/client shared routes and types *) +open Types + +type _ t = + | Home : Thread_w_reply.t t + | Register : session t + | Login : session t + | Logout : session t + | Profile : session t + | Account : session t + | Thread : post_id -> Thread_w_reply.t t + | Delete : post_id -> post t + | Report : + post_id + -> report list t (* only reports made by user, or all if user is admin *) + | Admin_ignore : post_id -> report list t + | Admin_delete : post_id -> post t + | Admin_banish : user_id -> user t + +type wrapped = W : 'a t -> wrapped [@@unboxed] + +let name : type a. a t -> string = function + | Home -> "new-thread" + | Register -> "register" + | Login -> "login" + | Logout -> "logout" + | Profile -> "profile" + | Account -> "account" + | Thread _ -> "post" + | Delete _ -> "delete-post" + | Report _ -> "report-post" + | Admin_ignore _ -> "admin-ignore" + | Admin_delete _ -> "admin-delete" + | Admin_banish _ -> "admin-banish" + +let action : type a. a t -> string = function + | Home -> "/" + | Register -> "/register" + | Login -> "/login" + | Logout -> "/logout" + | Profile -> "/profile" + | Account -> "/account" + | Thread id -> Fmt.str "/thread/%d" id + | Delete id -> Fmt.str "/delete/%d" id + | Report id -> Fmt.str "/report/%d" id + | Admin_ignore id -> Fmt.str "/admin/ignore/%d" id + | Admin_delete id -> Fmt.str "/admin/delete/%d" id + | Admin_banish id -> Fmt.str "/admin/banish/%s" id + +let action k = Fmt.str "/api%s" (action k) diff --git a/src/client/html.ml b/src/client/html.ml new file mode 100644 index 0000000..33b14cc --- /dev/null +++ b/src/client/html.ml @@ -0,0 +1,366 @@ +open Brr +open Note +open Note_brr +open Types +open Client_types +open Page +open Model +open Util +open Html_util + +module Header = struct + let mk t_s = + let left = El.div ~at:[ class' "nav-left" ] [ mk_page_link Home ] in + let right session = + let dropmenu user = + let class_prefix = "settings" in + let label = user.user_nick in + let at_title = "Settings" in + let mk_content () = + [ mk_page_link Profile + ; mk_page_link Account + ; Html_form.mk_logout () + ; mk_page_link About + ] + in + mk_dropdown_menu ~class_prefix ~label ~at_title ~placeholder:true + mk_content + in + let l = + match Option.map user_private_to_public session.user_private with + | None -> List.map mk_page_link [ About; Register; Login ] + | Some u when u.user_is_admin -> + [ mk_page_link (Admin (Loading ())); dropmenu u ] + | Some u -> [ dropmenu u ] + in + El.div ~at:[ class' "nav-right" ] l + in + let children = S.map (fun t -> [ left; right t.session ]) t_s in + let el = El.nav ~at:[ id "top" ] [] in + Elr.def_children el children; + el + + let f t_s = + let header = El.header [ mk t_s ] in + header +end + +module Home = struct + let left t_s = + let new_thread_view = + (* TODO try to find better class names *) + let new_thread_form_div = + El.div + ~at:[ class' "new-thread-form-div" ] + [ Html_form.new_thread_el t_s ] + in + El.div + ~at:[ class' "new-thread-view" ] + [ h2 "New thread" + ; El.span + ~at:[ class' "new-thread-info" ] + [ el_txt "Click the map and make a new thread:" ] + ; new_thread_form_div + ] + in + let thread_view = Html_thread.f t_s in + let new_thread_link = Html_thread.new_thread_link_el t_s in + let return_link = El.a ~at:[ href (to_path Home) ] [ el_txt "Return" ] in + let navigation_div = + El.div + ~at:[ class' "home-left-navigation-div" ] + [ new_thread_link; return_link ] + in + let mode k = S.map (is_page_kind k) t_s in + def_on (mode New_thread) new_thread_view; + def_off (mode Thread) navigation_div; + def_on (mode Thread) thread_view; + def_off (mode New_thread) new_thread_link; + def_on (mode New_thread) return_link; + let el = + El.div + ~at:[ class' "home-left" ] + [ navigation_div; new_thread_view; thread_view ] + in + el + + let f t_s = + let left_el = left t_s in + let right_el = Leaflet_map.f t_s in + let el = El.div ~at:[ class' "home-page" ] [ left_el; right_el ] in + def_on + (S.map + (fun t -> + is_page_kind Home t || is_page_kind New_thread t + || is_page_kind Thread t ) + t_s ) + el; + el +end + +module About = struct + let f t_s = + let l = [ h1 "TODO about page" ] in + let el = mk_page About t_s l in + el +end + +module Register = struct + let f t_s = + let l = [ h1 "Register"; Html_form.mk_register () ] in + let el = mk_page Register t_s l in + el +end + +module Login = struct + let f t_s = + let l = [ h1 "Login"; Html_form.mk_login () ] in + let el = mk_page Login t_s l in + el +end + +module Admin = struct + let mk t_s t = + match get_user_admin t with + | None -> [] + | Some _user -> ( + match t.page with + | Home | New_thread | Thread _ | About | Register | Login | Profile + | Account | Delete _ | Report _ | User _ -> + [] + | Admin (Loading ()) -> loading_el + | Admin (Not_found ()) -> not_found_el + | Admin (Ready reports) -> + let forms = + match reports with + | [] -> + [ el_txt "Report list is empty!~" + ; El.br () + ; el_txt "good job! ( ๑>ᴗ<๑ )" + ] + | reports -> + (* TODO add reported_post_parent_t_id to report type? *) + List.map + (fun report -> + let post = report.reported_post in + let post_view = Html_post.post_view t_s post in + let span_info_on_report = + let s = + Fmt.str "From: %s, Reason: %s" report.reporter_nick + report.reason + in + El.span [ el_txt s ] + in + let forms = + El.div + Html_form. + [ admin_ignore post.id + ; admin_delete post.id + ; admin_banish post.poster_id + ] + in + El.div + ~at:[ class' "report" ] + [ post_view; span_info_on_report; forms ] ) + reports + in + let reports_div = El.div ~at:[ class' "reports-div" ] forms in + [ h1 "Administration board"; reports_div ] ) + + let f t_s = + let el = mk_page Admin t_s [] in + Elr.def_children el (S.map (mk t_s) t_s); + el +end + +module Profile = struct + let mk t = + match get_user t with + | None -> [] + | Some user -> + let public_profile_link = + El.p + [ el_txt "Check your " + ; mk_page_link ~label:"public profile" (User (Loading user.user_id)) + ] + in + let forms = Html_form.profile user in + [ h1 "Profile settings"; public_profile_link ] @ forms + + let f t_s = + let el = mk_page Profile t_s [] in + Elr.def_children el (S.map mk t_s); + el +end + +module Account = struct + let mk t = + match get_user_private t with + | None -> [] + | Some user_private -> + let forms = Html_form.account user_private in + h1 "Account settings" :: forms + + let f t_s = + let el = mk_page Account t_s [] in + Elr.def_children el (S.map mk t_s); + el +end + +module User = struct + let mk t = + match t.page with + | Home | New_thread | Thread _ | About | Register | Login | Admin _ + | Profile | Account | Delete _ | Report _ -> + [] + | User (Loading _user_id) -> loading_el + | User (Not_found _user_id) -> not_found_el + | User (Ready user) -> + let bio = El.div [ El.blockquote (Html_util.insert_br user.bio) ] in + let img = + match user.avatar_info with + | None -> [] + | Some info -> + let alt_at = + if String.equal "" info.alt then [] + else [ alt info.alt; name info.name; title info.alt ] + in + let at = + [ Fmt.kstr src "/user/%s/avatar" user.user_id + ; class' "img-thumbnail" + ] + @ alt_at + in + [ El.img ~at () ] + in + h1 user.user_nick :: bio :: img + + let f t_s = + let el = mk_page User t_s [] in + Elr.def_children el (S.map mk t_s); + el +end + +module Delete = struct + let mk t_s t = + match get_user t with + | None -> [] + | Some user -> ( + match t.page with + | Home | New_thread | Thread _ | About | Register | Login | Admin _ + | Profile | Account | Report _ | User _ -> + [] + | Delete (Loading _id) -> loading_el + | Delete (Not_found _id) -> not_found_el + | Delete (Ready post) -> ( + match String.equal post.poster_id user.user_id with + | false -> (* TODO error can not delete other's posts *) [] + | true -> + let post_view = Html_post.post_view t_s post in + let form = Html_form.delete post in + [ post_view; form ] ) ) + + let f t_s = + let el = mk_page Delete t_s [] in + Elr.def_children el (S.map (mk t_s) t_s); + el +end + +module Report = struct + let mk t_s t = + match get_user t with + | None -> [] + | Some _user -> ( + match t.page with + | Home | New_thread | Thread _ | About | Register | Login | Admin _ + | Profile | Account | Delete _ | User _ -> + [] + | Report (Loading _id) -> loading_el + | Report (Not_found _id) -> not_found_el + | Report (Ready post) -> + let post_view = Html_post.post_view t_s post in + let form = Html_form.report post in + [ post_view; form ] ) + + let f t_s = + let el = mk_page Report t_s [] in + Elr.def_children el (S.map (mk t_s) t_s); + el +end + +module Error_popup = struct + let mk container_el opt = + match opt with + | None -> [] + | Some error -> + let dragzone = + let close_btn = + El.button ~at:[ class' "close-error-popup-btn" ] [ el_txt "X" ] + in + hold_on close_btn Ev.click (fun _ev -> Events.send_action Clear_error); + El.div ~at:[ class' "error-popup-dragzone" ] [ close_btn ] + in + Html_form.Dragzone.f ~dragzone container_el; + let content = + El.div + ~at:[ class' "error-popup-content" ] + [ El.span [ el_txt (Fmt.str "%a" Client_types.pp_error error) ] ] + in + [ dragzone; content ] + + let f t_s = + let el = El.div ~at:[ class' "error-popup" ] [] in + Elr.def_children el (S.map (fun t -> mk el t.error) t_s); + def_off (S.map (fun t -> Option.is_none t.error) t_s) el; + el +end + +module Main = struct + let f t_s = + let l = + List.map + (fun f -> f t_s) + [ Home.f + ; About.f + ; Register.f + ; Login.f + ; Admin.f + ; Profile.f + ; Account.f + ; User.f + ; Delete.f + ; Report.f + ; Error_popup.f + ] + in + let main = El.v (str "main") l in + main +end + +let def_page_title t_s = + let set_title page = + let s = + match page with + | Thread (Loading _) | User (Loading _) -> "loading" + | Thread (Not_found _) | User (Not_found _) -> "not found" + | Thread (Ready v) -> v.subject + | User (Ready u) -> u.user_nick + | page -> ( + match to_kind page with + | New_thread -> "new thread" + | kind -> Kind.to_string kind ) + in + Fmt.str "%s | Permap" s |> String.capitalize_ascii |> Jstr.v + |> Document.set_title G.document + in + S.map (fun t -> t.page) t_s |> S.changes |> hold_endless set_title; + (* init *) + let k = (S.value t_s).page in + set_title k; + () + +let f t_s = + let header_el = Header.f t_s in + let main_el = Main.f t_s in + def_page_title t_s; + [ header_el; main_el ] diff --git a/src/client/html_form.ml b/src/client/html_form.ml new file mode 100644 index 0000000..a9578eb --- /dev/null +++ b/src/client/html_form.ml @@ -0,0 +1,391 @@ +open Brr +open Note +open Note_brr +open Types +open Client_types +open Util + +let handle_submit kind form ev = + Fmt.pr "catched form submit event@."; + Ev.prevent_default ev; + Events.send_action (Submit_event (W kind, form)); + () + +let mk kind ~btn l = + let class_prefix = Form_kind.name kind in + let action = Form_kind.action kind in + let at = + [ Fmt.kstr class' "%s-form " class_prefix + ; At.action (str action) + ; At.method' (str "POST") + ; mk_at "enctype" "multipart/form-data" + ] + in + let content = l @ [ El.div [ btn ] ] in + let form = El.form ~at content in + hold_on form Brr_io.Form.Ev.submit (fun ev -> handle_submit kind form ev); + form + +(* -- TODO clean up this mess -- *) + +let mk_field_unwraped kind ~name ~label ~at = + let type' = + type' + @@ + match kind with + | `Text | `Textarea _ -> "text" + | `Password -> "password" + | `File -> "file" + in + let label = + El.label + ~at: + [ At.for' (str name); Fmt.kstr id "%s-label" name; class' "form-label" ] + [ el_txt label ] + in + let at = + [ type' + ; id name + ; At.name (str name) + ; class' "form-label" + ; Fmt.kstr (mk_at "aria-labelledby") "%s-label" name + ] + @ at + in + let item = + match kind with + | `Text | `File | `Password -> El.input ~at () + | `Textarea content -> El.textarea ~at [ el_txt content ] + in + (label, item) + +let mk_field kind ~name ~label ~at = + let label, item = mk_field_unwraped kind ~name ~label ~at in + El.div [ label; item ] + +let mk_btn ?(at = []) s = + let at = [ type' "submit"; class' "submit-btn" ] @ at in + El.button ~at [ el_txt s ] + +let mk_btn_save () = mk_btn "Save" + +let mk_btn_submit () = mk_btn "Submit" + +let mk_logout () = + let btn = + let label = "❌ Logout" in + let btn_class = "logount-btn" in + El.button ~at:[ class' btn_class ] [ el_txt label ] + in + mk Logout ~btn [] + +let mk_register () = + let nick = mk_field `Text ~name:"nick" ~label:"Nickname" ~at:[] in + let email = mk_field `Text ~name:"email" ~label:"Email" ~at:[] in + let password = mk_field `Password ~name:"password" ~label:"Password" ~at:[] in + let btn = mk_btn_submit () in + mk Register ~btn [ nick; email; password ] + +let mk_login () = + let nick = mk_field `Text ~name:"login" ~label:"Nickname or email" ~at:[] in + let password = mk_field `Password ~name:"password" ~label:"Password" ~at:[] in + let btn = mk_btn_submit () in + mk Login ~btn [ nick; password ] + +let mk_subject_field_unwraped () = + mk_field_unwraped `Text ~name:"subject" ~label:"Subject" ~at:[] + +let mk_comment_field_unwraped s = + mk_field_unwraped (`Textarea s) ~name:"comment" ~label:"Comment" ~at:[] + +let mk_image_field_unwraped () = + let file_label, file = + mk_field_unwraped `File ~name:"file" ~label:"Add picture" + ~at: + [ mk_at "accept" + (String.concat "," (Array.to_list Config.supported_mime_type)) + ] + in + let alt = + El.div + ~at:[ class' "alt-image-input-div" ] + [ mk_field (`Textarea "") ~name:"alt" ~label:"Image desciption" ~at:[] ] + in + ((file_label, file), alt) + +let mk_image_field () = + let (file_label, file), alt = mk_image_field_unwraped () in + let file_div = El.div [ file_label; file ] in + El.div ~at:[ class' "image-input-div" ] [ file_div; alt ] + +(* -------- *) + +let sync_field input ~on form_action = + hold_on input Ev.input (fun _ev -> + let s = El.prop El.Prop.value input |> Jstr.to_string in + Events.send_action (Post_form_change (form_action s)) ); + Elr.set_prop El.Prop.value ~on input; + () + +let mk_comment_div t_s = + let open Model in + let label, textarea = mk_comment_field_unwraped "" in + let () = + let on = S.map (fun t -> t.post_form.comment |> Jstr.v) t_s |> S.changes in + let send s = Client_types.Form_comment s in + sync_field textarea ~on send + in + let focus_e = + S.map + (fun t -> + (* take reply_form here and not reply_form.is_open + so focus turn on when textarea content changes (quote insertion) *) + t.post_form ) + t_s + |> S.changes + |> E.filter_map (fun rf -> + match rf.Post_form_data.is_open with + | false -> None + | true -> Some true ) + in + Elr.set_has_focus ~on:focus_e textarea; + El.div ~at:[ class' "comment-input-div" ] [ label; textarea ] + +let mk_image_div t_s = + let open Model in + let (file_label, file), alt = mk_image_field_unwraped () in + let () = + let has_file = S.map (fun t -> Option.is_some t.post_form.file) t_s in + Util.def_on has_file alt; + let on = + S.map (fun t -> t.post_form.alt) t_s + |> S.changes |> E.filter_map Fun.id |> E.map Jstr.v + in + let send s = + let opt = if String.equal s "" then None else Some s in + Client_types.Form_alt opt + in + sync_field alt ~on send + in + hold_on file Ev.change (fun _ev -> + let opt = + match El.Input.files file with + | [] -> None + | file :: _l -> + let s = File.name file |> Jstr.to_string in + Some s + in + Events.send_action (Post_form_change (Form_file opt)) ); + (* clear image file name if needed *) + let on = + S.map + (fun t -> + match t.post_form.file with + | None -> Some (Jv.to_jstr Jv.null) + | Some _s -> None ) + t_s + |> S.changes |> E.filter_map Fun.id + in + Elr.set_prop El.Prop.value ~on file; + let file_div = El.div [ file_label; file ] in + El.div ~at:[ class' "image-input-div" ] [ file_div; alt ] + +let new_thread_el t_s = + let open Model in + let subject = + let label, input = mk_subject_field_unwraped () in + let () = + let on = + S.map (fun t -> t.post_form.subject |> Jstr.v) t_s |> S.changes + in + let send s = Client_types.Form_subject s in + sync_field input ~on send + in + El.div ~at:[ class' "subject-input-div" ] [ label; input ] + in + let comment = mk_comment_div t_s in + let image = mk_image_div t_s in + let lat = + El.input ~at:[ type' "hidden"; id "lat-input"; name "lat-input" ] () + in + let lng = + El.input ~at:[ type' "hidden"; id "lng-input"; name "lng-input" ] () + in + let latlng_s = S.map (fun t -> t.post_form.latlng) t_s in + Elr.def_at At.Name.value + (latlng_s |> S.map (Option.map fst) |> S.map (Option.map Jstr.of_float)) + lat; + Elr.def_at At.Name.value + (latlng_s |> S.map (Option.map snd) |> S.map (Option.map Jstr.of_float)) + lng; + let btn = + let at = [ class' "submit-post-btn" ] in + mk_btn ~at "Post" + in + Util.def_disabled (S.map Option.is_none latlng_s) btn; + mk Home ~btn [ subject; comment; image; lat; lng ] + +let profile user = + let mk = mk Profile in + let nickname = + let nick = + mk_field `Text ~name:"nick" ~label:"Change nickname" + ~at:[ value user.user_nick ] + in + let btn = mk_btn_save () in + let form = mk ~btn [ nick ] in + [ h2 "Nickname"; form ] + in + let bio = + let bio = + mk_field (`Textarea user.bio) ~name:"bio" ~label:"Change your biography" + ~at:[] + in + let btn = mk_btn_save () in + let form = mk ~btn [ bio ] in + [ h2 "Biography"; form ] + in + let avatar = + (* TODO + - small preview off current avatar on the left of delete avatar button + - preview of image to be uploaded + - add image preview in new-thread/reply form too*) + let delete = + user.avatar_info + |> Option.map (fun _ -> + let input_el = + El.input + ~at:[ type' "hidden"; name "delete-avatar"; value "" ] + () + in + let btn = mk_btn "delete current avatar" in + mk ~btn [ input_el ] ) + |> Option.to_list + in + let upload = + let file_el = + mk_field `File ~name:"file" ~label:"Change your avatar" + ~at: + [ mk_at "accept" + (String.concat "," (Array.to_list Config.supported_mime_type)) + ] + in + (* TODO disable alt field if no image; do the same for post form *) + let alt_el = + let content = + Option.fold ~none:"" ~some:(fun img -> img.alt) user.avatar_info + in + mk_field (`Textarea content) ~name:"alt" ~label:"Image desciption" + ~at:[] + in + let btn = mk_btn_save () in + [ mk ~btn [ file_el; alt_el ] ] + in + (h2 "Avatar" :: delete) @ upload + in + nickname @ bio @ avatar + +let account user_private = + let mk = mk Account in + let email = + let email = + mk_field `Text ~name:"email" ~label:"Email" + ~at:[ value user_private.User_private.email ] + in + let btn = mk_btn_save () in + let form = mk ~btn [ email ] in + [ h2 "Change email"; form ] + in + let password = + let pw1 = + mk_field `Password ~name:"new-password" ~label:"New password" ~at:[] + in + let pw2 = + mk_field `Password ~name:"confirm-new-password" + ~label:"Confirm new password" ~at:[] + in + let btn = mk_btn_save () in + let form = mk ~btn [ pw1; pw2 ] in + [ h2 "Change password"; form ] + in + let big_delete = + let btn = mk_btn ~at:[ class' "delete-account-btn" ] "DELETE ACCOUNT" in + let form = + mk ~btn + [ El.input ~at:[ type' "hidden"; name "delete-account"; value "" ] () ] + in + [ h2 "Delete account"; form ] + in + email @ password @ big_delete + +let delete post = + let btn = mk_btn "DELETE" in + mk (Delete post.id) ~btn [] + +let report post = + let btn = mk_btn "Report" in + let reason = mk_field `Text ~name:"reason" ~label:"Reason" ~at:[] in + mk (Report post.id) ~btn [ reason ] + +let admin_ignore post_id = + let btn = mk_btn "ignore" in + mk (Admin_ignore post_id) ~btn [] + +let admin_delete post_id = + let btn = mk_btn "DELETE" in + mk (Admin_delete post_id) ~btn [] + +let admin_banish user_id = + let btn = mk_btn "BANISH" in + mk (Admin_banish user_id) ~btn [] + +module Dragzone = struct + (* TODO + - send drag_state to model on dragend (mouseup) + need to differentiate which popup we are dragging for this *) + let drag_state = ref None + + let on_mousedown dragzone container ev = + match !drag_state with + | Some _ -> Fmt.failwith "Dragzone state error: double mousedown?" + | None -> + let evt = Ev.as_type ev in + let offset_x = El.bound_x container -. Ev.Mouse.client_x evt in + let offset_y = El.bound_y container -. Ev.Mouse.client_y evt in + drag_state := Some (dragzone, container, offset_x, offset_y); + (* css so nothing get highlighted *) + El.set_inline_style (Jstr.v "user-select") (Jstr.v "none") body; + El.set_inline_style (Jstr.v "pointer-events") (Jstr.v "none") body; + El.set_inline_style (Jstr.v "pointer-events") (Jstr.v "auto") dragzone + + let on_mousemove ev = + match !drag_state with + | None -> () + | Some (_dragzone, container, offset_x, offset_y) -> + let evt = Ev.as_type ev in + let x = Ev.Mouse.client_x evt +. offset_x in + let y = Ev.Mouse.client_y evt +. offset_y in + let x = clamp ~min:0. ~max:(window_width () -. El.bound_w container) x in + let y = clamp ~min:0. ~max:(window_height () -. El.bound_h container) y in + El.set_inline_style El.Style.position (Jstr.v "fixed") container; + El.set_inline_style El.Style.left (Fmt.kstr Jstr.v "%fpx" x) container; + El.set_inline_style El.Style.top (Fmt.kstr Jstr.v "%fpx" y) container + + let on_mouseup _ev = + match !drag_state with + | None -> () + | Some (dragzone, _container, _, _) -> + El.set_inline_style (Jstr.v "user-select") (Jstr.v "") body; + El.set_inline_style (Jstr.v "pointer-events") (Jstr.v "") body; + El.set_inline_style (Jstr.v "pointer-events") (Jstr.v "") dragzone; + drag_state := None + + let () = + hold_endless_on_window Ev.mousemove on_mousemove; + hold_endless_on_window Ev.mouseup on_mouseup; + () + + let f ~dragzone container = + hold_on dragzone Ev.mousedown (fun ev -> on_mousedown dragzone container ev); + () +end diff --git a/src/client/html_post.ml b/src/client/html_post.ml new file mode 100644 index 0000000..23c9e21 --- /dev/null +++ b/src/client/html_post.ml @@ -0,0 +1,335 @@ +open Brr +open Note +open Note_brr +open Types +open Client_types +open Util + +let nick post = + El.a + ~at: + [ class' "user-link" + ; class' "post-author-nick" + ; mk_at "data-user-id" post.poster_id + ; Fmt.kstr href "/user/%s" post.poster_id + ] + [ el_txt post.poster_nick ] + +let date post = + let print_date t = + let t = Unix.localtime t in + Fmt.str "%02d-%02d-%02d %02d:%02d" (1900 + t.tm_year) (1 + t.tm_mon) + t.tm_mday t.tm_hour t.tm_min + in + El.span + ~at:[ class' "post-date"; mk_at "data-time" (string_of_float post.date) ] + [ el_txt (print_date post.date) ] + +(* TODO rm this since we can click the post_id? *) +let link_to_post ?(is_vignette = false) post = + let url = + if is_vignette then Fmt.str "/thread/%d#%d" post.parent_t_id post.id + else Fmt.str "#%d" post.id + in + El.a + ~at:[ href url; title "Link to this post"; class' "post-link-to-self" ] + [ el_txt "#" ] + +let post_id post = + let el = + let at = + [ class' "post-id" + ; title "Reply to this post" + ; mk_at "data-id" (string_of_int post.id) + ; Fmt.kstr href "#%d" post.id + ] + in + El.a ~at [ el_txt (Fmt.str ">>%d" post.id) ] + in + hold_on el Ev.click (fun _ev -> + Events.send_action (Post_form_change Form_open); + Events.send_action (Post_form_change (Form_insert_quote post.id)) ); + el + +let post_id_quote = + let is_local_link t_s id = + (* list of post currently on the page + (consider thread page case only) *) + let post_l = + match (S.value t_s).Model.page with + | Thread (Ready v) -> v.reply_l + | _ -> [] + in + List.find_opt (fun p -> p.id = id) post_l |> Option.is_some + in + let hold_highlight_event el id = + let mouseenter = Evr.on_el Ev.mouseenter Evr.unit el in + let mouseleave = Evr.on_el Ev.mouseleave Evr.unit el in + let focus = Evr.on_el Ev.focus Evr.unit el in + let blur = Evr.on_el Ev.blur Evr.unit el in + let off = E.select [ mouseleave; blur ] |> E.map (fun () -> None) in + let on = + E.select [ mouseenter; focus ] + |> E.map (fun () -> Some (get_bounds el, id)) + in + let event = E.select [ off; on ] in + hold_event_on el event (fun opt -> + Events.send_action (Quickview_change opt) ); + () + in + fun t_s id -> + let at = [ class' "post-id-quote"; mk_at "data-id" (string_of_int id) ] in + let txt = el_txt (Fmt.str ">>%d" id) in + match is_local_link t_s id with + | true -> + (* simple #%d link *) + let at = [ Fmt.kstr href "#%d" id ] @ at in + let el = El.a ~at [ txt ] in + hold_highlight_event el id; + el + | false -> + (* remote link *) + let at = [ class' "remote" ] @ at in + let container = El.span [] in + hold_highlight_event container id; + let children = + let open Page in + S.map (fun t -> t.Model.quickview) t_s + |> S.changes |> E.filter_map Fun.id + |> E.map (fun (rect, v) -> + let quickview_id = unwrap_post_id v in + fun last_value -> + match quickview_id = id with + | true -> Some (rect, v) + | false -> last_value ) + |> S.accum None + |> S.map (function + | None -> [ El.button ~at [ txt ] ] + | Some (_rect, v) -> ( + match v with + | Loading _ -> + let at = [ class' "loading" ] @ at in + [ El.button ~at [ txt ] ] + | Not_found _ -> + let at = [ class' "not-found" ] @ at in + [ El.button ~at [ txt ] ] + | Ready p -> + let at = + [ class' "ready" + ; Fmt.kstr href "/thread/%d#%d" p.parent_t_id p.id + ] + @ at + in + [ El.a ~at [ txt ] ] ) ) + in + Elr.def_children container children; + container + +let post_menu t_s post = + let mk s = + El.a + ~at: + [ Fmt.kstr href "/%s/%d" s post.id + ; Fmt.kstr class' "%s-link" s + ; mk_at "data-post-id" (string_of_int post.id) + ] + [ el_txt (String.capitalize_ascii s) ] + in + let mk_content () = + let delete = mk "delete" in + let report = mk "report" in + let own_post = + S.map Model.get_user t_s + |> S.map (function + | None -> false + | Some u -> String.equal u.user_id post.poster_id ) + in + def_on own_post delete; + [ delete; report ] + in + Html_util.mk_dropdown_menu ~class_prefix:"post-info" ~label:"" + ~at_title:"Post menu" ~placeholder:false mk_content + +let backlinks t_s post = + let l = List.map (post_id_quote t_s) post.backlinks in + El.div ~at:[ class' "post-replies" ] l + +let image t_s ?(is_vignette = false) post = + match post.image_info with + | None -> None + | Some image -> ( + (* TODO show image dimension/name *) + let mk is_small = + let class_small = + if is_small then [ class' "post-image-small" ] else [] + in + let sizes = + [ mk_at "width" + (string_of_int (if is_small then image.thumb_w else image.w)) + ; mk_at "height" + (string_of_int (if is_small then image.thumb_h else image.h)) + ] + in + let url = + src + @@ + if is_small then Fmt.str "/img/s/%d" post.id + else Fmt.str "/img/%d" post.id + in + let at = + class_small @ sizes + @ url + :: [ class' "post-image" + ; alt image.alt + ; title image.alt + ; mk_at "data-id" (string_of_int post.id) + ; mk_at "loading" "lazy" + ] + in + El.img ~at () + in + let img_small, img_big = (mk true, mk false) in + let el = El.div ~at:[ class' "post-image-div" ] [ img_small ] in + match is_vignette with + | true -> Some el + | false -> + (* swap img_(small/big) on click *) + hold_on el Ev.click (fun _ev -> Events.send_action (Image_click post.id)); + let img_s = + S.map (fun t -> t.Model.opened_image) t_s + |> S.map (function + | Some id when Int.equal id post.id -> [ img_big ] + | Some _ | None -> [ img_small ] ) + in + Elr.def_children el img_s; + Some el ) + +let comment = + let open Comment in + let insert_br_between_lines l = + match l with + | [] -> [] + | hd :: tl -> + List.rev + @@ List.fold_left (fun acc x -> x :: [ El.br () ] :: acc) [ hd ] tl + in + let item t_s = function Txt s -> el_txt s | Id i -> post_id_quote t_s i in + let items t_s l = List.map (item t_s) l in + let line t_s = function + | Line l -> items t_s l + | Line_quote l -> + [ El.span ~at:[ class' "line-quote" ] (el_txt ">" :: items t_s l) ] + in + fun t_s comment -> + let content = + List.map (line t_s) comment |> insert_br_between_lines |> List.flatten + in + El.div ~at:[ class' "post-comment" ] content + +let info t_s post = + El.div + ~at:[ class' "post-info" ] + [ nick post + ; date post + ; post_id post + ; link_to_post post + ; post_menu t_s post + ; backlinks t_s post + ] + +let post_view t_s post = + let info = info t_s post in + let content = + let comment = comment t_s post.comment in + let l = + match image t_s post with + | None -> [ comment ] + | Some image -> [ image; comment ] + in + El.div ~at:[ class' "post-content" ] l + in + let at = [ class' "post"; id (string_of_int post.id) ] in + let el = El.div ~at [ info; content ] in + let is_selected = + S.map + (fun t -> + match t.Model.fragment with + | Id v -> + let id = Fragment.unwrap_id v in + post.id = id + | Empty | Top | Bottom -> false ) + t_s + in + Elr.def_class (Jstr.v "selected") is_selected el; + let is_highlighted = + S.map (fun t -> t.Model.quickview) t_s + |> S.map (function + | None -> false + | Some (_rect, v) -> Int.equal post.id (Page.unwrap_post_id v) ) + in + Elr.def_class (Jstr.v "highlighted") is_highlighted el; + el + +module Quickview = struct + open Model + + let quickview_class = "quickview-div" + + let to_px_jstr x = x |> int_of_float |> Fmt.str "%dpx" |> Jstr.of_string + + let is_in_viewport post = + (* find highlighted post DOM element *) + let id = string_of_int post.id in + match find_html_el_by_id id with + | None -> false + | Some el -> + (* check bounds *) + let x, y, w, h = get_bounds el in + let ( <= ) x y = Float.compare x y <= 0 in + 0. <= x && 0. <= y + && x +. w <= window_width () + && y +. h <= window_height () + + let f t_s = + let container = El.div ~at:[ class' quickview_class ] [] in + let mk (id_x, id_y, id_w, id_h) post = + if is_in_viewport post then [] + else + let quickview = post_view t_s post in + (* ensure we don't have duplicate html id attribute *) + El.set_at At.Name.id (Some (Jstr.v "quickview")) quickview; + (* hack: insert hidden quickview into DOM so we can compute it's bounds + we don't use the viewed post's already in DOM element for this + - it might actually not be in DOM + - it might have it's image opened and size changed *) + El.set_inline_style El.Style.visibility (Jstr.v "hidden") quickview; + El.set_children container [ quickview ]; + (* compute quickview position *) + let quickview_x = id_x +. id_w in + let quickview_h = El.bound_h quickview in + let quickview_y = id_y +. (0.5 *. id_h) -. (0.5 *. quickview_h) in + let quickview_y = + clamp ~min:0. ~max:(window_height () -. quickview_h) quickview_y + in + (* undo quickview DOM insertion *) + El.set_inline_style El.Style.visibility (Jstr.v "visible") quickview; + El.remove quickview; + (* set quickview style *) + El.set_inline_style El.Style.position (Jstr.v "fixed") quickview; + El.set_inline_style El.Style.z_index (Jstr.v "99999") quickview; + El.set_inline_style El.Style.left (to_px_jstr quickview_x) quickview; + El.set_inline_style El.Style.top (to_px_jstr quickview_y) quickview; + [ quickview ] + in + let children = + S.map (fun t -> t.quickview) t_s + |> S.map (function + | None -> [] + | Some (rect, v) -> ( + match v with + | Page.Loading _ | Not_found _ -> [] + | Ready post -> mk rect post ) ) + in + Elr.def_children container children; + container +end diff --git a/src/client/html_thread.ml b/src/client/html_thread.ml new file mode 100644 index 0000000..205e40c --- /dev/null +++ b/src/client/html_thread.ml @@ -0,0 +1,156 @@ +open Brr +open Note +open Note_brr +open Types +open Page +open Model +open Util +open Html_util + +let thread_el_aux t_s w = + match w with + | Loading _id -> loading_el + | Not_found _id -> not_found_el + | Ready (v : Thread_w_reply.t) -> + let subject = + El.div ~at:[ class' "thread-subject" ] [ El.strong [ el_txt v.subject ] ] + in + let reply_l = + let l = + List.sort (fun a b -> Float.compare a.date b.date) v.reply_l + |> List.map (fun p -> Html_post.post_view t_s p) + in + El.div ~at:[ class' "thread-replies" ] l + in + [ subject; reply_l ] + +let thread_el t_s w = + let id = Jstr.of_int (unwrap_thread_id w) in + let el = + El.div + ~at:[ class' "thread"; At.v (str "data-id") id ] + (thread_el_aux t_s w) + in + el + +let reply_popup_el t_s w = + let dragzone = + let close_btn = + El.button ~at:[ class' "close-reply-popup-btn" ] [ el_txt "X" ] + in + hold_on close_btn Ev.click (fun _ev -> + Events.send_action (Post_form_change Form_close) ); + El.div ~at:[ class' "reply-popup-dragzone" ] [ close_btn ] + in + let content = + let open Html_form in + let comment = mk_comment_div t_s in + let image = mk_image_div t_s in + let btn = mk_btn "Post" in + let form = mk (Thread (unwrap_thread_id w)) ~btn [ comment; image ] in + let el = El.div ~at:[ class' "reply-popup-content" ] [ form ] in + el + in + let el = El.div ~at:[ class' "reply-popup" ] [ dragzone; content ] in + Html_form.Dragzone.f ~dragzone el; + let is_visible_s = S.map (fun t -> t.post_form.is_open) t_s in + def_on is_visible_s el; + el + +let new_thread_link_el t_s = + let mk user = + match user with + | None -> + (* TODO redirect *) + mk_page_link ~label:"Login to post a thread!" Login + | Some _user -> + El.a ~at:[ href (Page.to_path New_thread) ] [ el_txt "New thread" ] + in + let el = El.div ~at:[ class' "new-thread-link-div" ] [] in + let children = S.map get_user t_s |> S.map (fun u -> [ mk u ]) in + Elr.def_children el children; + el + +let bump_status_el v = + el_txt + @@ + match v with + | Types.Dead -> "Dead thread" + | Locked c -> + Fmt.str "bump order: [%d/%d]\nLocked thread, You cannot reply anymore." c + Config.thread_alive_max_count + | Alive c -> Fmt.str "bump order: [%d/%d]" c Config.thread_alive_max_count + +let reply_btn_el t_s w = + let mk user = + match w with + | Loading _ | Not_found _ -> [] + | Ready (v : Thread_w_reply.t) -> + let el = + match user with + | None -> + (* TODO redirect *) + mk_page_link ~label:"Login to reply!" Login + | Some _user -> ( + match v.bump_status with + | Dead | Locked _ -> bump_status_el v.bump_status + | Alive _bump_order -> + let btn = + let at = [ class' "open-reply-popup-btn" ] in + El.button ~at [ el_txt "Post a reply" ] + in + let is_hidden = S.map (fun t -> t.post_form.is_open) t_s in + Elr.def_class (Jstr.v "hidden") is_hidden btn; + hold_on btn Ev.click (fun _ev -> + Events.send_action (Post_form_change Form_open) ); + btn ) + in + [ el ] + in + let el = El.div ~at:[ class' "reply-popup-btn-div" ] [] in + let children = S.map get_user t_s |> S.map mk in + Elr.def_children el children; + el + +(* need t_s for user + reply form open/close state *) +let nav_el kind t_s w = + let str = match kind with `Top -> "top" | `Bottom -> "bottom" in + let str_inv = match kind with `Top -> "bottom" | `Bottom -> "top" in + let update_el = + El.a ~at:[ href (Page.to_path (Thread w)) ] [ el_txt "Update" ] + in + let at = + match kind with + | `Top -> + (* id="top" is set on nav bar instead *) + [ class' "sub-nav" ] + | `Bottom -> [ class' "sub-nav"; id str ] + in + let el = + El.div ~at + [ new_thread_link_el t_s + ; reply_btn_el t_s w + ; update_el + ; El.a + ~at:[ Fmt.kstr href "#%s" str_inv ] + [ Fmt.kstr el_txt "Go to %s" str_inv ] + ] + in + el + +let mk t_s w = + [ nav_el `Top t_s w + ; thread_el t_s w + ; reply_popup_el t_s w + ; Html_post.Quickview.f t_s + ; nav_el `Bottom t_s w + ] + +let f t_s = + let el = El.div ~at:[ class' "thread-view" ] [] in + let children_s = + S.map get_thread_w_reply t_s + |> S.map (function None -> [] | Some w -> mk t_s w) + in + Elr.def_children el children_s; + el diff --git a/src/client/html_util.ml b/src/client/html_util.ml new file mode 100644 index 0000000..4135605 --- /dev/null +++ b/src/client/html_util.ml @@ -0,0 +1,80 @@ +open Brr +open Note +open Model +open Util + +let loading_el = [ el_txt "ฅ^•ﻌ•^ฅ loading" ] + +let not_found_el = [ el_txt "ฅ^•ﻌ•^ฅ not found" ] + +let mk_page_link ?label p = + let open Page in + let href = href (to_path p) in + let k = to_kind p in + let s = Kind.to_string k in + let label = + match label with + | Some label -> label + | None -> ( + match Kind.to_emoji k with + | None -> s + | Some emoji -> Fmt.str "%s %s" emoji s ) + in + El.a ~at:[ Fmt.kstr class' "%s-link" s; href ] [ el_txt label ] + +let is_page_kind k t = Page.(Kind.equal k (to_kind t.page)) + +let mk_page kind t_s l = + let el = + let at = [ Fmt.kstr class' "%s-page" (Page.Kind.to_string kind) ] in + El.div ~at l + in + let is_on = S.map (is_page_kind kind) t_s in + def_on is_on el; + el + +let insert_br s = + match String.split_on_char '\n' s with + | [] -> [] + | hd :: tl -> + List.rev + @@ List.fold_left + (fun acc x -> el_txt x :: El.br () :: acc) + [ el_txt hd ] + tl + +(* glorious CSS dropdown menu + - need to take mk_content and not just content because El.t are only added one time in DOM + -> or clone content + - need placeholder for correct style *) +let mk_dropdown_menu ~class_prefix ~label ~at_title ~placeholder mk_content = + let mk_btn suffix = + let at = + [ Fmt.kstr class' "%s-dropdown%s" class_prefix suffix + ; Fmt.kstr class' "dropdown%s" suffix + ] + in + let arrow = El.span ~at:[ class' "dropdown-arrow" ] [ el_txt "▶" ] in + El.button ~at [ arrow; el_txt label ] + in + let mk_dropdown_content suffix = + let at = + [ Fmt.kstr class' "%s-dropdown-content%s" class_prefix suffix + ; Fmt.kstr class' "dropdown-content%s" suffix + ] + in + let l = mk_content () |> List.map (fun o -> El.li [ o ]) in + El.ul ~at l + in + let at = + [ Fmt.kstr class' "%s-dropdown" class_prefix + ; class' "dropdown" + ; At.title (str at_title) + ] + in + let l = + mk_btn "-open-btn" + :: (if placeholder then [ mk_dropdown_content "-placeholder" ] else []) + @ [ mk_dropdown_content ""; mk_btn "-close-btn" ] + in + El.div ~at l diff --git a/src/client/leaflet_map.ml b/src/client/leaflet_map.ml new file mode 100644 index 0000000..1d33cd7 --- /dev/null +++ b/src/client/leaflet_map.ml @@ -0,0 +1,199 @@ +open Brr +open Note +open Leaflet +open Util + +let geoloc_btn = + let s = "geolocalize-btn" in + El.button ~at:[ class' s; id s ] [ el_txt "Geolocalize me" ] + +let buttons = + let new_thread_link = + El.a ~at:[ href (Page.to_path New_thread) ] [ el_txt "New thread" ] + in + El.div ~at:[ class' "map-btn-div" ] [ new_thread_link; geoloc_btn ] + +let map_el = El.div ~at:[ id "map" ] [] + +let map = Map.create_from_div map_el + +let set_view (lat, lng, zoom) = + let latlng = Latlng.create ~lat ~lng in + let zoom = Some zoom in + Map.set_view latlng ~zoom map; + () + +let get_view () = + let latlng = Map.get_center map in + let lat = Latlng.lat latlng in + let lng = Latlng.lng latlng in + let zoom = Map.get_zoom map in + let wrapped_latlng = Map.wrap_latlng latlng map in + let is_wrapped = not @@ Latlng.equals latlng wrapped_latlng in + if is_wrapped then ( + (* wrap coordinates so we don't drift into a parralel universe + and lose track of markers *) + let w_lat = Latlng.lat wrapped_latlng in + let w_lng = Latlng.lng wrapped_latlng in + set_view (w_lat, w_lng, zoom); + (w_lat, w_lng, zoom) ) + else (lat, lng, zoom) + +(* todo better leaflet interface for open/close_popup? *) +let open_popup content latlng = + let popup = Popup.create ~content:(Some content) ~latlng:(Some latlng) [||] in + Map.open_popup popup map + +let close_popup () = Map.close_popup None map + +let on_move_end f = Map.on Event.Move_end f map + +let on_zoom_end f = Map.on Event.Zoom_end f map + +let on_click f = Map.on Event.Click f map + +(* init map, setup events *) +let () = + Note_brr.Elr.on_add + (fun () -> + Fmt.pr "leaflet map init@."; + let osm_layer = Layer.create_tile_osm [||] in + Layer.add_to map osm_layer; + set_view (Storage.init_map_view ()) ) + map_el; + on_move_end (fun _ev -> + let o = get_view () in + Events.send_action (Map_input (Move_end o)) ); + on_zoom_end (fun _ev -> + let o = get_view () in + Events.send_action (Map_input (Zoom_end o)) ); + on_click (fun ev -> + let latlng = + (* TODO wrap/check it server side too *) + (* wrap it to avoid creating thread on wrong earth *) + let latlng = Event.latlng ev in + Map.wrap_latlng latlng map + in + let lat = Latlng.lat latlng in + let lng = Latlng.lng latlng in + Events.send_action (Map_input (Click_latlng (lat, lng))) ); + (* TODO: + - show a loading animation until we get the geolocation + - show something in case of error + - add special marker on map *) + let geolocalize _ev = + let open Brr_io.Geolocation in + let l = of_navigator G.navigator in + let opts = opts ~high_accuracy:true () in + Events.send_action (Map_input Geoloc_start); + (* only get first Geoloc_pos for now + let _ : watch_id = + watch l ~opts (fun pos_res -> + *) + let _fut : unit Fut.t = + get l ~opts + |> Fut.map (fun pos_res -> + match pos_res with + | Error err -> Events.send_action (Map_input (Geoloc_err err)) + | Ok pos -> + Events.send_action (Map_input (Geoloc_pos pos)); + let lat = Pos.latitude pos in + let lng = Pos.longitude pos in + let zoom = 17 in + set_view (lat, lng, zoom); + Storage.set_map_view (lat, lng, zoom); + () ) + in + () + in + hold_on geoloc_btn Ev.click geolocalize; + () + +let toggle_latlng_popup latlng_opt = + match latlng_opt with + | None -> close_popup () + | Some (lat, lng) -> + (* TODO add a marker with special icon here *) + open_popup "create thread here" (Latlng.create ~lat ~lng) + +module Markers = struct + let icon mode = + (* TODO define in App *) + let default_url = "/assets/img/marker-icon.png" in + let default_icon = Icon.create default_url [||] in + let selected_icon = Icon.create default_url [||] in + match mode with `Selected -> selected_icon | `Normal -> default_icon + + let selected_id = ref None + + let select id_opt = selected_id := id_opt + + let is_selected id = + match !selected_id with + | None -> false + | Some selected_id -> Int.equal selected_id id + + let refresh = + let set_layer = + (* replace previous geojson layer: avoid stacking layers and handle thread deletion *) + let layer_ref = ref None in + fun layer -> + Option.iter (Layer.remove_from map) !layer_ref; + layer_ref := Some layer; + Layer.add_to map layer + in + let on_marker_click id = + Events.send_action (Map_input (Click_marker id)); + Navigation.load (Thread (Loading id)) + in + let spawn_marker geojsonpoint_feature latlng = + let id = + let feature_properties = Jv.get geojsonpoint_feature "properties" in + Jv.get feature_properties "id" |> Jv.to_int + in + let icon = + match is_selected id with + | false -> icon `Normal + | true -> icon `Selected + in + let marker = Marker.create latlng [| Icon icon |] in + Layer.on Event.Click (fun _ev -> on_marker_click id) marker; + marker + in + fun catalog -> + let geojson_res = + let open Types in + catalog + |> List.map (fun v -> (v.lat, v.lng, v.op.id)) + |> Json_data.Write.geojson_markers |> Jstr.of_string |> Brr.Json.decode + in + match geojson_res with + | Error e -> + Fmt.failwith "Markers.refresh failure: geojson serialization error `%s`" + (Util.str_of_error e) + | Ok geojson -> + let layer = + Layer.create_geojson geojson [| Point_to_layer spawn_marker |] + in + set_layer layer +end + +let f t_s = + let open Model in + S.map (fun t -> t.post_form.latlng) t_s + |> S.changes + |> hold_endless toggle_latlng_popup; + S.map (fun t -> t.page) t_s + |> S.changes + |> E.map (fun _ -> None) + |> hold_endless toggle_latlng_popup; + (* todo: refresh on selection change may be too much because we clear and re-add all markers *) + S.map (fun t -> t.catalog) t_s |> S.changes |> hold_endless Markers.refresh; + S.map get_thread_w_reply t_s + |> S.map (Option.map Page.unwrap_thread_id) + |> S.changes + |> hold_endless (fun id_opt -> + Markers.select id_opt; + Markers.refresh (S.value t_s).catalog ); + let el = El.div ~at:[ class' "home-right" ] [ map_el; buttons ] in + el diff --git a/src/client/main.ml b/src/client/main.ml new file mode 100644 index 0000000..af018bc --- /dev/null +++ b/src/client/main.ml @@ -0,0 +1,24 @@ +open Brr +open Note +open Model + +let ui : t -> t signal * El.t list = + fun t -> + let def t_s = + let els = Html.f t_s in + let do_stuff = + let do_actions = E.map do_action Events.actions in + let do_data_updates = E.map do_data_update Events.data_updates in + let do_error_popup_updates = E.map do_error Events.errors in + E.select [ do_actions; do_data_updates; do_error_popup_updates ] + in + let t_s' = S.accum (S.value t_s) do_stuff in + (t_s', (t_s', els)) + in + S.fix t def + +let () = + let t_s, els = ui (init ()) in + (* don't forget to hold model signal! *) + Logr.(hold @@ S.log t_s (fun (_ : Model.t) -> ())); + El.set_children Util.body els diff --git a/src/client/model.ml b/src/client/model.ml new file mode 100644 index 0000000..fce19ba --- /dev/null +++ b/src/client/model.ml @@ -0,0 +1,303 @@ +open Types +open Client_types +open Page + +type t = + { (* navigation state *) + session : session + ; fragment : Fragment.t + ; (* ui state *) + catalog : thread list + ; page : Page.t + ; post_form : Post_form_data.t + ; map_view : float * float * int + ; (* todo: just remove rect from here *) + quickview : (rect * (post_id, post) wrap) option + ; opened_image : post_id option + ; error : Client_types.error option + } + +(* TODO better session initialization/no dummy *) +(* initialize session with dummy data and launch a GET.session request + catalog will be fetched on home and thread page navigation *) +let init () = + Fmt.pr "model init@."; + let dummy_session = + { user_private = None; csrf_token = "dummy"; csrf_time_limit = 0.0 } + in + Network.GET.session (); + { session = dummy_session + ; fragment = Empty + ; catalog = Db.get_catalog () + ; page = Home + ; post_form = Post_form_data.empty + ; map_view = Storage.init_map_view () + ; quickview = None + ; opened_image = None + ; error = None + } + +(* TODO mv to ../util.ml *) +let user_private_to_public u = + let User_private. + { user_id; user_nick; user_is_admin; bio; avatar_info; email = _ } = + u + in + { user_id; user_nick; user_is_admin; bio; avatar_info } + +let get_user t = Option.map user_private_to_public t.session.user_private + +let get_user_private t = t.session.user_private + +let get_user_admin t = + match get_user t with + | None -> None + | Some u -> ( match u.user_is_admin with false -> None | true -> Some u ) + +let get_thread_w_reply t = match t.page with Thread t -> Some t | _ -> None + +(* TODO + - use CSS `scroll-margin-top` property *) +(* History.push_state does not fire hashchange + scroll, so we have to do it + manually this relies on html `id` attribute: + - id attribute must exists and be unique + be careful to have posts only once in the html + - if html is invalid and multiple element have the same id. + it scroll to the first, which can be in a hidden page + This must be called after a fragment change when page is ready. + The DOM need to be re-rendered before we scroll_into_view. + So the scoll is delayed to the next JavaScript event loop cycle + with [Futr.to_event] *) +let schedule_scroll_into_view = + let f opt = + match opt with + | None -> () + | Some "" -> () + | Some id -> ( + match Util.find_html_el_by_id id with + | None -> + Fmt.failwith "scroll_into_view: html element with id `%s` not found@." + id + | Some el -> + Fmt.pr "scroll_into_view `%s`@." id; + Brr.El.scroll_into_view el ) + in + fun s -> + let open Note in + (* TODO hold; need a hold_once? *) + let _ : Logr.t = + Fut.return s |> Note_brr.Futr.to_event |> E.obs + |> Logr.(app (const f)) + |> Logr.create ~now:false + in + () + +let load_aux find is_404 id = + match find id with + | Some v -> Ready v + | None -> ( + match is_404 id with true -> Not_found id | false -> Loading id ) + +let load_thread v = + let id = unwrap_thread_id v in + load_aux Db.find_thread_w_reply Db.thread_is_404 id + +let load_post v = + let id = unwrap_post_id v in + load_aux Db.find_post Db.post_is_404 id + +let load_user v = + let id = unwrap_user_id v in + load_aux Db.find_user Db.user_is_404 id + +let load_fragment page fragment = + let open Fragment in + match fragment with + | Empty | Top | Bottom -> fragment + | Id v -> + let id = unwrap_id v in + (* only consider fragment on thread pages *) + let v = + match page with + | Thread v -> ( + match v with + | Loading _ -> Loading id + | Not_found _ -> Not_found id + | Ready v -> ( + match List.exists (fun p -> p.id = id) v.reply_l with + | false -> Not_found id + | true -> Ready id ) ) + | _ -> Not_found id + in + Id v + +let load_page = function + | Home -> Home + | New_thread -> New_thread + | About -> About + | Register -> Register + | Login -> Login + | Profile -> Profile + | Account -> Account + | Admin _ -> Admin (Ready (Db.get_reports ())) + | Thread id -> Thread (load_thread id) + | User id -> User (load_user id) + | Delete id -> Delete (load_post id) + | Report id -> Report (load_post id) + +let load_quickview opt = + opt + |> Option.map (fun (rect, v) -> + ( rect + , match v with Ready _ | Not_found _ -> v | Loading _ -> load_post v ) ) + +let load_model t = + let session = Db.get_session () in + let catalog = Db.get_catalog () in + let page = load_page t.page in + let fragment = load_fragment page t.fragment in + let () = + match + (Fragment.get_ready_value t.fragment, Fragment.get_ready_value fragment) + with + | _, None | Some _, Some _ -> () + | None, Some s -> schedule_scroll_into_view s + in + let quickview = load_quickview t.quickview in + { t with session; catalog; page; fragment; quickview } + +let do_post_form_action form_action post_form = + let open Post_form_data in + match form_action with + | Form_open -> { post_form with is_open = true } + | Form_close -> { post_form with is_open = false } + | Form_insert_quote id -> + let comment = + let s = post_form.comment in + (* insert quote on newline *) + match String.ends_with ~suffix:"\n" s || String.length s = 0 with + | true -> Fmt.str "%s>>%d " s id + | false -> Fmt.str "%s@\n>>%d " s id + in + { post_form with comment } + | Form_comment comment -> { post_form with comment } + | Form_file file -> { post_form with file } + | Form_alt alt -> { post_form with alt } + | Form_subject subject -> { post_form with subject } + | Form_latlng latlng -> { post_form with latlng } + | Form_reset -> Post_form_data.empty + +let do_map_action a t = + let set_latlng t opt = + let post_form = do_post_form_action (Form_latlng opt) t.post_form in + { t with post_form } + in + match a with + | Move_end map_view | Zoom_end map_view -> + Storage.set_map_view map_view; + { t with map_view } + | Geoloc_start -> t + | Geoloc_pos (_pos : Brr_io.Geolocation.Pos.t) -> t + | Geoloc_err (_err : Brr_io.Geolocation.Error.t) -> t + | Click_latlng latlng -> ( + match t.page with New_thread -> set_latlng t (Some latlng) | _ -> t ) + | Click_marker _thread_id -> set_latlng t None + +let do_action : Client_types.action -> t -> t = + fun action t -> + Fmt.pr {|do action: "%a"@.|} pp_action action; + match action with + | Navigation_event (page_opt, frag) -> + let page = + match page_opt with + | None -> t.page + | Some loading_page -> + Network.GET.f loading_page; + load_page loading_page + in + let fragment = load_fragment page frag in + let () = + match Fragment.get_ready_value fragment with + | None -> () + | Some s -> schedule_scroll_into_view s + in + (* when we click the id to go to post blur event is not triggered, + so we clear quick view on hashchange too *) + let quickview = None in + let post_form = + match page_opt with + | None -> t.post_form + | Some _ -> { t.post_form with is_open = false; latlng = None } + in + { t with page; fragment; quickview; post_form } + | Post_form_change form_action -> ( + (* TODO error message/feedback; use Validate_str *) + (* ignore reply form action if not logged in *) + match get_user t with + | None -> t + | Some _ -> + let post_form = do_post_form_action form_action t.post_form in + { t with post_form } ) + | Map_input map_action -> do_map_action map_action t + | Submit_event (Form_kind.W kind, form) -> + let session = Db.get_session () in + Network.POST.f kind form session.csrf_token; + let t = + match kind with + | Logout -> + (* todo reload window? Brr.Window.reload Brr.G.window; *) + (* clear all state on logout + we do it here so we can logout even if offline *) + Db.clear (); + Storage.clear (); + init () + | _ -> t + in + t + | Quickview_change opt -> + let quickview = + opt |> Option.map (fun (rect, v) -> (rect, Loading v)) |> load_quickview + in + begin + match quickview with + | Some (_, Loading post_id) -> Network.GET.post post_id + | _ -> () + end; + { t with quickview } + | Image_click id -> ( + match t.opened_image with + | Some current_image_id when Int.equal current_image_id id -> + { t with opened_image = None } + | Some _ | None -> { t with opened_image = Some id } ) + | Clear_error -> { t with error = None } + +let do_data_update : Client_types.data_update -> t -> t = + fun action t -> + Fmt.pr {|do data update: "%a"@.|} pp_data_update action; + begin + match action with + | Post_update v -> Db.add_post v + | Thread_update thread_w_reply -> + Db.update_thread_w_reply (Some thread_w_reply) + | Catalog_update l -> Db.update_catalog l + | User_update u -> Db.update_user (Some u) + | Reports_update reports -> Db.update_reports reports + | Session_update session -> Db.update_session session + end; + load_model t + +let do_error : Client_types.error -> t -> t = + fun e t -> + Fmt.pr {|do error: "%a"@.|} pp_error e; + let t = { t with error = Some e } in + let () = + match e with + | Network_err _ -> () + | Err_response e -> ( + match e with + | Not_found_post id -> Db.add_post_404 id + | Not_found_thread id -> Db.add_thread_404 id + | Not_found_user user_id -> Db.add_user_404 user_id + | _ -> () ) + in + load_model t diff --git a/src/client/navigation.ml b/src/client/navigation.ml new file mode 100644 index 0000000..20d6b00 --- /dev/null +++ b/src/client/navigation.ml @@ -0,0 +1,98 @@ +(* TODO + - use navigation API when ready(?) + https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API *) +open Brr +open Util + +let of_uri uri = + let page, frag = Page.of_uri uri in + let frag = + let open Client_types.Fragment in + match of_string frag with + | Error e -> + Fmt.epr "%s@." e; + Empty + | Ok v -> v + in + (page, frag) + +let go_to ~just_change_hash uri = + let open Window in + let history = history window in + History.push_state ~uri history; + let page, frag = of_uri uri in + let opt = match just_change_hash with true -> None | false -> Some page in + Events.send_action (Navigation_event (opt, frag)); + () + +let load p = + let uri = Page.to_uri p in + go_to ~just_change_hash:false uri; + () + +let update_to_current_location () = + let uri = Window.location window in + let page, frag = of_uri uri in + Events.send_action (Navigation_event (Some page, frag)); + () + +let on_load () = + (* todo hold + only observe once; destroy logger after first event *) + hold_endless_on_window Ev.load (fun _ev -> update_to_current_location ()); + () + +let on_link_click () = + let handle_link href = + let open Window in + let is_local = String.starts_with ~prefix:"/" href in + let is_hash = String.starts_with ~prefix:"#" href in + if is_local || is_hash then + (* how to build uri correctly from just href..? *) + let href_jstr = Jstr.v href in + let uri = + let with_uri = + match is_local with + | true -> + let e = Jstr.v "" in + fun uri -> Uri.with_uri ~path:e ~query:e ~fragment:e uri + | false -> fun uri -> Uri.with_uri ~fragment:href_jstr uri + in + let base = + match with_uri (location window) with + | Error e -> + Fmt.failwith "on_link_click: with_uri error `%s`" + (Util.str_of_error e) + | Ok v -> Uri.to_jstr v + in + Uri.v ~base href_jstr + in + go_to ~just_change_hash:is_hash uri + in + let navigation_handler ev = + (* TODO rm magick if possible *) + let el : El.t = Obj.magic (Ev.target ev) in + begin + if Jstr.equal (El.tag_name el) (Jstr.v "a") then + match El.at (Jstr.v "href") el with + | None -> Fmt.failwith " element with no href" + | Some href -> begin + Ev.prevent_default ev; + handle_link (Jstr.to_string href) + end + end + in + hold_on body Ev.click navigation_handler + +let on_pop_state () = + let open Window in + hold_endless_on_window History.Ev.popstate (fun _ev -> + update_to_current_location () ); + () + +(* setup navigation listeners *) +let () = + on_load (); + on_link_click (); + on_pop_state (); + () diff --git a/src/client/network.ml b/src/client/network.ml new file mode 100644 index 0000000..a3e82b9 --- /dev/null +++ b/src/client/network.ml @@ -0,0 +1,214 @@ +open Types +open Client_types +open Util + +(* TODO handle no network connection/unreachable server *) +let handle_response meth fetch read_ok on_ok = + let open Brr_io.Fetch in + let read_body response res = + match res with + | Error e -> Error (Body_err (str_of_error e)) + | Ok jstr -> ( + let url = Jstr.to_string (Response.url response) in + let status = Response.status response in + let status_text = Jstr.to_string (Response.status_text response) in + let body = Jstr.to_string jstr in + let r = { meth; url; status; status_text; body } in + match Response.ok response with + | true -> ( + match read_ok r.body with + | Error e -> Error (Read_err (e, r)) + | Ok v -> Ok (Either.Left v) ) + | false -> ( + match Json_data.Read.err r.body with + | Error e -> Error (Read_err (e, r)) + | Ok v -> Ok (Either.Right v) ) ) + in + let read_response res = + match res with + | Error e -> Fut.return @@ Error (Fetch_err (str_of_error e)) + | Ok response -> + let body = Response.as_body response in + Body.text body |> Fut.map (read_body response) + in + let f res = + read_response res + |> Fut.map (function + | Error e -> + Events.send_error (Network_err e); + () + | Ok (Either.Left v) -> + on_ok v; + () + | Ok (Either.Right err) -> + Events.send_error (Err_response err); + () ) + in + Fut.bind (fetch ()) f + +module GET = struct + type _ t = + | Catalog : thread list t + | Thread : int -> Thread_w_reply.t t + | Post : int -> post t + | Admin : report list t + | User : string -> user t + | Session : session t + + let reader : type a. a t -> string -> (a, string) result = + fun t -> + let open Json_data.Read in + match t with + | Catalog -> catalog + | Thread _id -> thread_w_reply + | Post _id -> post + | Admin -> reports + | User _id -> user + | Session -> session + + let url : type a. a t -> string = + fun t -> + Fmt.str "/api%s" + ( match t with + | Catalog -> "/catalog" + | Thread id -> Fmt.str "/thread/%d" id + | Post id -> Fmt.str "/post/%d" id + | Admin -> "/admin" + | User id -> Fmt.str "/user/%s" id + | Session -> "/session" ) + + let on_ok : type a. a t -> a -> unit = + fun req v -> + let open Client_types in + let open Events in + begin + match req with + | Catalog -> send_data_update (Catalog_update v) + | Thread _id -> send_data_update (Thread_update v) + | Post _id -> send_data_update (Post_update v) + | Admin -> send_data_update (Reports_update v) + | User _id -> send_data_update (User_update v) + | Session -> send_data_update (Session_update v) + end; + () + + let fetch t = + let s = url t in + Fmt.pr "fetch `%s`@." s; + let fetch () = Brr_io.Fetch.url (Jstr.v s) in + let _fut = handle_response GET fetch (reader t) (on_ok t) in + () + + let catalog () = fetch Catalog + + let thread id = fetch (Thread id) + + let post id = fetch (Post id) + + let admin () = fetch Admin + + let user id = fetch (User id) + + let session () = fetch Session + + let f page = + let open Page in + match page with + | About | Register | Login -> () + | Account | Profile -> session () + | Home | New_thread -> catalog () + | Admin _ -> admin () + | Thread v -> + let id = unwrap_thread_id v in + thread id; + catalog () + | Delete v | Report v -> + let id = unwrap_post_id v in + post id + | User v -> + let id = unwrap_user_id v in + user id +end + +module POST = struct + open Form_kind + + let reader : type a. a t -> string -> (a, string) result = + fun t -> + let open Json_data.Read in + match t with + | Home -> thread_w_reply + | Register -> session + | Login -> session + | Logout -> session + | Profile -> session + | Account -> session + | Thread _ -> thread_w_reply + | Delete _ -> post + | Report _ -> reports + | Admin_ignore _ -> reports + | Admin_delete _ -> post + | Admin_banish _ -> user + + (* TODO implement redirection mechanism *) + let on_ok : type a. a t -> a -> unit = + fun o v -> + let open Client_types in + let open Events in + begin + match o with + | Home -> + send_data_update (Thread_update v); + send_action (Post_form_change Form_reset); + let id = v.op.id in + Navigation.load (Thread (Loading id)) + | Thread _ -> + (* server respond to successful POST with full thread *) + send_data_update (Thread_update v); + send_action (Post_form_change Form_reset); + let id = v.op.id in + Navigation.load (Thread (Loading id)) + | Register -> + send_data_update (Session_update v); + Navigation.load Profile + | Login -> + send_data_update (Session_update v); + Navigation.load Home + | Logout -> send_data_update (Session_update v) + | Delete _ -> ( + let is_op = Int.equal v.id v.parent_t_id in + match is_op with + | true -> Navigation.load Home + | false -> Navigation.load (Thread (Loading v.parent_t_id)) ) + | Report _ -> + send_data_update (Reports_update v); + (* TODO need redirection to page before report here *) + Navigation.load Home + | Admin_ignore _ -> send_data_update (Reports_update v) + | Admin_delete _ -> () + | Admin_banish _ -> () + | Profile -> send_data_update (Session_update v) + | Account -> send_data_update (Session_update v) + end; + () + + let fetch t request = + let fetch () = Brr_io.Fetch.request request in + handle_response POST fetch (reader t) (on_ok t) + + let f kind form_el csrf_token = + let open Brr_io in + let method' = Jstr.v "POST" in + let form = Form.of_el form_el in + let action = Form_kind.action kind |> Jstr.v in + let form_data = Form.Data.of_form form in + Form.Data.set form_data (Jstr.v "dream.csrf") (Jstr.v csrf_token); + let body = Fetch.Body.of_form_data form_data in + let init = Fetch.Request.init ~method' ~body () in + let request = Fetch.Request.v ~init action in + let fut = fetch kind request in + let _fut : unit Fut.t = + Fut.map (fun () -> Fmt.pr "`%s` xhr done@." (Form_kind.name kind)) fut + in + () +end diff --git a/src/client/page.ml b/src/client/page.ml new file mode 100644 index 0000000..04f0f9f --- /dev/null +++ b/src/client/page.ml @@ -0,0 +1,193 @@ +open Types + +module Kind = struct + type t = + | Home + | New_thread + | Thread + | About + | Register + | Login + | Admin + | Profile + | Account + | User + | Delete + | Report + + let equal : t -> t -> bool = fun a b -> Obj.magic a = Obj.magic b + + let to_string = function + | Home -> "home" + | New_thread -> "new-thread" + | Thread -> "thread" + | About -> "about" + | Register -> "register" + | Login -> "login" + | Admin -> "administration" + | Profile -> "profile" + | Account -> "account" + | User -> "user" + | Delete -> "delete" + | Report -> "report" + + let of_string s = + match s with + | "" | "home" -> Some Home + | "new-thread" -> Some New_thread + | "thread" -> Some Thread + | "about" -> Some About + | "register" -> Some Register + | "login" -> Some Login + | "administration" -> Some Admin + | "profile" -> Some Profile + | "account" -> Some Account + | "user" -> Some User + | "delete" -> Some Delete + | "report" -> Some Report + | _ -> None + + let to_emoji = function + | Home -> Some "🗺️" + | About -> Some "🛸" + | Register -> Some "🍎" + | Login -> Some "🚪" + | Admin -> Some "🪄" + | Profile -> Some "🦩" + | Account -> Some "⚙" + | New_thread | Thread | User | Delete | Report -> None +end + +type ('a, 'b) wrap = + | Loading of 'a + | Not_found of 'a + | Ready of 'b + +type t = + | Home + | New_thread + | Thread of (int, Thread_w_reply.t) wrap + | About + | Register + | Login + | Admin of (unit, report list) wrap + | Profile + | Account + | User of (user_id, user) wrap + | Delete of (post_id, post) wrap + | Report of (post_id, post) wrap + +let is_ready = function + | Home | New_thread | About | Register | Login | Profile | Account -> true + | Thread (Ready _) + | Admin (Ready _) + | User (Ready _) + | Delete (Ready _) + | Report (Ready _) -> + true + | _ -> false + +let unwrap_thread_id = function + | Loading v | Not_found v -> v + | Ready v -> v.Thread_w_reply.op.id + +let unwrap_post_id = function Loading v | Not_found v -> v | Ready v -> v.id + +let unwrap_user_id = function + | Loading v | Not_found v -> v + | Ready v -> v.user_id + +let to_kind = function + | Home -> Kind.Home + | New_thread -> New_thread + | Thread _ -> Thread + | About -> About + | Register -> Register + | Login -> Login + | Admin _ -> Admin + | Profile -> Profile + | Account -> Account + | User _ -> User + | Delete _ -> Delete + | Report _ -> Report + +(* TODO handle failure *) +let of_uri = + let admin () = Admin (Loading ()) in + let user id = User (Loading id) in + let thread id = Thread (Loading id) in + let delete id = Delete (Loading id) in + let report id = Report (Loading id) in + let bind_int opt f = Option.bind opt int_of_string_opt |> Option.map f in + let of_kind ~item_id k = + match k with + | Kind.Home -> Some Home + | New_thread -> Some New_thread + | About -> Some About + | Register -> Some Register + | Login -> Some Login + | Profile -> Some Profile + | Account -> Some Account + | Admin -> Some (admin ()) + | User -> item_id |> Option.map user + | Thread -> bind_int item_id thread + | Delete -> bind_int item_id delete + | Report -> bind_int item_id report + in + fun uri -> + let open Brr in + let segment_1, segment_2 = + let segments = + match Uri.path_segments uri with + | Error e -> + Fmt.failwith "Page.of_uri failure: path_segments error `%s`" + (Util.str_of_error e) + | Ok l -> List.map Jstr.to_string l + in + match segments with + | [] -> ("home", None) + | x :: [] -> (x, None) + | [ x; y ] -> (x, Some y) + | _ -> Fmt.failwith "Page.of_uri failure: invalid path segments" + in + match + Option.bind (Kind.of_string segment_1) (of_kind ~item_id:segment_2) + with + | None -> Fmt.failwith "Page.of_uri failure: invalid path format" + | Some page -> + let fragment_opt = uri |> Uri.fragment |> Jstr.to_string in + (page, fragment_opt) + +let to_path o = + let page_name = to_kind o |> Kind.to_string in + let param = + match o with + | Home | New_thread | About | Register | Login | Profile | Account -> None + | Admin _ -> None + | User v -> + let id = unwrap_user_id v in + Some id + | Thread v -> + let id = unwrap_thread_id v in + Some (string_of_int id) + | Delete v | Report v -> + let id = unwrap_post_id v in + Some (string_of_int id) + in + match param with + | None -> Fmt.str "/%s" page_name + | Some s -> Fmt.str "/%s/%s" page_name s + +let to_uri o = + let open Brr in + let uri = + (* clear query and fragment of the current uri *) + let empty_params = Uri.Params.of_jstr (Jstr.v "") in + let uri = Window.location G.window in + let uri = Uri.with_query_params uri empty_params in + Uri.with_fragment_params uri empty_params + in + let path = Jstr.v (to_path o) in + match Uri.with_uri ~path uri with + | Error e -> Fmt.failwith "%s" (Jv.of_error e |> Jv.to_string) + | Ok uri -> uri diff --git a/src/client/storage.ml b/src/client/storage.ml new file mode 100644 index 0000000..381f629 --- /dev/null +++ b/src/client/storage.ml @@ -0,0 +1,43 @@ +module Local = struct + open Brr + open Brr_io + + let local = Storage.local G.window + + let set k v = + match Storage.set_item local (Jstr.v k) (Jstr.v v) with + | (exception Jv.Error e) | Error e -> + Fmt.failwith "local storage failure `%s`" (Util.str_of_error e) + | Ok () -> () + + let get k = Storage.get_item local (Jstr.v k) |> Option.map Jstr.to_string + + let clear () = Storage.clear local +end + +let init_map_view () = + let default_map_view = (51.505, -0.09, 13) in + let lat = Local.get "lat" in + let lng = Local.get "lng" in + let zoom = Local.get "zoom" in + match (lat, lng, zoom) with + | Some lat, Some lng, Some zoom -> + let lat = lat |> Jstr.v |> Jstr.to_float in + let lng = lng |> Jstr.v |> Jstr.to_float in + let zoom = + match int_of_string_opt zoom with + | None -> Fmt.failwith "init_map_view: int_of_string failure on zoom" + | Some zoom -> zoom + in + (lat, lng, zoom) + | _ -> default_map_view + +let set_map_view (lat, lng, zoom) = + Local.set "lat" (string_of_float lat); + Local.set "lng" (string_of_float lng); + Local.set "zoom" (string_of_int zoom); + () + +let clear () = + Local.clear (); + () diff --git a/src/client/util.ml b/src/client/util.ml new file mode 100644 index 0000000..252208d --- /dev/null +++ b/src/client/util.ml @@ -0,0 +1,89 @@ +open Brr + +let str = Jstr.v + +let str_of_error e = Jv.of_error e |> Jv.to_string + +(* redefine At module? *) +let class' j = At.class' (str j) + +let id j = At.id (str j) + +let href j = At.href (str j) + +let src j = At.src (str j) + +let alt j = At.v (str "alt") (str j) + +let title j = At.title (str j) + +let type' j = At.type' (str j) + +let name j = At.name (str j) + +let value j = At.value (str j) + +let mk_at k v = At.v (str k) (str v) + +let el_txt s = El.txt (str s) + +let h1 s = El.h1 [ el_txt s ] + +let h2 s = El.h2 [ el_txt s ] + +let window = G.window + +let window_as_target = Window.as_target window + +let window_jv = Jv.get Jv.global "window" + +let window_width () = Jv.get window_jv "innerWidth" |> Jv.to_float + +let window_height () = Jv.get window_jv "innerHeight" |> Jv.to_float + +let document = G.document + +let document_as_target = Document.as_target document + +let body = Document.body document + +let find_html_el_by_id id = + Document.find_el_by_id G.document (Jstr.of_string id) + +let get_bounds el = + let x = El.bound_x el in + let y = El.bound_y el in + let w = El.bound_w el in + let h = El.bound_h el in + (x, y, w, h) + +let clamp ~min ~max x = Float.max (Float.min max x) min + +(* -- Note util -- *) +open Note +open Note_brr + +let def_off b_s el = Elr.def_class (str "off") b_s el + +let def_on b_s el = Elr.def_class (str "off") (S.map not b_s) el + +let def_disabled b_s el = + Elr.def_at At.Name.disabled + (S.map (function true -> Some (Jstr.v "") | false -> None) b_s) + el + +let hold_on el ev_type f = + let event = Evr.on_el ev_type Fun.id el in + Elr.may_hold_logr el (E.log event f); + () + +let hold_event_on el event f = + Elr.may_hold_logr el (E.log event f); + () + +let hold_endless f e = Logr.may_hold (E.log e f) + +let hold_endless_on_window ev_type f = + let event = Evr.on_target ev_type Fun.id window_as_target in + hold_endless f event; + () diff --git a/src/comment/ast.ml b/src/comment/ast.ml new file mode 100644 index 0000000..6a24a70 --- /dev/null +++ b/src/comment/ast.ml @@ -0,0 +1,9 @@ +type item = + | Id of int + | Txt of string + +type line = + | Line of item list + | Line_quote of item list + +type t = line list diff --git a/src/comment/comment.ml b/src/comment/comment.ml new file mode 100644 index 0000000..ebff440 --- /dev/null +++ b/src/comment/comment.ml @@ -0,0 +1,85 @@ +include Ast + +let backlinks comment = + comment + |> List.map (function Line l -> l | Line_quote l -> l) + |> List.flatten + |> List.filter_map (function Id id -> Some id | Txt _s -> None) + |> List.sort_uniq Int.compare + +let of_string = + (* merge adjacent Txt together *) + let merge_txt_aux item_l = + let rec loop l acc txt_acc = + match l with + | [] -> ( + match txt_acc with + | [] -> List.rev acc + | _ -> + let txt = Txt (String.concat "" (List.rev txt_acc)) in + List.rev (txt :: acc) ) + | Txt s :: tl -> loop tl acc (s :: txt_acc) + | hd :: tl -> ( + match txt_acc with + | [] -> loop tl (hd :: acc) [] + | _ -> + let txt = Txt (String.concat "" (List.rev txt_acc)) in + loop tl (hd :: txt :: acc) [] ) + in + let l = loop item_l [] [] in + l + in + let merge_txt = + fun line_l -> + List.map + (function + | Line l -> Line (merge_txt_aux l) + | Line_quote l -> Line_quote (merge_txt_aux l) ) + line_l + in + (* for debug and error msg *) + let token_to_string = function + | Parser.EOF -> "eof" + | NEWLINE -> "newline" + | GT -> ">" + | ID i -> Fmt.str "id: `%d`" i + | TXT s -> Fmt.str "text: `%s`" s + in + (* parser *) + let from_lexbuf = + let parser = + MenhirLib.Convert.Simplified.traditional2revised Parser.comment + in + fun buf -> + let provider () = + let tok = Lexer.token buf in + let start, stop = Sedlexing.lexing_positions buf in + (tok, start, stop) + in + try Ok (parser provider) with + | Lexer.Lexing_error e -> Error e + | Sedlexing.MalFormed -> Error (Fmt.str "malformed utf8 encoding") + | Parser.Error -> + let tok = Lexer.token buf |> token_to_string in + Error (Fmt.str "unexpected token `%s`" tok) + in + fun s -> from_lexbuf (Sedlexing.Utf8.from_string s) |> Result.map merge_txt + +let to_string = + let pp_item fmt = function + | Txt s -> Fmt.pf fmt "%s" s + | Id i -> Fmt.pf fmt ">>%d" i + in + let pp_line = + let sep = Fmt.any "" in + fun fmt line -> + match line with + | Line items -> Fmt.pf fmt "%a" (Fmt.list ~sep pp_item) items + | Line_quote items -> Fmt.pf fmt ">%a" (Fmt.list ~sep pp_item) items + in + let pp fmt line_l = + Fmt.pf fmt "%a" (Fmt.list ~sep:(Fmt.any "@\n") pp_line) line_l + in + fun comment -> + let s = Fmt.str "%a" pp comment in + s diff --git a/src/comment/dune b/src/comment/dune new file mode 100644 index 0000000..b7cd020 --- /dev/null +++ b/src/comment/dune @@ -0,0 +1,9 @@ +(menhir + (modules parser)) + +(library + (name comment) + (modules comment lexer parser ast) + (libraries menhirLib sedlex fmt) + (preprocess + (pps sedlex.ppx))) diff --git a/src/comment/lexer.ml b/src/comment/lexer.ml new file mode 100644 index 0000000..a09347a --- /dev/null +++ b/src/comment/lexer.ml @@ -0,0 +1,30 @@ +open Sedlexing +open Parser + +exception Lexing_error of string + +let newline = [%sedlex.regexp? '\n' | "\r\n"] + +let gt = [%sedlex.regexp? '>'] + +let id = [%sedlex.regexp? ">>", Plus '0' .. '9'] + +let txt = [%sedlex.regexp? Plus (Compl ('>' | '\n'))] + +let token lexbuf = + match%sedlex lexbuf with + | eof -> EOF + | newline -> NEWLINE + | id -> + let lexeme = Utf8.lexeme lexbuf in + let id = + ID (int_of_string (String.sub lexeme 2 (String.length lexeme - 2))) + in + id + | gt -> GT + | txt -> + let lexeme = Utf8.lexeme lexbuf in + TXT lexeme + | _ -> raise (Lexing_error ("Unexpected character: " ^ Utf8.lexeme lexbuf)) + +let lexer lexbuf = Sedlexing.with_tokenizer token lexbuf diff --git a/src/comment/parser.mly b/src/comment/parser.mly new file mode 100644 index 0000000..79b5411 --- /dev/null +++ b/src/comment/parser.mly @@ -0,0 +1,30 @@ +%{ + open Ast +%} +%token EOF +%token NEWLINE +%token ID +%token TXT +%token GT + +%start comment +%type comment + +%% + +comment: + | separated_nonempty_list(NEWLINE, line) EOF { $1 } + +line: + | {Line ([]) } + | ID items {Line ( (Id $1) :: $2) } + | TXT items {Line ( (Txt $1) :: $2) } + | GT items {Line_quote $2 } + +items: + | list(item) { $1 } + +item: + | ID { Id $1 } + | TXT { Txt $1 } + | GT { Txt ">" } diff --git a/src/content/about.md b/src/content/about.md deleted file mode 100644 index 97159d8..0000000 --- a/src/content/about.md +++ /dev/null @@ -1,23 +0,0 @@ -# What is Permap - -Permap is an open source geo-message-board software written in OCaml. - -Permap was initially made to be a gardening/permaculture forum. -Permap's aim is to help people find friends with similar interests around them -and build local communities. - -You can make threads with geographical coordinate, -this way you can find people near you doing interesting stuffs, -socialize with them and share local knowledge. - -## Permap's future - -- Make permap federate - -- More than coordinates - -Make threads on anything with a geographical position. -Instead of making threads with a simple (latitude * longitude) data, -we want to be able to make threads on any OpenStreetMap's item/ActivityPub object -that can resolve to a geographical position. - diff --git a/src/content/assets/css/bootstrap.min.css b/src/content/assets/css/bootstrap.min.css deleted file mode 100644 index 9a2086c..0000000 --- a/src/content/assets/css/bootstrap.min.css +++ /dev/null @@ -1,6 +0,0 @@ -@charset "UTF-8";/*! - * Bootstrap v5.0.2 (https://getbootstrap.com/) - * Copyright 2011-2021 The Bootstrap Authors - * Copyright 2011-2021 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0))}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-font-sans-serif);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x) * -.5);margin-left:calc(var(--bs-gutter-x) * -.5)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + (.5rem + 2px));padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + (1rem + 2px));padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + (.75rem + 2px))}textarea.form-control-sm{min-height:calc(1.5em + (.5rem + 2px))}textarea.form-control-lg{min-height:calc(1.5em + (1rem + 2px))}.form-control-color{max-width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast:not(.showing):not(.show){opacity:0}.toast.hide{display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1060;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1050;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{color:#0d6efd!important}.text-secondary{color:#6c757d!important}.text-success{color:#198754!important}.text-info{color:#0dcaf0!important}.text-warning{color:#ffc107!important}.text-danger{color:#dc3545!important}.text-light{color:#f8f9fa!important}.text-dark{color:#212529!important}.text-white{color:#fff!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-reset{color:inherit!important}.bg-primary{background-color:#0d6efd!important}.bg-secondary{background-color:#6c757d!important}.bg-success{background-color:#198754!important}.bg-info{background-color:#0dcaf0!important}.bg-warning{background-color:#ffc107!important}.bg-danger{background-color:#dc3545!important}.bg-light{background-color:#f8f9fa!important}.bg-dark{background-color:#212529!important}.bg-body{background-color:#fff!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} diff --git a/src/content/assets/css/style.css b/src/content/assets/css/style.css deleted file mode 100644 index 6cf5540..0000000 --- a/src/content/assets/css/style.css +++ /dev/null @@ -1,169 +0,0 @@ -body { - padding-top: 0rem; - padding-bottom: 3rem; - color: #5a5a5a; - background-color: #e8eaf6; - line-height: 1.6; - font-size: 18px; -} - -.featurette-divider { - margin: 5rem 0; -} - -#page-title { - text-align: center; -} - -blockquote.blockquote { - border-left: 6px solid #3131e0; - border-radius: 6px; - padding-left: 16px; - background-color: #c0c0f0; -} - -#map { - height: 800px; - width: auto; -} - -.post { - background-color: #C5E1A5; - margin: 5px 5px 5px 5px; - border: 2px solid #FFB300; - padding: 2px; - display: table; -} - -.post + .highlight { - background-color: #9dd162; - margin: 5px 5px 5px 5px; - border: 2px solid #FFB300; - padding: 2px; - display: table; -} - -.post + .selected { - background-color: #9dd162; - margin: 5px 5px 5px 5px; - border: 2px solid #FFB300; - padding: 2px; - display: table; -} - -.post-info { - display: block; - width: 100%; -} - -.nick { - color: #FFB300; -} - -.post-comment { - display: block; - padding-top: 10px; - color: #333333; -} - -.post-image-container { - float: left; - padding: 5px 5px 5px 5px; -} - -.post-image { - max-width: 300px; - max-height: 300px; -} - -.post-image-big { - max-width: 1200px; - height: auto; -} - -.quote { - color: green; -} - -.quote-link { - background-color: #FCE4EC; - padding: 2px; - text-align: center; - color: #5a5a5a; - font-size: 10px; - border-radius: 12px; - border: 2px solid DodgerBlue; -} - -.quote-link:focus { - background-color: #FCE4EC; - padding: 2px; - text-align: center; - color: #5a5a5a; - font-size: 10px; - border-radius: 12px; - border: 2px solid DodgerBlue; -} - -.post-form { - background-color: #FCE4EC; - margin: 5px 5px 5px 5px; - border: 2px solid #FFB300; - padding: 2px; - display: table; - width: 500px; -} - -#newthread-form { - visibility: hidden; -} - -a.preview-link { - text-decoration: none; - color: unset; -} - -.thread-subject { - margin: auto; - width: 50%; - text-align: center; - color: #5a5a5a; - font-size: 30px; -} - -.tag { - background-color: #FFB300; - border-radius: 4px; - padding: 2px; -} - -.category { - background-color: #FFB300; - border-radius: 4px; - padding: 2px; - font-weight: bold; - font-size: 20px; -} - -.off { - display: none; -} - -.post-menu-div { - display: inline; -} - -a.post-menu-link { - text-decoration: none; - color: green; -} - -.rss-logo { - height: 30px; - width: auto; - float: right; -} - -#submit-button { - float: right; -} diff --git a/src/content/assets/js/bootstrap.bundle.min.js b/src/content/assets/js/bootstrap.bundle.min.js deleted file mode 100644 index c19bf0a..0000000 --- a/src/content/assets/js/bootstrap.bundle.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Bootstrap v5.0.2 (https://getbootstrap.com/) - * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter(t=>t.matches(e)),parents(t,e){const i=[];let n=t.parentNode;for(;n&&n.nodeType===Node.ELEMENT_NODE&&3!==n.nodeType;)n.matches(e)&&i.push(n),n=n.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]}},e=t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t},i=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i="#"+i.split("#")[1]),e=i&&"#"!==i?i.trim():null}return e},n=t=>{const e=i(t);return e&&document.querySelector(e)?e:null},s=t=>{const e=i(t);return e?document.querySelector(e):null},o=t=>{t.dispatchEvent(new Event("transitionend"))},r=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),a=e=>r(e)?e.jquery?e[0]:e:"string"==typeof e&&e.length>0?t.findOne(e):null,l=(t,e,i)=>{Object.keys(i).forEach(n=>{const s=i[n],o=e[n],a=o&&r(o)?"element":null==(l=o)?""+l:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();var l;if(!new RegExp(s).test(a))throw new TypeError(`${t.toUpperCase()}: Option "${n}" provided type "${a}" but expected type "${s}".`)})},c=t=>!(!r(t)||0===t.getClientRects().length)&&"visible"===getComputedStyle(t).getPropertyValue("visibility"),h=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),d=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?d(t.parentNode):null},u=()=>{},f=t=>t.offsetHeight,p=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},m=[],g=()=>"rtl"===document.documentElement.dir,_=t=>{var e;e=()=>{const e=p();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(m.length||document.addEventListener("DOMContentLoaded",()=>{m.forEach(t=>t())}),m.push(e)):e()},b=t=>{"function"==typeof t&&t()},v=(t,e,i=!0)=>{if(!i)return void b(t);const n=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let s=!1;const r=({target:i})=>{i===e&&(s=!0,e.removeEventListener("transitionend",r),b(t))};e.addEventListener("transitionend",r),setTimeout(()=>{s||o(e)},n)},y=(t,e,i,n)=>{let s=t.indexOf(e);if(-1===s)return t[!i&&n?t.length-1:0];const o=t.length;return s+=i?1:-1,n&&(s=(s+o)%o),t[Math.max(0,Math.min(s,o-1))]},w=/[^.]*(?=\..*)\.|.*/,E=/\..*/,A=/::\d+$/,T={};let O=1;const C={mouseenter:"mouseover",mouseleave:"mouseout"},k=/^(mouseenter|mouseleave)/i,L=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function x(t,e){return e&&`${e}::${O++}`||t.uidEvent||O++}function D(t){const e=x(t);return t.uidEvent=e,T[e]=T[e]||{},T[e]}function S(t,e,i=null){const n=Object.keys(t);for(let s=0,o=n.length;sfunction(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};n?n=t(n):i=t(i)}const[o,r,a]=I(e,i,n),l=D(t),c=l[a]||(l[a]={}),h=S(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=x(r,e.replace(w,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(let a=o.length;a--;)if(o[a]===r)return s.delegateTarget=r,n.oneOff&&P.off(t,s.type,e,i),i.apply(r,[s]);return null}}(t,i,n):function(t,e){return function i(n){return n.delegateTarget=t,i.oneOff&&P.off(t,n.type,e),e.apply(t,[n])}}(t,i);u.delegationSelector=o?i:null,u.originalHandler=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function j(t,e,i,n,s){const o=S(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function M(t){return t=t.replace(E,""),C[t]||t}const P={on(t,e,i,n){N(t,e,i,n,!1)},one(t,e,i,n){N(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=I(e,i,n),a=r!==e,l=D(t),c=e.startsWith(".");if(void 0!==o){if(!l||!l[r])return;return void j(t,l,r,o,s?i:null)}c&&Object.keys(l).forEach(i=>{!function(t,e,i,n){const s=e[i]||{};Object.keys(s).forEach(o=>{if(o.includes(n)){const n=s[o];j(t,e,i,n.originalHandler,n.delegationSelector)}})}(t,l,i,e.slice(1))});const h=l[r]||{};Object.keys(h).forEach(i=>{const n=i.replace(A,"");if(!a||e.includes(n)){const e=h[i];j(t,l,r,e.originalHandler,e.delegationSelector)}})},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=p(),s=M(e),o=e!==s,r=L.has(s);let a,l=!0,c=!0,h=!1,d=null;return o&&n&&(a=n.Event(e,i),n(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),h=a.isDefaultPrevented()),r?(d=document.createEvent("HTMLEvents"),d.initEvent(s,l,!0)):d=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==i&&Object.keys(i).forEach(t=>{Object.defineProperty(d,t,{get:()=>i[t]})}),h&&d.preventDefault(),c&&t.dispatchEvent(d),d.defaultPrevented&&void 0!==a&&a.preventDefault(),d}},H=new Map;var R={set(t,e,i){H.has(t)||H.set(t,new Map);const n=H.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>H.has(t)&&H.get(t).get(e)||null,remove(t,e){if(!H.has(t))return;const i=H.get(t);i.delete(e),0===i.size&&H.delete(t)}};class B{constructor(t){(t=a(t))&&(this._element=t,R.set(this._element,this.constructor.DATA_KEY,this))}dispose(){R.remove(this._element,this.constructor.DATA_KEY),P.off(this._element,this.constructor.EVENT_KEY),Object.getOwnPropertyNames(this).forEach(t=>{this[t]=null})}_queueCallback(t,e,i=!0){v(t,e,i)}static getInstance(t){return R.get(t,this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.0.2"}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}static get DATA_KEY(){return"bs."+this.NAME}static get EVENT_KEY(){return"."+this.DATA_KEY}}class W extends B{static get NAME(){return"alert"}close(t){const e=t?this._getRootElement(t):this._element,i=this._triggerCloseEvent(e);null===i||i.defaultPrevented||this._removeElement(e)}_getRootElement(t){return s(t)||t.closest(".alert")}_triggerCloseEvent(t){return P.trigger(t,"close.bs.alert")}_removeElement(t){t.classList.remove("show");const e=t.classList.contains("fade");this._queueCallback(()=>this._destroyElement(t),t,e)}_destroyElement(t){t.remove(),P.trigger(t,"closed.bs.alert")}static jQueryInterface(t){return this.each((function(){const e=W.getOrCreateInstance(this);"close"===t&&e[t](this)}))}static handleDismiss(t){return function(e){e&&e.preventDefault(),t.close(this)}}}P.on(document,"click.bs.alert.data-api",'[data-bs-dismiss="alert"]',W.handleDismiss(new W)),_(W);class q extends B{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=q.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}function z(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function $(t){return t.replace(/[A-Z]/g,t=>"-"+t.toLowerCase())}P.on(document,"click.bs.button.data-api",'[data-bs-toggle="button"]',t=>{t.preventDefault();const e=t.target.closest('[data-bs-toggle="button"]');q.getOrCreateInstance(e).toggle()}),_(q);const U={setDataAttribute(t,e,i){t.setAttribute("data-bs-"+$(e),i)},removeDataAttribute(t,e){t.removeAttribute("data-bs-"+$(e))},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter(t=>t.startsWith("bs")).forEach(i=>{let n=i.replace(/^bs/,"");n=n.charAt(0).toLowerCase()+n.slice(1,n.length),e[n]=z(t.dataset[i])}),e},getDataAttribute:(t,e)=>z(t.getAttribute("data-bs-"+$(e))),offset(t){const e=t.getBoundingClientRect();return{top:e.top+document.body.scrollTop,left:e.left+document.body.scrollLeft}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},F={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},V={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},K="next",X="prev",Y="left",Q="right",G={ArrowLeft:Q,ArrowRight:Y};class Z extends B{constructor(e,i){super(e),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(i),this._indicatorsElement=t.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return F}static get NAME(){return"carousel"}next(){this._slide(K)}nextWhenVisible(){!document.hidden&&c(this._element)&&this.next()}prev(){this._slide(X)}pause(e){e||(this._isPaused=!0),t.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(o(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(e){this._activeElement=t.findOne(".active.carousel-item",this._element);const i=this._getItemIndex(this._activeElement);if(e>this._items.length-1||e<0)return;if(this._isSliding)return void P.one(this._element,"slid.bs.carousel",()=>this.to(e));if(i===e)return this.pause(),void this.cycle();const n=e>i?K:X;this._slide(n,this._items[e])}_getConfig(t){return t={...F,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},l("carousel",t,V),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?Q:Y)}_addEventListeners(){this._config.keyboard&&P.on(this._element,"keydown.bs.carousel",t=>this._keydown(t)),"hover"===this._config.pause&&(P.on(this._element,"mouseenter.bs.carousel",t=>this.pause(t)),P.on(this._element,"mouseleave.bs.carousel",t=>this.cycle(t))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const e=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType?this._pointerEvent||(this.touchStartX=t.touches[0].clientX):this.touchStartX=t.clientX},i=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},n=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType||(this.touchDeltaX=t.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(t=>this.cycle(t),500+this._config.interval))};t.find(".carousel-item img",this._element).forEach(t=>{P.on(t,"dragstart.bs.carousel",t=>t.preventDefault())}),this._pointerEvent?(P.on(this._element,"pointerdown.bs.carousel",t=>e(t)),P.on(this._element,"pointerup.bs.carousel",t=>n(t)),this._element.classList.add("pointer-event")):(P.on(this._element,"touchstart.bs.carousel",t=>e(t)),P.on(this._element,"touchmove.bs.carousel",t=>i(t)),P.on(this._element,"touchend.bs.carousel",t=>n(t)))}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=G[t.key];e&&(t.preventDefault(),this._slide(e))}_getItemIndex(e){return this._items=e&&e.parentNode?t.find(".carousel-item",e.parentNode):[],this._items.indexOf(e)}_getItemByOrder(t,e){const i=t===K;return y(this._items,e,i,this._config.wrap)}_triggerSlideEvent(e,i){const n=this._getItemIndex(e),s=this._getItemIndex(t.findOne(".active.carousel-item",this._element));return P.trigger(this._element,"slide.bs.carousel",{relatedTarget:e,direction:i,from:s,to:n})}_setActiveIndicatorElement(e){if(this._indicatorsElement){const i=t.findOne(".active",this._indicatorsElement);i.classList.remove("active"),i.removeAttribute("aria-current");const n=t.find("[data-bs-target]",this._indicatorsElement);for(let t=0;t{P.trigger(this._element,"slid.bs.carousel",{relatedTarget:r,direction:u,from:o,to:a})};if(this._element.classList.contains("slide")){r.classList.add(d),f(r),s.classList.add(h),r.classList.add(h);const t=()=>{r.classList.remove(h,d),r.classList.add("active"),s.classList.remove("active",d,h),this._isSliding=!1,setTimeout(p,0)};this._queueCallback(t,s,!0)}else s.classList.remove("active"),r.classList.add("active"),this._isSliding=!1,p();l&&this.cycle()}_directionToOrder(t){return[Q,Y].includes(t)?g()?t===Y?X:K:t===Y?K:X:t}_orderToDirection(t){return[K,X].includes(t)?g()?t===X?Y:Q:t===X?Q:Y:t}static carouselInterface(t,e){const i=Z.getOrCreateInstance(t,e);let{_config:n}=i;"object"==typeof e&&(n={...n,...e});const s="string"==typeof e?e:n.slide;if("number"==typeof e)i.to(e);else if("string"==typeof s){if(void 0===i[s])throw new TypeError(`No method named "${s}"`);i[s]()}else n.interval&&n.ride&&(i.pause(),i.cycle())}static jQueryInterface(t){return this.each((function(){Z.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=s(this);if(!e||!e.classList.contains("carousel"))return;const i={...U.getDataAttributes(e),...U.getDataAttributes(this)},n=this.getAttribute("data-bs-slide-to");n&&(i.interval=!1),Z.carouselInterface(e,i),n&&Z.getInstance(e).to(n),t.preventDefault()}}P.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",Z.dataApiClickHandler),P.on(window,"load.bs.carousel.data-api",()=>{const e=t.find('[data-bs-ride="carousel"]');for(let t=0,i=e.length;tt===this._element);null!==o&&r.length&&(this._selector=o,this._triggerArray.push(i))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}static get Default(){return J}static get NAME(){return"collapse"}toggle(){this._element.classList.contains("show")?this.hide():this.show()}show(){if(this._isTransitioning||this._element.classList.contains("show"))return;let e,i;this._parent&&(e=t.find(".show, .collapsing",this._parent).filter(t=>"string"==typeof this._config.parent?t.getAttribute("data-bs-parent")===this._config.parent:t.classList.contains("collapse")),0===e.length&&(e=null));const n=t.findOne(this._selector);if(e){const t=e.find(t=>n!==t);if(i=t?et.getInstance(t):null,i&&i._isTransitioning)return}if(P.trigger(this._element,"show.bs.collapse").defaultPrevented)return;e&&e.forEach(t=>{n!==t&&et.collapseInterface(t,"hide"),i||R.set(t,"bs.collapse",null)});const s=this._getDimension();this._element.classList.remove("collapse"),this._element.classList.add("collapsing"),this._element.style[s]=0,this._triggerArray.length&&this._triggerArray.forEach(t=>{t.classList.remove("collapsed"),t.setAttribute("aria-expanded",!0)}),this.setTransitioning(!0);const o="scroll"+(s[0].toUpperCase()+s.slice(1));this._queueCallback(()=>{this._element.classList.remove("collapsing"),this._element.classList.add("collapse","show"),this._element.style[s]="",this.setTransitioning(!1),P.trigger(this._element,"shown.bs.collapse")},this._element,!0),this._element.style[s]=this._element[o]+"px"}hide(){if(this._isTransitioning||!this._element.classList.contains("show"))return;if(P.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=this._element.getBoundingClientRect()[t]+"px",f(this._element),this._element.classList.add("collapsing"),this._element.classList.remove("collapse","show");const e=this._triggerArray.length;if(e>0)for(let t=0;t{this.setTransitioning(!1),this._element.classList.remove("collapsing"),this._element.classList.add("collapse"),P.trigger(this._element,"hidden.bs.collapse")},this._element,!0)}setTransitioning(t){this._isTransitioning=t}_getConfig(t){return(t={...J,...t}).toggle=Boolean(t.toggle),l("collapse",t,tt),t}_getDimension(){return this._element.classList.contains("width")?"width":"height"}_getParent(){let{parent:e}=this._config;e=a(e);const i=`[data-bs-toggle="collapse"][data-bs-parent="${e}"]`;return t.find(i,e).forEach(t=>{const e=s(t);this._addAriaAndCollapsedClass(e,[t])}),e}_addAriaAndCollapsedClass(t,e){if(!t||!e.length)return;const i=t.classList.contains("show");e.forEach(t=>{i?t.classList.remove("collapsed"):t.classList.add("collapsed"),t.setAttribute("aria-expanded",i)})}static collapseInterface(t,e){let i=et.getInstance(t);const n={...J,...U.getDataAttributes(t),..."object"==typeof e&&e?e:{}};if(!i&&n.toggle&&"string"==typeof e&&/show|hide/.test(e)&&(n.toggle=!1),i||(i=new et(t,n)),"string"==typeof e){if(void 0===i[e])throw new TypeError(`No method named "${e}"`);i[e]()}}static jQueryInterface(t){return this.each((function(){et.collapseInterface(this,t)}))}}P.on(document,"click.bs.collapse.data-api",'[data-bs-toggle="collapse"]',(function(e){("A"===e.target.tagName||e.delegateTarget&&"A"===e.delegateTarget.tagName)&&e.preventDefault();const i=U.getDataAttributes(this),s=n(this);t.find(s).forEach(t=>{const e=et.getInstance(t);let n;e?(null===e._parent&&"string"==typeof i.parent&&(e._config.parent=i.parent,e._parent=e._getParent()),n="toggle"):n=i,et.collapseInterface(t,n)})})),_(et);var it="top",nt="bottom",st="right",ot="left",rt=[it,nt,st,ot],at=rt.reduce((function(t,e){return t.concat([e+"-start",e+"-end"])}),[]),lt=[].concat(rt,["auto"]).reduce((function(t,e){return t.concat([e,e+"-start",e+"-end"])}),[]),ct=["beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite"];function ht(t){return t?(t.nodeName||"").toLowerCase():null}function dt(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function ut(t){return t instanceof dt(t).Element||t instanceof Element}function ft(t){return t instanceof dt(t).HTMLElement||t instanceof HTMLElement}function pt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof dt(t).ShadowRoot||t instanceof ShadowRoot)}var mt={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];ft(s)&&ht(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});ft(n)&&ht(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function gt(t){return t.split("-")[0]}function _t(t){var e=t.getBoundingClientRect();return{width:e.width,height:e.height,top:e.top,right:e.right,bottom:e.bottom,left:e.left,x:e.left,y:e.top}}function bt(t){var e=_t(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function vt(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&pt(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function yt(t){return dt(t).getComputedStyle(t)}function wt(t){return["table","td","th"].indexOf(ht(t))>=0}function Et(t){return((ut(t)?t.ownerDocument:t.document)||window.document).documentElement}function At(t){return"html"===ht(t)?t:t.assignedSlot||t.parentNode||(pt(t)?t.host:null)||Et(t)}function Tt(t){return ft(t)&&"fixed"!==yt(t).position?t.offsetParent:null}function Ot(t){for(var e=dt(t),i=Tt(t);i&&wt(i)&&"static"===yt(i).position;)i=Tt(i);return i&&("html"===ht(i)||"body"===ht(i)&&"static"===yt(i).position)?e:i||function(t){var e=-1!==navigator.userAgent.toLowerCase().indexOf("firefox");if(-1!==navigator.userAgent.indexOf("Trident")&&ft(t)&&"fixed"===yt(t).position)return null;for(var i=At(t);ft(i)&&["html","body"].indexOf(ht(i))<0;){var n=yt(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Ct(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}var kt=Math.max,Lt=Math.min,xt=Math.round;function Dt(t,e,i){return kt(t,Lt(e,i))}function St(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function It(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}var Nt={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=gt(i.placement),l=Ct(a),c=[ot,st].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return St("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:It(t,rt))}(s.padding,i),d=bt(o),u="y"===l?it:ot,f="y"===l?nt:st,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=Ot(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,E=Dt(v,w,y),A=l;i.modifiersData[n]=((e={})[A]=E,e.centerOffset=E-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&vt(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]},jt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function Mt(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.offsets,r=t.position,a=t.gpuAcceleration,l=t.adaptive,c=t.roundOffsets,h=!0===c?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:xt(xt(e*n)/n)||0,y:xt(xt(i*n)/n)||0}}(o):"function"==typeof c?c(o):o,d=h.x,u=void 0===d?0:d,f=h.y,p=void 0===f?0:f,m=o.hasOwnProperty("x"),g=o.hasOwnProperty("y"),_=ot,b=it,v=window;if(l){var y=Ot(i),w="clientHeight",E="clientWidth";y===dt(i)&&"static"!==yt(y=Et(i)).position&&(w="scrollHeight",E="scrollWidth"),y=y,s===it&&(b=nt,p-=y[w]-n.height,p*=a?1:-1),s===ot&&(_=st,u-=y[E]-n.width,u*=a?1:-1)}var A,T=Object.assign({position:r},l&&jt);return a?Object.assign({},T,((A={})[b]=g?"0":"",A[_]=m?"0":"",A.transform=(v.devicePixelRatio||1)<2?"translate("+u+"px, "+p+"px)":"translate3d("+u+"px, "+p+"px, 0)",A)):Object.assign({},T,((e={})[b]=g?p+"px":"",e[_]=m?u+"px":"",e.transform="",e))}var Pt={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:gt(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,Mt(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,Mt(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}},Ht={passive:!0},Rt={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=dt(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,Ht)})),a&&l.addEventListener("resize",i.update,Ht),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,Ht)})),a&&l.removeEventListener("resize",i.update,Ht)}},data:{}},Bt={left:"right",right:"left",bottom:"top",top:"bottom"};function Wt(t){return t.replace(/left|right|bottom|top/g,(function(t){return Bt[t]}))}var qt={start:"end",end:"start"};function zt(t){return t.replace(/start|end/g,(function(t){return qt[t]}))}function $t(t){var e=dt(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ut(t){return _t(Et(t)).left+$t(t).scrollLeft}function Ft(t){var e=yt(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Vt(t,e){var i;void 0===e&&(e=[]);var n=function t(e){return["html","body","#document"].indexOf(ht(e))>=0?e.ownerDocument.body:ft(e)&&Ft(e)?e:t(At(e))}(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=dt(n),r=s?[o].concat(o.visualViewport||[],Ft(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Vt(At(r)))}function Kt(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function Xt(t,e){return"viewport"===e?Kt(function(t){var e=dt(t),i=Et(t),n=e.visualViewport,s=i.clientWidth,o=i.clientHeight,r=0,a=0;return n&&(s=n.width,o=n.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(r=n.offsetLeft,a=n.offsetTop)),{width:s,height:o,x:r+Ut(t),y:a}}(t)):ft(e)?function(t){var e=_t(t);return e.top=e.top+t.clientTop,e.left=e.left+t.clientLeft,e.bottom=e.top+t.clientHeight,e.right=e.left+t.clientWidth,e.width=t.clientWidth,e.height=t.clientHeight,e.x=e.left,e.y=e.top,e}(e):Kt(function(t){var e,i=Et(t),n=$t(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=kt(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=kt(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ut(t),l=-n.scrollTop;return"rtl"===yt(s||i).direction&&(a+=kt(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Et(t)))}function Yt(t){return t.split("-")[1]}function Qt(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?gt(s):null,r=s?Yt(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case it:e={x:a,y:i.y-n.height};break;case nt:e={x:a,y:i.y+i.height};break;case st:e={x:i.x+i.width,y:l};break;case ot:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ct(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case"start":e[c]=e[c]-(i[h]/2-n[h]/2);break;case"end":e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function Gt(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.boundary,r=void 0===o?"clippingParents":o,a=i.rootBoundary,l=void 0===a?"viewport":a,c=i.elementContext,h=void 0===c?"popper":c,d=i.altBoundary,u=void 0!==d&&d,f=i.padding,p=void 0===f?0:f,m=St("number"!=typeof p?p:It(p,rt)),g="popper"===h?"reference":"popper",_=t.elements.reference,b=t.rects.popper,v=t.elements[u?g:h],y=function(t,e,i){var n="clippingParents"===e?function(t){var e=Vt(At(t)),i=["absolute","fixed"].indexOf(yt(t).position)>=0&&ft(t)?Ot(t):t;return ut(i)?e.filter((function(t){return ut(t)&&vt(t,i)&&"body"!==ht(t)})):[]}(t):[].concat(e),s=[].concat(n,[i]),o=s[0],r=s.reduce((function(e,i){var n=Xt(t,i);return e.top=kt(n.top,e.top),e.right=Lt(n.right,e.right),e.bottom=Lt(n.bottom,e.bottom),e.left=kt(n.left,e.left),e}),Xt(t,o));return r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}(ut(v)?v:v.contextElement||Et(t.elements.popper),r,l),w=_t(_),E=Qt({reference:w,element:b,strategy:"absolute",placement:s}),A=Kt(Object.assign({},b,E)),T="popper"===h?A:w,O={top:y.top-T.top+m.top,bottom:T.bottom-y.bottom+m.bottom,left:y.left-T.left+m.left,right:T.right-y.right+m.right},C=t.modifiersData.offset;if("popper"===h&&C){var k=C[s];Object.keys(O).forEach((function(t){var e=[st,nt].indexOf(t)>=0?1:-1,i=[it,nt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function Zt(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?lt:l,h=Yt(n),d=h?a?at:at.filter((function(t){return Yt(t)===h})):rt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=Gt(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[gt(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}var Jt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=gt(g),b=l||(_!==g&&p?function(t){if("auto"===gt(t))return[];var e=Wt(t);return[zt(t),e,zt(e)]}(g):[Wt(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat("auto"===gt(i)?Zt(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,E=new Map,A=!0,T=v[0],O=0;O=0,D=x?"width":"height",S=Gt(e,{placement:C,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),I=x?L?st:ot:L?nt:it;y[D]>w[D]&&(I=Wt(I));var N=Wt(I),j=[];if(o&&j.push(S[k]<=0),a&&j.push(S[I]<=0,S[N]<=0),j.every((function(t){return t}))){T=C,A=!1;break}E.set(C,j)}if(A)for(var M=function(t){var e=v.find((function(e){var i=E.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},P=p?3:1;P>0&&"break"!==M(P);P--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function te(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ee(t){return[it,st,nt,ot].some((function(e){return t[e]>=0}))}var ie={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=Gt(e,{elementContext:"reference"}),a=Gt(e,{altBoundary:!0}),l=te(r,n),c=te(a,s,o),h=ee(l),d=ee(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},ne={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=lt.reduce((function(t,i){return t[i]=function(t,e,i){var n=gt(t),s=[ot,it].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[ot,st].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},se={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=Qt({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},oe={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=Gt(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=gt(e.placement),b=Yt(e.placement),v=!b,y=Ct(_),w="x"===y?"y":"x",E=e.modifiersData.popperOffsets,A=e.rects.reference,T=e.rects.popper,O="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,C={x:0,y:0};if(E){if(o||a){var k="y"===y?it:ot,L="y"===y?nt:st,x="y"===y?"height":"width",D=E[y],S=E[y]+g[k],I=E[y]-g[L],N=f?-T[x]/2:0,j="start"===b?A[x]:T[x],M="start"===b?-T[x]:-A[x],P=e.elements.arrow,H=f&&P?bt(P):{width:0,height:0},R=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},B=R[k],W=R[L],q=Dt(0,A[x],H[x]),z=v?A[x]/2-N-q-B-O:j-q-B-O,$=v?-A[x]/2+N+q+W+O:M+q+W+O,U=e.elements.arrow&&Ot(e.elements.arrow),F=U?"y"===y?U.clientTop||0:U.clientLeft||0:0,V=e.modifiersData.offset?e.modifiersData.offset[e.placement][y]:0,K=E[y]+z-V-F,X=E[y]+$-V;if(o){var Y=Dt(f?Lt(S,K):S,D,f?kt(I,X):I);E[y]=Y,C[y]=Y-D}if(a){var Q="x"===y?it:ot,G="x"===y?nt:st,Z=E[w],J=Z+g[Q],tt=Z-g[G],et=Dt(f?Lt(J,K):J,Z,f?kt(tt,X):tt);E[w]=et,C[w]=et-Z}}e.modifiersData[n]=C}},requiresIfExists:["offset"]};function re(t,e,i){void 0===i&&(i=!1);var n,s,o=Et(e),r=_t(t),a=ft(e),l={scrollLeft:0,scrollTop:0},c={x:0,y:0};return(a||!a&&!i)&&(("body"!==ht(e)||Ft(o))&&(l=(n=e)!==dt(n)&&ft(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:$t(n)),ft(e)?((c=_t(e)).x+=e.clientLeft,c.y+=e.clientTop):o&&(c.x=Ut(o))),{x:r.left+l.scrollLeft-c.x,y:r.top+l.scrollTop-c.y,width:r.width,height:r.height}}var ae={placement:"bottom",modifiers:[],strategy:"absolute"};function le(){for(var t=arguments.length,e=new Array(t),i=0;i"applyStyles"===t.name&&!1===t.enabled);this._popper=ue(e,this._menu,i),n&&U.setDataAttribute(this._menu,"popper","static")}"ontouchstart"in document.documentElement&&!t.closest(".navbar-nav")&&[].concat(...document.body.children).forEach(t=>P.on(t,"mouseover",u)),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.toggle("show"),this._element.classList.toggle("show"),P.trigger(this._element,"shown.bs.dropdown",e)}}hide(){if(h(this._element)||!this._menu.classList.contains("show"))return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_addEventListeners(){P.on(this._element,"click.bs.dropdown",t=>{t.preventDefault(),this.toggle()})}_completeHide(t){P.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>P.off(t,"mouseover",u)),this._popper&&this._popper.destroy(),this._menu.classList.remove("show"),this._element.classList.remove("show"),this._element.setAttribute("aria-expanded","false"),U.removeDataAttribute(this._menu,"popper"),P.trigger(this._element,"hidden.bs.dropdown",t))}_getConfig(t){if(t={...this.constructor.Default,...U.getDataAttributes(this._element),...t},l("dropdown",t,this.constructor.DefaultType),"object"==typeof t.reference&&!r(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError("dropdown".toUpperCase()+': Option "reference" provided type "object" without a required "getBoundingClientRect" method.');return t}_getMenuElement(){return t.next(this._element,".dropdown-menu")[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return ve;if(t.classList.contains("dropstart"))return ye;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?ge:me:e?be:_e}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:e,target:i}){const n=t.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(c);n.length&&y(n,i,"ArrowDown"===e,!n.includes(i)).focus()}static dropdownInterface(t,e){const i=Ae.getOrCreateInstance(t,e);if("string"==typeof e){if(void 0===i[e])throw new TypeError(`No method named "${e}"`);i[e]()}}static jQueryInterface(t){return this.each((function(){Ae.dropdownInterface(this,t)}))}static clearMenus(e){if(e&&(2===e.button||"keyup"===e.type&&"Tab"!==e.key))return;const i=t.find('[data-bs-toggle="dropdown"]');for(let t=0,n=i.length;tthis.matches('[data-bs-toggle="dropdown"]')?this:t.prev(this,'[data-bs-toggle="dropdown"]')[0];return"Escape"===e.key?(n().focus(),void Ae.clearMenus()):"ArrowUp"===e.key||"ArrowDown"===e.key?(i||n().click(),void Ae.getInstance(n())._selectMenuItem(e)):void(i&&"Space"!==e.key||Ae.clearMenus())}}P.on(document,"keydown.bs.dropdown.data-api",'[data-bs-toggle="dropdown"]',Ae.dataApiKeydownHandler),P.on(document,"keydown.bs.dropdown.data-api",".dropdown-menu",Ae.dataApiKeydownHandler),P.on(document,"click.bs.dropdown.data-api",Ae.clearMenus),P.on(document,"keyup.bs.dropdown.data-api",Ae.clearMenus),P.on(document,"click.bs.dropdown.data-api",'[data-bs-toggle="dropdown"]',(function(t){t.preventDefault(),Ae.dropdownInterface(this)})),_(Ae);class Te{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,"paddingRight",e=>e+t),this._setElementAttributes(".fixed-top, .fixed-bottom, .is-fixed, .sticky-top","paddingRight",e=>e+t),this._setElementAttributes(".sticky-top","marginRight",e=>e-t)}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t)[e];t.style[e]=i(Number.parseFloat(s))+"px"})}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,"paddingRight"),this._resetElementAttributes(".fixed-top, .fixed-bottom, .is-fixed, .sticky-top","paddingRight"),this._resetElementAttributes(".sticky-top","marginRight")}_saveInitialAttribute(t,e){const i=t.style[e];i&&U.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,t=>{const i=U.getDataAttribute(t,e);void 0===i?t.style.removeProperty(e):(U.removeDataAttribute(t,e),t.style[e]=i)})}_applyManipulationCallback(e,i){r(e)?i(e):t.find(e,this._element).forEach(i)}isOverflowing(){return this.getWidth()>0}}const Oe={isVisible:!0,isAnimated:!1,rootElement:"body",clickCallback:null},Ce={isVisible:"boolean",isAnimated:"boolean",rootElement:"(element|string)",clickCallback:"(function|null)"};class ke{constructor(t){this._config=this._getConfig(t),this._isAppended=!1,this._element=null}show(t){this._config.isVisible?(this._append(),this._config.isAnimated&&f(this._getElement()),this._getElement().classList.add("show"),this._emulateAnimation(()=>{b(t)})):b(t)}hide(t){this._config.isVisible?(this._getElement().classList.remove("show"),this._emulateAnimation(()=>{this.dispose(),b(t)})):b(t)}_getElement(){if(!this._element){const t=document.createElement("div");t.className="modal-backdrop",this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_getConfig(t){return(t={...Oe,..."object"==typeof t?t:{}}).rootElement=a(t.rootElement),l("backdrop",t,Ce),t}_append(){this._isAppended||(this._config.rootElement.appendChild(this._getElement()),P.on(this._getElement(),"mousedown.bs.backdrop",()=>{b(this._config.clickCallback)}),this._isAppended=!0)}dispose(){this._isAppended&&(P.off(this._element,"mousedown.bs.backdrop"),this._element.remove(),this._isAppended=!1)}_emulateAnimation(t){v(t,this._getElement(),this._config.isAnimated)}}const Le={backdrop:!0,keyboard:!0,focus:!0},xe={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"};class De extends B{constructor(e,i){super(e),this._config=this._getConfig(i),this._dialog=t.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._isShown=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollBar=new Te}static get Default(){return Le}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||P.trigger(this._element,"show.bs.modal",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isAnimated()&&(this._isTransitioning=!0),this._scrollBar.hide(),document.body.classList.add("modal-open"),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),P.on(this._element,"click.dismiss.bs.modal",'[data-bs-dismiss="modal"]',t=>this.hide(t)),P.on(this._dialog,"mousedown.dismiss.bs.modal",()=>{P.one(this._element,"mouseup.dismiss.bs.modal",t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)})}),this._showBackdrop(()=>this._showElement(t)))}hide(t){if(t&&["A","AREA"].includes(t.target.tagName)&&t.preventDefault(),!this._isShown||this._isTransitioning)return;if(P.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const e=this._isAnimated();e&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),P.off(document,"focusin.bs.modal"),this._element.classList.remove("show"),P.off(this._element,"click.dismiss.bs.modal"),P.off(this._dialog,"mousedown.dismiss.bs.modal"),this._queueCallback(()=>this._hideModal(),this._element,e)}dispose(){[window,this._dialog].forEach(t=>P.off(t,".bs.modal")),this._backdrop.dispose(),super.dispose(),P.off(document,"focusin.bs.modal")}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new ke({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_getConfig(t){return t={...Le,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},l("modal",t,xe),t}_showElement(e){const i=this._isAnimated(),n=t.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,n&&(n.scrollTop=0),i&&f(this._element),this._element.classList.add("show"),this._config.focus&&this._enforceFocus(),this._queueCallback(()=>{this._config.focus&&this._element.focus(),this._isTransitioning=!1,P.trigger(this._element,"shown.bs.modal",{relatedTarget:e})},this._dialog,i)}_enforceFocus(){P.off(document,"focusin.bs.modal"),P.on(document,"focusin.bs.modal",t=>{document===t.target||this._element===t.target||this._element.contains(t.target)||this._element.focus()})}_setEscapeEvent(){this._isShown?P.on(this._element,"keydown.dismiss.bs.modal",t=>{this._config.keyboard&&"Escape"===t.key?(t.preventDefault(),this.hide()):this._config.keyboard||"Escape"!==t.key||this._triggerBackdropTransition()}):P.off(this._element,"keydown.dismiss.bs.modal")}_setResizeEvent(){this._isShown?P.on(window,"resize.bs.modal",()=>this._adjustDialog()):P.off(window,"resize.bs.modal")}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide(()=>{document.body.classList.remove("modal-open"),this._resetAdjustments(),this._scrollBar.reset(),P.trigger(this._element,"hidden.bs.modal")})}_showBackdrop(t){P.on(this._element,"click.dismiss.bs.modal",t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&(!0===this._config.backdrop?this.hide():"static"===this._config.backdrop&&this._triggerBackdropTransition())}),this._backdrop.show(t)}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(P.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const{classList:t,scrollHeight:e,style:i}=this._element,n=e>document.documentElement.clientHeight;!n&&"hidden"===i.overflowY||t.contains("modal-static")||(n||(i.overflowY="hidden"),t.add("modal-static"),this._queueCallback(()=>{t.remove("modal-static"),n||this._queueCallback(()=>{i.overflowY=""},this._dialog)},this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;(!i&&t&&!g()||i&&!t&&g())&&(this._element.style.paddingLeft=e+"px"),(i&&!t&&!g()||!i&&t&&g())&&(this._element.style.paddingRight=e+"px")}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=De.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}P.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=s(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),P.one(e,"show.bs.modal",t=>{t.defaultPrevented||P.one(e,"hidden.bs.modal",()=>{c(this)&&this.focus()})}),De.getOrCreateInstance(e).toggle(this)})),_(De);const Se={backdrop:!0,keyboard:!0,scroll:!1},Ie={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"};class Ne extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._addEventListeners()}static get NAME(){return"offcanvas"}static get Default(){return Se}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||P.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._backdrop.show(),this._config.scroll||((new Te).hide(),this._enforceFocusOnElement(this._element)),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add("show"),this._queueCallback(()=>{P.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})},this._element,!0))}hide(){this._isShown&&(P.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(P.off(document,"focusin.bs.offcanvas"),this._element.blur(),this._isShown=!1,this._element.classList.remove("show"),this._backdrop.hide(),this._queueCallback(()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.scroll||(new Te).reset(),P.trigger(this._element,"hidden.bs.offcanvas")},this._element,!0)))}dispose(){this._backdrop.dispose(),super.dispose(),P.off(document,"focusin.bs.offcanvas")}_getConfig(t){return t={...Se,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},l("offcanvas",t,Ie),t}_initializeBackDrop(){return new ke({isVisible:this._config.backdrop,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:()=>this.hide()})}_enforceFocusOnElement(t){P.off(document,"focusin.bs.offcanvas"),P.on(document,"focusin.bs.offcanvas",e=>{document===e.target||t===e.target||t.contains(e.target)||t.focus()}),t.focus()}_addEventListeners(){P.on(this._element,"click.dismiss.bs.offcanvas",'[data-bs-dismiss="offcanvas"]',()=>this.hide()),P.on(this._element,"keydown.dismiss.bs.offcanvas",t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()})}static jQueryInterface(t){return this.each((function(){const e=Ne.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}P.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(e){const i=s(this);if(["A","AREA"].includes(this.tagName)&&e.preventDefault(),h(this))return;P.one(i,"hidden.bs.offcanvas",()=>{c(this)&&this.focus()});const n=t.findOne(".offcanvas.show");n&&n!==i&&Ne.getInstance(n).hide(),Ne.getOrCreateInstance(i).toggle(this)})),P.on(window,"load.bs.offcanvas.data-api",()=>t.find(".offcanvas.show").forEach(t=>Ne.getOrCreateInstance(t).show())),_(Ne);const je=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Me=/^(?:(?:https?|mailto|ftp|tel|file):|[^#&/:?]*(?:[#/?]|$))/i,Pe=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,He=(t,e)=>{const i=t.nodeName.toLowerCase();if(e.includes(i))return!je.has(i)||Boolean(Me.test(t.nodeValue)||Pe.test(t.nodeValue));const n=e.filter(t=>t instanceof RegExp);for(let t=0,e=n.length;t{He(t,a)||i.removeAttribute(t.nodeName)})}return n.body.innerHTML}const Be=new RegExp("(^|\\s)bs-tooltip\\S+","g"),We=new Set(["sanitize","allowList","sanitizeFn"]),qe={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},ze={AUTO:"auto",TOP:"top",RIGHT:g()?"left":"right",BOTTOM:"bottom",LEFT:g()?"right":"left"},$e={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},Ue={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"};class Fe extends B{constructor(t,e){if(void 0===fe)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this._config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return $e}static get NAME(){return"tooltip"}static get Event(){return Ue}static get DefaultType(){return qe}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains("show"))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),P.off(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.tip&&this.tip.remove(),this._popper&&this._popper.destroy(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const t=P.trigger(this._element,this.constructor.Event.SHOW),i=d(this._element),n=null===i?this._element.ownerDocument.documentElement.contains(this._element):i.contains(this._element);if(t.defaultPrevented||!n)return;const s=this.getTipElement(),o=e(this.constructor.NAME);s.setAttribute("id",o),this._element.setAttribute("aria-describedby",o),this.setContent(),this._config.animation&&s.classList.add("fade");const r="function"==typeof this._config.placement?this._config.placement.call(this,s,this._element):this._config.placement,a=this._getAttachment(r);this._addAttachmentClass(a);const{container:l}=this._config;R.set(s,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(l.appendChild(s),P.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=ue(this._element,s,this._getPopperConfig(a)),s.classList.add("show");const c="function"==typeof this._config.customClass?this._config.customClass():this._config.customClass;c&&s.classList.add(...c.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>{P.on(t,"mouseover",u)});const h=this.tip.classList.contains("fade");this._queueCallback(()=>{const t=this._hoverState;this._hoverState=null,P.trigger(this._element,this.constructor.Event.SHOWN),"out"===t&&this._leave(null,this)},this.tip,h)}hide(){if(!this._popper)return;const t=this.getTipElement();if(P.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented)return;t.classList.remove("show"),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>P.off(t,"mouseover",u)),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1;const e=this.tip.classList.contains("fade");this._queueCallback(()=>{this._isWithActiveTrigger()||("show"!==this._hoverState&&t.remove(),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),P.trigger(this._element,this.constructor.Event.HIDDEN),this._popper&&(this._popper.destroy(),this._popper=null))},this.tip,e),this._hoverState=""}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");return t.innerHTML=this._config.template,this.tip=t.children[0],this.tip}setContent(){const e=this.getTipElement();this.setElementContent(t.findOne(".tooltip-inner",e),this.getTitle()),e.classList.remove("fade","show")}setElementContent(t,e){if(null!==t)return r(e)?(e=a(e),void(this._config.html?e.parentNode!==t&&(t.innerHTML="",t.appendChild(e)):t.textContent=e.textContent)):void(this._config.html?(this._config.sanitize&&(e=Re(e,this._config.allowList,this._config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){let t=this._element.getAttribute("data-bs-original-title");return t||(t="function"==typeof this._config.title?this._config.title.call(this._element):this._config.title),t}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){const i=this.constructor.DATA_KEY;return(e=e||R.get(t.delegateTarget,i))||(e=new this.constructor(t.delegateTarget,this._getDelegateConfig()),R.set(t.delegateTarget,i,e)),e}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add("bs-tooltip-"+this.updateAttachment(t))}_getAttachment(t){return ze[t.toUpperCase()]}_setListeners(){this._config.trigger.split(" ").forEach(t=>{if("click"===t)P.on(this._element,this.constructor.Event.CLICK,this._config.selector,t=>this.toggle(t));else if("manual"!==t){const e="hover"===t?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,i="hover"===t?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;P.on(this._element,e,this._config.selector,t=>this._enter(t)),P.on(this._element,i,this._config.selector,t=>this._leave(t))}}),this._hideModalHandler=()=>{this._element&&this.hide()},P.on(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this._config.selector?this._config={...this._config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?"focus":"hover"]=!0),e.getTipElement().classList.contains("show")||"show"===e._hoverState?e._hoverState="show":(clearTimeout(e._timeout),e._hoverState="show",e._config.delay&&e._config.delay.show?e._timeout=setTimeout(()=>{"show"===e._hoverState&&e.show()},e._config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?"focus":"hover"]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState="out",e._config.delay&&e._config.delay.hide?e._timeout=setTimeout(()=>{"out"===e._hoverState&&e.hide()},e._config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=U.getDataAttributes(this._element);return Object.keys(e).forEach(t=>{We.has(t)&&delete e[t]}),(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).container=!1===t.container?document.body:a(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),l("tooltip",t,this.constructor.DefaultType),t.sanitize&&(t.template=Re(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};if(this._config)for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=t.getAttribute("class").match(Be);null!==e&&e.length>0&&e.map(t=>t.trim()).forEach(e=>t.classList.remove(e))}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}static jQueryInterface(t){return this.each((function(){const e=Fe.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}_(Fe);const Ve=new RegExp("(^|\\s)bs-popover\\S+","g"),Ke={...Fe.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:''},Xe={...Fe.DefaultType,content:"(string|element|function)"},Ye={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class Qe extends Fe{static get Default(){return Ke}static get NAME(){return"popover"}static get Event(){return Ye}static get DefaultType(){return Xe}isWithContent(){return this.getTitle()||this._getContent()}getTipElement(){return this.tip||(this.tip=super.getTipElement(),this.getTitle()||t.findOne(".popover-header",this.tip).remove(),this._getContent()||t.findOne(".popover-body",this.tip).remove()),this.tip}setContent(){const e=this.getTipElement();this.setElementContent(t.findOne(".popover-header",e),this.getTitle());let i=this._getContent();"function"==typeof i&&(i=i.call(this._element)),this.setElementContent(t.findOne(".popover-body",e),i),e.classList.remove("fade","show")}_addAttachmentClass(t){this.getTipElement().classList.add("bs-popover-"+this.updateAttachment(t))}_getContent(){return this._element.getAttribute("data-bs-content")||this._config.content}_cleanTipClass(){const t=this.getTipElement(),e=t.getAttribute("class").match(Ve);null!==e&&e.length>0&&e.map(t=>t.trim()).forEach(e=>t.classList.remove(e))}static jQueryInterface(t){return this.each((function(){const e=Qe.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}_(Qe);const Ge={offset:10,method:"auto",target:""},Ze={offset:"number",method:"string",target:"(string|element)"};class Je extends B{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._selector=`${this._config.target} .nav-link, ${this._config.target} .list-group-item, ${this._config.target} .dropdown-item`,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,P.on(this._scrollElement,"scroll.bs.scrollspy",()=>this._process()),this.refresh(),this._process()}static get Default(){return Ge}static get NAME(){return"scrollspy"}refresh(){const e=this._scrollElement===this._scrollElement.window?"offset":"position",i="auto"===this._config.method?e:this._config.method,s="position"===i?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),t.find(this._selector).map(e=>{const o=n(e),r=o?t.findOne(o):null;if(r){const t=r.getBoundingClientRect();if(t.width||t.height)return[U[i](r).top+s,o]}return null}).filter(t=>t).sort((t,e)=>t[0]-e[0]).forEach(t=>{this._offsets.push(t[0]),this._targets.push(t[1])})}dispose(){P.off(this._scrollElement,".bs.scrollspy"),super.dispose()}_getConfig(t){if("string"!=typeof(t={...Ge,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}}).target&&r(t.target)){let{id:i}=t.target;i||(i=e("scrollspy"),t.target.id=i),t.target="#"+i}return l("scrollspy",t,Ze),t}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),i=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=i){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t`${t}[data-bs-target="${e}"],${t}[href="${e}"]`),n=t.findOne(i.join(","));n.classList.contains("dropdown-item")?(t.findOne(".dropdown-toggle",n.closest(".dropdown")).classList.add("active"),n.classList.add("active")):(n.classList.add("active"),t.parents(n,".nav, .list-group").forEach(e=>{t.prev(e,".nav-link, .list-group-item").forEach(t=>t.classList.add("active")),t.prev(e,".nav-item").forEach(e=>{t.children(e,".nav-link").forEach(t=>t.classList.add("active"))})})),P.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:e})}_clear(){t.find(this._selector).filter(t=>t.classList.contains("active")).forEach(t=>t.classList.remove("active"))}static jQueryInterface(t){return this.each((function(){const e=Je.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}P.on(window,"load.bs.scrollspy.data-api",()=>{t.find('[data-bs-spy="scroll"]').forEach(t=>new Je(t))}),_(Je);class ti extends B{static get NAME(){return"tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains("active"))return;let e;const i=s(this._element),n=this._element.closest(".nav, .list-group");if(n){const i="UL"===n.nodeName||"OL"===n.nodeName?":scope > li > .active":".active";e=t.find(i,n),e=e[e.length-1]}const o=e?P.trigger(e,"hide.bs.tab",{relatedTarget:this._element}):null;if(P.trigger(this._element,"show.bs.tab",{relatedTarget:e}).defaultPrevented||null!==o&&o.defaultPrevented)return;this._activate(this._element,n);const r=()=>{P.trigger(e,"hidden.bs.tab",{relatedTarget:this._element}),P.trigger(this._element,"shown.bs.tab",{relatedTarget:e})};i?this._activate(i,i.parentNode,r):r()}_activate(e,i,n){const s=(!i||"UL"!==i.nodeName&&"OL"!==i.nodeName?t.children(i,".active"):t.find(":scope > li > .active",i))[0],o=n&&s&&s.classList.contains("fade"),r=()=>this._transitionComplete(e,s,n);s&&o?(s.classList.remove("show"),this._queueCallback(r,e,!0)):r()}_transitionComplete(e,i,n){if(i){i.classList.remove("active");const e=t.findOne(":scope > .dropdown-menu .active",i.parentNode);e&&e.classList.remove("active"),"tab"===i.getAttribute("role")&&i.setAttribute("aria-selected",!1)}e.classList.add("active"),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!0),f(e),e.classList.contains("fade")&&e.classList.add("show");let s=e.parentNode;if(s&&"LI"===s.nodeName&&(s=s.parentNode),s&&s.classList.contains("dropdown-menu")){const i=e.closest(".dropdown");i&&t.find(".dropdown-toggle",i).forEach(t=>t.classList.add("active")),e.setAttribute("aria-expanded",!0)}n&&n()}static jQueryInterface(t){return this.each((function(){const e=ti.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}P.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),h(this)||ti.getOrCreateInstance(this).show()})),_(ti);const ei={animation:"boolean",autohide:"boolean",delay:"number"},ii={animation:!0,autohide:!0,delay:5e3};class ni extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get DefaultType(){return ei}static get Default(){return ii}static get NAME(){return"toast"}show(){P.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove("hide"),f(this._element),this._element.classList.add("showing"),this._queueCallback(()=>{this._element.classList.remove("showing"),this._element.classList.add("show"),P.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()},this._element,this._config.animation))}hide(){this._element.classList.contains("show")&&(P.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.remove("show"),this._queueCallback(()=>{this._element.classList.add("hide"),P.trigger(this._element,"hidden.bs.toast")},this._element,this._config.animation)))}dispose(){this._clearTimeout(),this._element.classList.contains("show")&&this._element.classList.remove("show"),super.dispose()}_getConfig(t){return t={...ii,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},l("toast",t,this.constructor.DefaultType),t}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){P.on(this._element,"click.dismiss.bs.toast",'[data-bs-dismiss="toast"]',()=>this.hide()),P.on(this._element,"mouseover.bs.toast",t=>this._onInteraction(t,!0)),P.on(this._element,"mouseout.bs.toast",t=>this._onInteraction(t,!1)),P.on(this._element,"focusin.bs.toast",t=>this._onInteraction(t,!0)),P.on(this._element,"focusout.bs.toast",t=>this._onInteraction(t,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ni.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return _(ni),{Alert:W,Button:q,Carousel:Z,Collapse:et,Dropdown:Ae,Modal:De,Offcanvas:Ne,Popover:Qe,ScrollSpy:Je,Tab:ti,Toast:ni,Tooltip:Fe}})); diff --git a/src/content/assets/js/dune b/src/content/assets/js/dune deleted file mode 100644 index de39723..0000000 --- a/src/content/assets/js/dune +++ /dev/null @@ -1,26 +0,0 @@ -(rule - (target catalog.js) - (deps - (file ../../../js/catalog.bc.js)) - (action - (with-stdout-to - %{target} - (cat ../../../js/catalog.bc.js)))) - -(rule - (target babillard.js) - (deps - (file ../../../js/babillard.bc.js)) - (action - (with-stdout-to - %{target} - (cat ../../../js/babillard.bc.js)))) - -(rule - (target thread.js) - (deps - (file ../../../js/thread.bc.js)) - (action - (with-stdout-to - %{target} - (cat ../../../js/thread.bc.js)))) diff --git a/src/content/emoji-test.txt b/src/content/emoji-test.txt deleted file mode 100644 index 4f8b1a5..0000000 --- a/src/content/emoji-test.txt +++ /dev/null @@ -1,4991 +0,0 @@ -# emoji-test.txt -# Date: 2021-08-26, 17:22:23 GMT -# © 2021 Unicode®, Inc. -# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. -# For terms of use, see http://www.unicode.org/terms_of_use.html -# -# Emoji Keyboard/Display Test Data for UTS #51 -# Version: 14.0 -# -# For documentation and usage, see http://www.unicode.org/reports/tr51 -# -# This file provides data for testing which emoji forms should be in keyboards and which should also be displayed/processed. -# Format: code points; status # emoji name -# Code points — list of one or more hex code points, separated by spaces -# Status -# component — an Emoji_Component, -# excluding Regional_Indicators, ASCII, and non-Emoji. -# fully-qualified — a fully-qualified emoji (see ED-18 in UTS #51), -# excluding Emoji_Component -# minimally-qualified — a minimally-qualified emoji (see ED-18a in UTS #51) -# unqualified — a unqualified emoji (See ED-19 in UTS #51) -# Notes: -# • This includes the emoji components that need emoji presentation (skin tone and hair) -# when isolated, but omits the components that need not have an emoji -# presentation when isolated. -# • The RGI set is covered by the listed fully-qualified emoji. -# • The listed minimally-qualified and unqualified cover all cases where an -# element of the RGI set is missing one or more emoji presentation selectors. -# • The file is in CLDR order, not codepoint order. This is recommended (but not required!) for keyboard palettes. -# • The groups and subgroups are illustrative. See the Emoji Order chart for more information. - - -# group: Smileys & Emotion - -# subgroup: face-smiling -#1F600 ; fully-qualified # 😀 E1.0 grinning face -#1F603 ; fully-qualified # 😃 E0.6 grinning face with big eyes -#1F604 ; fully-qualified # 😄 E0.6 grinning face with smiling eyes -#1F601 ; fully-qualified # 😁 E0.6 beaming face with smiling eyes -#1F606 ; fully-qualified # 😆 E0.6 grinning squinting face -#1F605 ; fully-qualified # 😅 E0.6 grinning face with sweat -#1F923 ; fully-qualified # 🤣 E3.0 rolling on the floor laughing -#1F602 ; fully-qualified # 😂 E0.6 face with tears of joy -#1F642 ; fully-qualified # 🙂 E1.0 slightly smiling face -#1F643 ; fully-qualified # 🙃 E1.0 upside-down face -#1FAE0 ; fully-qualified # 🫠 E14.0 melting face -#1F609 ; fully-qualified # 😉 E0.6 winking face -#1F60A ; fully-qualified # 😊 E0.6 smiling face with smiling eyes -#1F607 ; fully-qualified # 😇 E1.0 smiling face with halo -# -## subgroup: face-affection -#1F970 ; fully-qualified # 🥰 E11.0 smiling face with hearts -#1F60D ; fully-qualified # 😍 E0.6 smiling face with heart-eyes -#1F929 ; fully-qualified # 🤩 E5.0 star-struck -#1F618 ; fully-qualified # 😘 E0.6 face blowing a kiss -#1F617 ; fully-qualified # 😗 E1.0 kissing face -#263A FE0F ; fully-qualified # ☺️ E0.6 smiling face -#263A ; unqualified # ☺ E0.6 smiling face -#1F61A ; fully-qualified # 😚 E0.6 kissing face with closed eyes -#1F619 ; fully-qualified # 😙 E1.0 kissing face with smiling eyes -#1F972 ; fully-qualified # 🥲 E13.0 smiling face with tear -# -## subgroup: face-tongue -#1F60B ; fully-qualified # 😋 E0.6 face savoring food -#1F61B ; fully-qualified # 😛 E1.0 face with tongue -#1F61C ; fully-qualified # 😜 E0.6 winking face with tongue -#1F92A ; fully-qualified # 🤪 E5.0 zany face -#1F61D ; fully-qualified # 😝 E0.6 squinting face with tongue -#1F911 ; fully-qualified # 🤑 E1.0 money-mouth face -# -## subgroup: face-hand -#1F917 ; fully-qualified # 🤗 E1.0 smiling face with open hands -#1F92D ; fully-qualified # 🤭 E5.0 face with hand over mouth -#1FAE2 ; fully-qualified # 🫢 E14.0 face with open eyes and hand over mouth -#1FAE3 ; fully-qualified # 🫣 E14.0 face with peeking eye -#1F92B ; fully-qualified # 🤫 E5.0 shushing face -#1F914 ; fully-qualified # 🤔 E1.0 thinking face -#1FAE1 ; fully-qualified # 🫡 E14.0 saluting face -# -## subgroup: face-neutral-skeptical -#1F910 ; fully-qualified # 🤐 E1.0 zipper-mouth face -#1F928 ; fully-qualified # 🤨 E5.0 face with raised eyebrow -#1F610 ; fully-qualified # 😐 E0.7 neutral face -#1F611 ; fully-qualified # 😑 E1.0 expressionless face -#1F636 ; fully-qualified # 😶 E1.0 face without mouth -#1FAE5 ; fully-qualified # 🫥 E14.0 dotted line face -#1F636 200D 1F32B FE0F ; fully-qualified # 😶‍🌫️ E13.1 face in clouds -#1F636 200D 1F32B ; minimally-qualified # 😶‍🌫 E13.1 face in clouds -#1F60F ; fully-qualified # 😏 E0.6 smirking face -#1F612 ; fully-qualified # 😒 E0.6 unamused face -#1F644 ; fully-qualified # 🙄 E1.0 face with rolling eyes -#1F62C ; fully-qualified # 😬 E1.0 grimacing face -#1F62E 200D 1F4A8 ; fully-qualified # 😮‍💨 E13.1 face exhaling -#1F925 ; fully-qualified # 🤥 E3.0 lying face -# -## subgroup: face-sleepy -#1F60C ; fully-qualified # 😌 E0.6 relieved face -#1F614 ; fully-qualified # 😔 E0.6 pensive face -#1F62A ; fully-qualified # 😪 E0.6 sleepy face -#1F924 ; fully-qualified # 🤤 E3.0 drooling face -#1F634 ; fully-qualified # 😴 E1.0 sleeping face -# -## subgroup: face-unwell -#1F637 ; fully-qualified # 😷 E0.6 face with medical mask -#1F912 ; fully-qualified # 🤒 E1.0 face with thermometer -#1F915 ; fully-qualified # 🤕 E1.0 face with head-bandage -#1F922 ; fully-qualified # 🤢 E3.0 nauseated face -#1F92E ; fully-qualified # 🤮 E5.0 face vomiting -#1F927 ; fully-qualified # 🤧 E3.0 sneezing face -#1F975 ; fully-qualified # 🥵 E11.0 hot face -#1F976 ; fully-qualified # 🥶 E11.0 cold face -#1F974 ; fully-qualified # 🥴 E11.0 woozy face -#1F635 ; fully-qualified # 😵 E0.6 face with crossed-out eyes -#1F635 200D 1F4AB ; fully-qualified # 😵‍💫 E13.1 face with spiral eyes -#1F92F ; fully-qualified # 🤯 E5.0 exploding head -# -## subgroup: face-hat -#1F920 ; fully-qualified # 🤠 E3.0 cowboy hat face -#1F973 ; fully-qualified # 🥳 E11.0 partying face -#1F978 ; fully-qualified # 🥸 E13.0 disguised face -# -## subgroup: face-glasses -#1F60E ; fully-qualified # 😎 E1.0 smiling face with sunglasses -#1F913 ; fully-qualified # 🤓 E1.0 nerd face -#1F9D0 ; fully-qualified # 🧐 E5.0 face with monocle -# -## subgroup: face-concerned -#1F615 ; fully-qualified # 😕 E1.0 confused face -#1FAE4 ; fully-qualified # 🫤 E14.0 face with diagonal mouth -#1F61F ; fully-qualified # 😟 E1.0 worried face -#1F641 ; fully-qualified # 🙁 E1.0 slightly frowning face -#2639 FE0F ; fully-qualified # ☹️ E0.7 frowning face -#2639 ; unqualified # ☹ E0.7 frowning face -#1F62E ; fully-qualified # 😮 E1.0 face with open mouth -#1F62F ; fully-qualified # 😯 E1.0 hushed face -#1F632 ; fully-qualified # 😲 E0.6 astonished face -#1F633 ; fully-qualified # 😳 E0.6 flushed face -#1F97A ; fully-qualified # 🥺 E11.0 pleading face -#1F979 ; fully-qualified # 🥹 E14.0 face holding back tears -#1F626 ; fully-qualified # 😦 E1.0 frowning face with open mouth -#1F627 ; fully-qualified # 😧 E1.0 anguished face -#1F628 ; fully-qualified # 😨 E0.6 fearful face -#1F630 ; fully-qualified # 😰 E0.6 anxious face with sweat -#1F625 ; fully-qualified # 😥 E0.6 sad but relieved face -#1F622 ; fully-qualified # 😢 E0.6 crying face -#1F62D ; fully-qualified # 😭 E0.6 loudly crying face -#1F631 ; fully-qualified # 😱 E0.6 face screaming in fear -#1F616 ; fully-qualified # 😖 E0.6 confounded face -#1F623 ; fully-qualified # 😣 E0.6 persevering face -#1F61E ; fully-qualified # 😞 E0.6 disappointed face -#1F613 ; fully-qualified # 😓 E0.6 downcast face with sweat -#1F629 ; fully-qualified # 😩 E0.6 weary face -#1F62B ; fully-qualified # 😫 E0.6 tired face -#1F971 ; fully-qualified # 🥱 E12.0 yawning face -# -## subgroup: face-negative -#1F624 ; fully-qualified # 😤 E0.6 face with steam from nose -#1F621 ; fully-qualified # 😡 E0.6 pouting face -#1F620 ; fully-qualified # 😠 E0.6 angry face -#1F92C ; fully-qualified # 🤬 E5.0 face with symbols on mouth -#1F608 ; fully-qualified # 😈 E1.0 smiling face with horns -#1F47F ; fully-qualified # 👿 E0.6 angry face with horns -#1F480 ; fully-qualified # 💀 E0.6 skull -#2620 FE0F ; fully-qualified # ☠️ E1.0 skull and crossbones -#2620 ; unqualified # ☠ E1.0 skull and crossbones -# -## subgroup: face-costume -#1F4A9 ; fully-qualified # 💩 E0.6 pile of poo -#1F921 ; fully-qualified # 🤡 E3.0 clown face -#1F479 ; fully-qualified # 👹 E0.6 ogre -#1F47A ; fully-qualified # 👺 E0.6 goblin -#1F47B ; fully-qualified # 👻 E0.6 ghost -#1F47D ; fully-qualified # 👽 E0.6 alien -#1F47E ; fully-qualified # 👾 E0.6 alien monster -#1F916 ; fully-qualified # 🤖 E1.0 robot -# -## subgroup: cat-face -#1F63A ; fully-qualified # 😺 E0.6 grinning cat -#1F638 ; fully-qualified # 😸 E0.6 grinning cat with smiling eyes -#1F639 ; fully-qualified # 😹 E0.6 cat with tears of joy -#1F63B ; fully-qualified # 😻 E0.6 smiling cat with heart-eyes -#1F63C ; fully-qualified # 😼 E0.6 cat with wry smile -#1F63D ; fully-qualified # 😽 E0.6 kissing cat -#1F640 ; fully-qualified # 🙀 E0.6 weary cat -#1F63F ; fully-qualified # 😿 E0.6 crying cat -#1F63E ; fully-qualified # 😾 E0.6 pouting cat -# -## subgroup: monkey-face -#1F648 ; fully-qualified # 🙈 E0.6 see-no-evil monkey -#1F649 ; fully-qualified # 🙉 E0.6 hear-no-evil monkey -#1F64A ; fully-qualified # 🙊 E0.6 speak-no-evil monkey -# -## subgroup: emotion -#1F48B ; fully-qualified # 💋 E0.6 kiss mark -#1F48C ; fully-qualified # 💌 E0.6 love letter -#1F498 ; fully-qualified # 💘 E0.6 heart with arrow -#1F49D ; fully-qualified # 💝 E0.6 heart with ribbon -#1F496 ; fully-qualified # 💖 E0.6 sparkling heart -#1F497 ; fully-qualified # 💗 E0.6 growing heart -#1F493 ; fully-qualified # 💓 E0.6 beating heart -#1F49E ; fully-qualified # 💞 E0.6 revolving hearts -#1F495 ; fully-qualified # 💕 E0.6 two hearts -#1F49F ; fully-qualified # 💟 E0.6 heart decoration -#2763 FE0F ; fully-qualified # ❣️ E1.0 heart exclamation -#2763 ; unqualified # ❣ E1.0 heart exclamation -#1F494 ; fully-qualified # 💔 E0.6 broken heart -#2764 FE0F 200D 1F525 ; fully-qualified # ❤️‍🔥 E13.1 heart on fire -#2764 200D 1F525 ; unqualified # ❤‍🔥 E13.1 heart on fire -#2764 FE0F 200D 1FA79 ; fully-qualified # ❤️‍🩹 E13.1 mending heart -#2764 200D 1FA79 ; unqualified # ❤‍🩹 E13.1 mending heart -#2764 FE0F ; fully-qualified # ❤️ E0.6 red heart -#2764 ; unqualified # ❤ E0.6 red heart -#1F9E1 ; fully-qualified # 🧡 E5.0 orange heart -#1F49B ; fully-qualified # 💛 E0.6 yellow heart -#1F49A ; fully-qualified # 💚 E0.6 green heart -#1F499 ; fully-qualified # 💙 E0.6 blue heart -#1F49C ; fully-qualified # 💜 E0.6 purple heart -#1F90E ; fully-qualified # 🤎 E12.0 brown heart -#1F5A4 ; fully-qualified # 🖤 E3.0 black heart -#1F90D ; fully-qualified # 🤍 E12.0 white heart -#1F4AF ; fully-qualified # 💯 E0.6 hundred points -#1F4A2 ; fully-qualified # 💢 E0.6 anger symbol -#1F4A5 ; fully-qualified # 💥 E0.6 collision -#1F4AB ; fully-qualified # 💫 E0.6 dizzy -#1F4A6 ; fully-qualified # 💦 E0.6 sweat droplets -#1F4A8 ; fully-qualified # 💨 E0.6 dashing away -#1F573 FE0F ; fully-qualified # 🕳️ E0.7 hole -#1F573 ; unqualified # 🕳 E0.7 hole -#1F4A3 ; fully-qualified # 💣 E0.6 bomb -#1F4AC ; fully-qualified # 💬 E0.6 speech balloon -#1F441 FE0F 200D 1F5E8 FE0F ; fully-qualified # 👁️‍🗨️ E2.0 eye in speech bubble -#1F441 200D 1F5E8 FE0F ; unqualified # 👁‍🗨️ E2.0 eye in speech bubble -#1F441 FE0F 200D 1F5E8 ; unqualified # 👁️‍🗨 E2.0 eye in speech bubble -#1F441 200D 1F5E8 ; unqualified # 👁‍🗨 E2.0 eye in speech bubble -#1F5E8 FE0F ; fully-qualified # 🗨️ E2.0 left speech bubble -#1F5E8 ; unqualified # 🗨 E2.0 left speech bubble -#1F5EF FE0F ; fully-qualified # 🗯️ E0.7 right anger bubble -#1F5EF ; unqualified # 🗯 E0.7 right anger bubble -#1F4AD ; fully-qualified # 💭 E1.0 thought balloon -#1F4A4 ; fully-qualified # 💤 E0.6 zzz -# -## Smileys & Emotion subtotal: 177 -## Smileys & Emotion subtotal: 177 w/o modifiers -# -## group: People & Body -# -## subgroup: hand-fingers-open -#1F44B ; fully-qualified # 👋 E0.6 waving hand -#1F44B 1F3FB ; fully-qualified # 👋🏻 E1.0 waving hand: light skin tone -#1F44B 1F3FC ; fully-qualified # 👋🏼 E1.0 waving hand: medium-light skin tone -#1F44B 1F3FD ; fully-qualified # 👋🏽 E1.0 waving hand: medium skin tone -#1F44B 1F3FE ; fully-qualified # 👋🏾 E1.0 waving hand: medium-dark skin tone -#1F44B 1F3FF ; fully-qualified # 👋🏿 E1.0 waving hand: dark skin tone -#1F91A ; fully-qualified # 🤚 E3.0 raised back of hand -#1F91A 1F3FB ; fully-qualified # 🤚🏻 E3.0 raised back of hand: light skin tone -#1F91A 1F3FC ; fully-qualified # 🤚🏼 E3.0 raised back of hand: medium-light skin tone -#1F91A 1F3FD ; fully-qualified # 🤚🏽 E3.0 raised back of hand: medium skin tone -#1F91A 1F3FE ; fully-qualified # 🤚🏾 E3.0 raised back of hand: medium-dark skin tone -#1F91A 1F3FF ; fully-qualified # 🤚🏿 E3.0 raised back of hand: dark skin tone -#1F590 FE0F ; fully-qualified # 🖐️ E0.7 hand with fingers splayed -#1F590 ; unqualified # 🖐 E0.7 hand with fingers splayed -#1F590 1F3FB ; fully-qualified # 🖐🏻 E1.0 hand with fingers splayed: light skin tone -#1F590 1F3FC ; fully-qualified # 🖐🏼 E1.0 hand with fingers splayed: medium-light skin tone -#1F590 1F3FD ; fully-qualified # 🖐🏽 E1.0 hand with fingers splayed: medium skin tone -#1F590 1F3FE ; fully-qualified # 🖐🏾 E1.0 hand with fingers splayed: medium-dark skin tone -#1F590 1F3FF ; fully-qualified # 🖐🏿 E1.0 hand with fingers splayed: dark skin tone -#270B ; fully-qualified # ✋ E0.6 raised hand -#270B 1F3FB ; fully-qualified # ✋🏻 E1.0 raised hand: light skin tone -#270B 1F3FC ; fully-qualified # ✋🏼 E1.0 raised hand: medium-light skin tone -#270B 1F3FD ; fully-qualified # ✋🏽 E1.0 raised hand: medium skin tone -#270B 1F3FE ; fully-qualified # ✋🏾 E1.0 raised hand: medium-dark skin tone -#270B 1F3FF ; fully-qualified # ✋🏿 E1.0 raised hand: dark skin tone -#1F596 ; fully-qualified # 🖖 E1.0 vulcan salute -#1F596 1F3FB ; fully-qualified # 🖖🏻 E1.0 vulcan salute: light skin tone -#1F596 1F3FC ; fully-qualified # 🖖🏼 E1.0 vulcan salute: medium-light skin tone -#1F596 1F3FD ; fully-qualified # 🖖🏽 E1.0 vulcan salute: medium skin tone -#1F596 1F3FE ; fully-qualified # 🖖🏾 E1.0 vulcan salute: medium-dark skin tone -#1F596 1F3FF ; fully-qualified # 🖖🏿 E1.0 vulcan salute: dark skin tone -#1FAF1 ; fully-qualified # 🫱 E14.0 rightwards hand -#1FAF1 1F3FB ; fully-qualified # 🫱🏻 E14.0 rightwards hand: light skin tone -#1FAF1 1F3FC ; fully-qualified # 🫱🏼 E14.0 rightwards hand: medium-light skin tone -#1FAF1 1F3FD ; fully-qualified # 🫱🏽 E14.0 rightwards hand: medium skin tone -#1FAF1 1F3FE ; fully-qualified # 🫱🏾 E14.0 rightwards hand: medium-dark skin tone -#1FAF1 1F3FF ; fully-qualified # 🫱🏿 E14.0 rightwards hand: dark skin tone -#1FAF2 ; fully-qualified # 🫲 E14.0 leftwards hand -#1FAF2 1F3FB ; fully-qualified # 🫲🏻 E14.0 leftwards hand: light skin tone -#1FAF2 1F3FC ; fully-qualified # 🫲🏼 E14.0 leftwards hand: medium-light skin tone -#1FAF2 1F3FD ; fully-qualified # 🫲🏽 E14.0 leftwards hand: medium skin tone -#1FAF2 1F3FE ; fully-qualified # 🫲🏾 E14.0 leftwards hand: medium-dark skin tone -#1FAF2 1F3FF ; fully-qualified # 🫲🏿 E14.0 leftwards hand: dark skin tone -#1FAF3 ; fully-qualified # 🫳 E14.0 palm down hand -#1FAF3 1F3FB ; fully-qualified # 🫳🏻 E14.0 palm down hand: light skin tone -#1FAF3 1F3FC ; fully-qualified # 🫳🏼 E14.0 palm down hand: medium-light skin tone -#1FAF3 1F3FD ; fully-qualified # 🫳🏽 E14.0 palm down hand: medium skin tone -#1FAF3 1F3FE ; fully-qualified # 🫳🏾 E14.0 palm down hand: medium-dark skin tone -#1FAF3 1F3FF ; fully-qualified # 🫳🏿 E14.0 palm down hand: dark skin tone -#1FAF4 ; fully-qualified # 🫴 E14.0 palm up hand -#1FAF4 1F3FB ; fully-qualified # 🫴🏻 E14.0 palm up hand: light skin tone -#1FAF4 1F3FC ; fully-qualified # 🫴🏼 E14.0 palm up hand: medium-light skin tone -#1FAF4 1F3FD ; fully-qualified # 🫴🏽 E14.0 palm up hand: medium skin tone -#1FAF4 1F3FE ; fully-qualified # 🫴🏾 E14.0 palm up hand: medium-dark skin tone -#1FAF4 1F3FF ; fully-qualified # 🫴🏿 E14.0 palm up hand: dark skin tone -# -## subgroup: hand-fingers-partial -#1F44C ; fully-qualified # 👌 E0.6 OK hand -#1F44C 1F3FB ; fully-qualified # 👌🏻 E1.0 OK hand: light skin tone -#1F44C 1F3FC ; fully-qualified # 👌🏼 E1.0 OK hand: medium-light skin tone -#1F44C 1F3FD ; fully-qualified # 👌🏽 E1.0 OK hand: medium skin tone -#1F44C 1F3FE ; fully-qualified # 👌🏾 E1.0 OK hand: medium-dark skin tone -#1F44C 1F3FF ; fully-qualified # 👌🏿 E1.0 OK hand: dark skin tone -#1F90C ; fully-qualified # 🤌 E13.0 pinched fingers -#1F90C 1F3FB ; fully-qualified # 🤌🏻 E13.0 pinched fingers: light skin tone -#1F90C 1F3FC ; fully-qualified # 🤌🏼 E13.0 pinched fingers: medium-light skin tone -#1F90C 1F3FD ; fully-qualified # 🤌🏽 E13.0 pinched fingers: medium skin tone -#1F90C 1F3FE ; fully-qualified # 🤌🏾 E13.0 pinched fingers: medium-dark skin tone -#1F90C 1F3FF ; fully-qualified # 🤌🏿 E13.0 pinched fingers: dark skin tone -#1F90F ; fully-qualified # 🤏 E12.0 pinching hand -#1F90F 1F3FB ; fully-qualified # 🤏🏻 E12.0 pinching hand: light skin tone -#1F90F 1F3FC ; fully-qualified # 🤏🏼 E12.0 pinching hand: medium-light skin tone -#1F90F 1F3FD ; fully-qualified # 🤏🏽 E12.0 pinching hand: medium skin tone -#1F90F 1F3FE ; fully-qualified # 🤏🏾 E12.0 pinching hand: medium-dark skin tone -#1F90F 1F3FF ; fully-qualified # 🤏🏿 E12.0 pinching hand: dark skin tone -#270C FE0F ; fully-qualified # ✌️ E0.6 victory hand -#270C ; unqualified # ✌ E0.6 victory hand -#270C 1F3FB ; fully-qualified # ✌🏻 E1.0 victory hand: light skin tone -#270C 1F3FC ; fully-qualified # ✌🏼 E1.0 victory hand: medium-light skin tone -#270C 1F3FD ; fully-qualified # ✌🏽 E1.0 victory hand: medium skin tone -#270C 1F3FE ; fully-qualified # ✌🏾 E1.0 victory hand: medium-dark skin tone -#270C 1F3FF ; fully-qualified # ✌🏿 E1.0 victory hand: dark skin tone -#1F91E ; fully-qualified # 🤞 E3.0 crossed fingers -#1F91E 1F3FB ; fully-qualified # 🤞🏻 E3.0 crossed fingers: light skin tone -#1F91E 1F3FC ; fully-qualified # 🤞🏼 E3.0 crossed fingers: medium-light skin tone -#1F91E 1F3FD ; fully-qualified # 🤞🏽 E3.0 crossed fingers: medium skin tone -#1F91E 1F3FE ; fully-qualified # 🤞🏾 E3.0 crossed fingers: medium-dark skin tone -#1F91E 1F3FF ; fully-qualified # 🤞🏿 E3.0 crossed fingers: dark skin tone -#1FAF0 ; fully-qualified # 🫰 E14.0 hand with index finger and thumb crossed -#1FAF0 1F3FB ; fully-qualified # 🫰🏻 E14.0 hand with index finger and thumb crossed: light skin tone -#1FAF0 1F3FC ; fully-qualified # 🫰🏼 E14.0 hand with index finger and thumb crossed: medium-light skin tone -#1FAF0 1F3FD ; fully-qualified # 🫰🏽 E14.0 hand with index finger and thumb crossed: medium skin tone -#1FAF0 1F3FE ; fully-qualified # 🫰🏾 E14.0 hand with index finger and thumb crossed: medium-dark skin tone -#1FAF0 1F3FF ; fully-qualified # 🫰🏿 E14.0 hand with index finger and thumb crossed: dark skin tone -#1F91F ; fully-qualified # 🤟 E5.0 love-you gesture -#1F91F 1F3FB ; fully-qualified # 🤟🏻 E5.0 love-you gesture: light skin tone -#1F91F 1F3FC ; fully-qualified # 🤟🏼 E5.0 love-you gesture: medium-light skin tone -#1F91F 1F3FD ; fully-qualified # 🤟🏽 E5.0 love-you gesture: medium skin tone -#1F91F 1F3FE ; fully-qualified # 🤟🏾 E5.0 love-you gesture: medium-dark skin tone -#1F91F 1F3FF ; fully-qualified # 🤟🏿 E5.0 love-you gesture: dark skin tone -#1F918 ; fully-qualified # 🤘 E1.0 sign of the horns -#1F918 1F3FB ; fully-qualified # 🤘🏻 E1.0 sign of the horns: light skin tone -#1F918 1F3FC ; fully-qualified # 🤘🏼 E1.0 sign of the horns: medium-light skin tone -#1F918 1F3FD ; fully-qualified # 🤘🏽 E1.0 sign of the horns: medium skin tone -#1F918 1F3FE ; fully-qualified # 🤘🏾 E1.0 sign of the horns: medium-dark skin tone -#1F918 1F3FF ; fully-qualified # 🤘🏿 E1.0 sign of the horns: dark skin tone -#1F919 ; fully-qualified # 🤙 E3.0 call me hand -#1F919 1F3FB ; fully-qualified # 🤙🏻 E3.0 call me hand: light skin tone -#1F919 1F3FC ; fully-qualified # 🤙🏼 E3.0 call me hand: medium-light skin tone -#1F919 1F3FD ; fully-qualified # 🤙🏽 E3.0 call me hand: medium skin tone -#1F919 1F3FE ; fully-qualified # 🤙🏾 E3.0 call me hand: medium-dark skin tone -#1F919 1F3FF ; fully-qualified # 🤙🏿 E3.0 call me hand: dark skin tone -# -## subgroup: hand-single-finger -#1F448 ; fully-qualified # 👈 E0.6 backhand index pointing left -#1F448 1F3FB ; fully-qualified # 👈🏻 E1.0 backhand index pointing left: light skin tone -#1F448 1F3FC ; fully-qualified # 👈🏼 E1.0 backhand index pointing left: medium-light skin tone -#1F448 1F3FD ; fully-qualified # 👈🏽 E1.0 backhand index pointing left: medium skin tone -#1F448 1F3FE ; fully-qualified # 👈🏾 E1.0 backhand index pointing left: medium-dark skin tone -#1F448 1F3FF ; fully-qualified # 👈🏿 E1.0 backhand index pointing left: dark skin tone -#1F449 ; fully-qualified # 👉 E0.6 backhand index pointing right -#1F449 1F3FB ; fully-qualified # 👉🏻 E1.0 backhand index pointing right: light skin tone -#1F449 1F3FC ; fully-qualified # 👉🏼 E1.0 backhand index pointing right: medium-light skin tone -#1F449 1F3FD ; fully-qualified # 👉🏽 E1.0 backhand index pointing right: medium skin tone -#1F449 1F3FE ; fully-qualified # 👉🏾 E1.0 backhand index pointing right: medium-dark skin tone -#1F449 1F3FF ; fully-qualified # 👉🏿 E1.0 backhand index pointing right: dark skin tone -#1F446 ; fully-qualified # 👆 E0.6 backhand index pointing up -#1F446 1F3FB ; fully-qualified # 👆🏻 E1.0 backhand index pointing up: light skin tone -#1F446 1F3FC ; fully-qualified # 👆🏼 E1.0 backhand index pointing up: medium-light skin tone -#1F446 1F3FD ; fully-qualified # 👆🏽 E1.0 backhand index pointing up: medium skin tone -#1F446 1F3FE ; fully-qualified # 👆🏾 E1.0 backhand index pointing up: medium-dark skin tone -#1F446 1F3FF ; fully-qualified # 👆🏿 E1.0 backhand index pointing up: dark skin tone -#1F595 ; fully-qualified # 🖕 E1.0 middle finger -#1F595 1F3FB ; fully-qualified # 🖕🏻 E1.0 middle finger: light skin tone -#1F595 1F3FC ; fully-qualified # 🖕🏼 E1.0 middle finger: medium-light skin tone -#1F595 1F3FD ; fully-qualified # 🖕🏽 E1.0 middle finger: medium skin tone -#1F595 1F3FE ; fully-qualified # 🖕🏾 E1.0 middle finger: medium-dark skin tone -#1F595 1F3FF ; fully-qualified # 🖕🏿 E1.0 middle finger: dark skin tone -#1F447 ; fully-qualified # 👇 E0.6 backhand index pointing down -#1F447 1F3FB ; fully-qualified # 👇🏻 E1.0 backhand index pointing down: light skin tone -#1F447 1F3FC ; fully-qualified # 👇🏼 E1.0 backhand index pointing down: medium-light skin tone -#1F447 1F3FD ; fully-qualified # 👇🏽 E1.0 backhand index pointing down: medium skin tone -#1F447 1F3FE ; fully-qualified # 👇🏾 E1.0 backhand index pointing down: medium-dark skin tone -#1F447 1F3FF ; fully-qualified # 👇🏿 E1.0 backhand index pointing down: dark skin tone -#261D FE0F ; fully-qualified # ☝️ E0.6 index pointing up -#261D ; unqualified # ☝ E0.6 index pointing up -#261D 1F3FB ; fully-qualified # ☝🏻 E1.0 index pointing up: light skin tone -#261D 1F3FC ; fully-qualified # ☝🏼 E1.0 index pointing up: medium-light skin tone -#261D 1F3FD ; fully-qualified # ☝🏽 E1.0 index pointing up: medium skin tone -#261D 1F3FE ; fully-qualified # ☝🏾 E1.0 index pointing up: medium-dark skin tone -#261D 1F3FF ; fully-qualified # ☝🏿 E1.0 index pointing up: dark skin tone -#1FAF5 ; fully-qualified # 🫵 E14.0 index pointing at the viewer -#1FAF5 1F3FB ; fully-qualified # 🫵🏻 E14.0 index pointing at the viewer: light skin tone -#1FAF5 1F3FC ; fully-qualified # 🫵🏼 E14.0 index pointing at the viewer: medium-light skin tone -#1FAF5 1F3FD ; fully-qualified # 🫵🏽 E14.0 index pointing at the viewer: medium skin tone -#1FAF5 1F3FE ; fully-qualified # 🫵🏾 E14.0 index pointing at the viewer: medium-dark skin tone -#1FAF5 1F3FF ; fully-qualified # 🫵🏿 E14.0 index pointing at the viewer: dark skin tone -# -## subgroup: hand-fingers-closed -#1F44D ; fully-qualified # 👍 E0.6 thumbs up -#1F44D 1F3FB ; fully-qualified # 👍🏻 E1.0 thumbs up: light skin tone -#1F44D 1F3FC ; fully-qualified # 👍🏼 E1.0 thumbs up: medium-light skin tone -#1F44D 1F3FD ; fully-qualified # 👍🏽 E1.0 thumbs up: medium skin tone -#1F44D 1F3FE ; fully-qualified # 👍🏾 E1.0 thumbs up: medium-dark skin tone -#1F44D 1F3FF ; fully-qualified # 👍🏿 E1.0 thumbs up: dark skin tone -#1F44E ; fully-qualified # 👎 E0.6 thumbs down -#1F44E 1F3FB ; fully-qualified # 👎🏻 E1.0 thumbs down: light skin tone -#1F44E 1F3FC ; fully-qualified # 👎🏼 E1.0 thumbs down: medium-light skin tone -#1F44E 1F3FD ; fully-qualified # 👎🏽 E1.0 thumbs down: medium skin tone -#1F44E 1F3FE ; fully-qualified # 👎🏾 E1.0 thumbs down: medium-dark skin tone -#1F44E 1F3FF ; fully-qualified # 👎🏿 E1.0 thumbs down: dark skin tone -#270A ; fully-qualified # ✊ E0.6 raised fist -#270A 1F3FB ; fully-qualified # ✊🏻 E1.0 raised fist: light skin tone -#270A 1F3FC ; fully-qualified # ✊🏼 E1.0 raised fist: medium-light skin tone -#270A 1F3FD ; fully-qualified # ✊🏽 E1.0 raised fist: medium skin tone -#270A 1F3FE ; fully-qualified # ✊🏾 E1.0 raised fist: medium-dark skin tone -#270A 1F3FF ; fully-qualified # ✊🏿 E1.0 raised fist: dark skin tone -#1F44A ; fully-qualified # 👊 E0.6 oncoming fist -#1F44A 1F3FB ; fully-qualified # 👊🏻 E1.0 oncoming fist: light skin tone -#1F44A 1F3FC ; fully-qualified # 👊🏼 E1.0 oncoming fist: medium-light skin tone -#1F44A 1F3FD ; fully-qualified # 👊🏽 E1.0 oncoming fist: medium skin tone -#1F44A 1F3FE ; fully-qualified # 👊🏾 E1.0 oncoming fist: medium-dark skin tone -#1F44A 1F3FF ; fully-qualified # 👊🏿 E1.0 oncoming fist: dark skin tone -#1F91B ; fully-qualified # 🤛 E3.0 left-facing fist -#1F91B 1F3FB ; fully-qualified # 🤛🏻 E3.0 left-facing fist: light skin tone -#1F91B 1F3FC ; fully-qualified # 🤛🏼 E3.0 left-facing fist: medium-light skin tone -#1F91B 1F3FD ; fully-qualified # 🤛🏽 E3.0 left-facing fist: medium skin tone -#1F91B 1F3FE ; fully-qualified # 🤛🏾 E3.0 left-facing fist: medium-dark skin tone -#1F91B 1F3FF ; fully-qualified # 🤛🏿 E3.0 left-facing fist: dark skin tone -#1F91C ; fully-qualified # 🤜 E3.0 right-facing fist -#1F91C 1F3FB ; fully-qualified # 🤜🏻 E3.0 right-facing fist: light skin tone -#1F91C 1F3FC ; fully-qualified # 🤜🏼 E3.0 right-facing fist: medium-light skin tone -#1F91C 1F3FD ; fully-qualified # 🤜🏽 E3.0 right-facing fist: medium skin tone -#1F91C 1F3FE ; fully-qualified # 🤜🏾 E3.0 right-facing fist: medium-dark skin tone -#1F91C 1F3FF ; fully-qualified # 🤜🏿 E3.0 right-facing fist: dark skin tone -# -## subgroup: hands -#1F44F ; fully-qualified # 👏 E0.6 clapping hands -#1F44F 1F3FB ; fully-qualified # 👏🏻 E1.0 clapping hands: light skin tone -#1F44F 1F3FC ; fully-qualified # 👏🏼 E1.0 clapping hands: medium-light skin tone -#1F44F 1F3FD ; fully-qualified # 👏🏽 E1.0 clapping hands: medium skin tone -#1F44F 1F3FE ; fully-qualified # 👏🏾 E1.0 clapping hands: medium-dark skin tone -#1F44F 1F3FF ; fully-qualified # 👏🏿 E1.0 clapping hands: dark skin tone -#1F64C ; fully-qualified # 🙌 E0.6 raising hands -#1F64C 1F3FB ; fully-qualified # 🙌🏻 E1.0 raising hands: light skin tone -#1F64C 1F3FC ; fully-qualified # 🙌🏼 E1.0 raising hands: medium-light skin tone -#1F64C 1F3FD ; fully-qualified # 🙌🏽 E1.0 raising hands: medium skin tone -#1F64C 1F3FE ; fully-qualified # 🙌🏾 E1.0 raising hands: medium-dark skin tone -#1F64C 1F3FF ; fully-qualified # 🙌🏿 E1.0 raising hands: dark skin tone -#1FAF6 ; fully-qualified # 🫶 E14.0 heart hands -#1FAF6 1F3FB ; fully-qualified # 🫶🏻 E14.0 heart hands: light skin tone -#1FAF6 1F3FC ; fully-qualified # 🫶🏼 E14.0 heart hands: medium-light skin tone -#1FAF6 1F3FD ; fully-qualified # 🫶🏽 E14.0 heart hands: medium skin tone -#1FAF6 1F3FE ; fully-qualified # 🫶🏾 E14.0 heart hands: medium-dark skin tone -#1FAF6 1F3FF ; fully-qualified # 🫶🏿 E14.0 heart hands: dark skin tone -#1F450 ; fully-qualified # 👐 E0.6 open hands -#1F450 1F3FB ; fully-qualified # 👐🏻 E1.0 open hands: light skin tone -#1F450 1F3FC ; fully-qualified # 👐🏼 E1.0 open hands: medium-light skin tone -#1F450 1F3FD ; fully-qualified # 👐🏽 E1.0 open hands: medium skin tone -#1F450 1F3FE ; fully-qualified # 👐🏾 E1.0 open hands: medium-dark skin tone -#1F450 1F3FF ; fully-qualified # 👐🏿 E1.0 open hands: dark skin tone -#1F932 ; fully-qualified # 🤲 E5.0 palms up together -#1F932 1F3FB ; fully-qualified # 🤲🏻 E5.0 palms up together: light skin tone -#1F932 1F3FC ; fully-qualified # 🤲🏼 E5.0 palms up together: medium-light skin tone -#1F932 1F3FD ; fully-qualified # 🤲🏽 E5.0 palms up together: medium skin tone -#1F932 1F3FE ; fully-qualified # 🤲🏾 E5.0 palms up together: medium-dark skin tone -#1F932 1F3FF ; fully-qualified # 🤲🏿 E5.0 palms up together: dark skin tone -#1F91D ; fully-qualified # 🤝 E3.0 handshake -#1F91D 1F3FB ; fully-qualified # 🤝🏻 E3.0 handshake: light skin tone -#1F91D 1F3FC ; fully-qualified # 🤝🏼 E3.0 handshake: medium-light skin tone -#1F91D 1F3FD ; fully-qualified # 🤝🏽 E3.0 handshake: medium skin tone -#1F91D 1F3FE ; fully-qualified # 🤝🏾 E3.0 handshake: medium-dark skin tone -#1F91D 1F3FF ; fully-qualified # 🤝🏿 E3.0 handshake: dark skin tone -#1FAF1 1F3FB 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏻‍🫲🏼 E14.0 handshake: light skin tone, medium-light skin tone -#1FAF1 1F3FB 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏻‍🫲🏽 E14.0 handshake: light skin tone, medium skin tone -#1FAF1 1F3FB 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏻‍🫲🏾 E14.0 handshake: light skin tone, medium-dark skin tone -#1FAF1 1F3FB 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏻‍🫲🏿 E14.0 handshake: light skin tone, dark skin tone -#1FAF1 1F3FC 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏼‍🫲🏻 E14.0 handshake: medium-light skin tone, light skin tone -#1FAF1 1F3FC 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏼‍🫲🏽 E14.0 handshake: medium-light skin tone, medium skin tone -#1FAF1 1F3FC 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏼‍🫲🏾 E14.0 handshake: medium-light skin tone, medium-dark skin tone -#1FAF1 1F3FC 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏼‍🫲🏿 E14.0 handshake: medium-light skin tone, dark skin tone -#1FAF1 1F3FD 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏽‍🫲🏻 E14.0 handshake: medium skin tone, light skin tone -#1FAF1 1F3FD 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏽‍🫲🏼 E14.0 handshake: medium skin tone, medium-light skin tone -#1FAF1 1F3FD 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏽‍🫲🏾 E14.0 handshake: medium skin tone, medium-dark skin tone -#1FAF1 1F3FD 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏽‍🫲🏿 E14.0 handshake: medium skin tone, dark skin tone -#1FAF1 1F3FE 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏾‍🫲🏻 E14.0 handshake: medium-dark skin tone, light skin tone -#1FAF1 1F3FE 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏾‍🫲🏼 E14.0 handshake: medium-dark skin tone, medium-light skin tone -#1FAF1 1F3FE 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏾‍🫲🏽 E14.0 handshake: medium-dark skin tone, medium skin tone -#1FAF1 1F3FE 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏾‍🫲🏿 E14.0 handshake: medium-dark skin tone, dark skin tone -#1FAF1 1F3FF 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏿‍🫲🏻 E14.0 handshake: dark skin tone, light skin tone -#1FAF1 1F3FF 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏿‍🫲🏼 E14.0 handshake: dark skin tone, medium-light skin tone -#1FAF1 1F3FF 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏿‍🫲🏽 E14.0 handshake: dark skin tone, medium skin tone -#1FAF1 1F3FF 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏿‍🫲🏾 E14.0 handshake: dark skin tone, medium-dark skin tone -#1F64F ; fully-qualified # 🙏 E0.6 folded hands -#1F64F 1F3FB ; fully-qualified # 🙏🏻 E1.0 folded hands: light skin tone -#1F64F 1F3FC ; fully-qualified # 🙏🏼 E1.0 folded hands: medium-light skin tone -#1F64F 1F3FD ; fully-qualified # 🙏🏽 E1.0 folded hands: medium skin tone -#1F64F 1F3FE ; fully-qualified # 🙏🏾 E1.0 folded hands: medium-dark skin tone -#1F64F 1F3FF ; fully-qualified # 🙏🏿 E1.0 folded hands: dark skin tone -# -## subgroup: hand-prop -#270D FE0F ; fully-qualified # ✍️ E0.7 writing hand -#270D ; unqualified # ✍ E0.7 writing hand -#270D 1F3FB ; fully-qualified # ✍🏻 E1.0 writing hand: light skin tone -#270D 1F3FC ; fully-qualified # ✍🏼 E1.0 writing hand: medium-light skin tone -#270D 1F3FD ; fully-qualified # ✍🏽 E1.0 writing hand: medium skin tone -#270D 1F3FE ; fully-qualified # ✍🏾 E1.0 writing hand: medium-dark skin tone -#270D 1F3FF ; fully-qualified # ✍🏿 E1.0 writing hand: dark skin tone -#1F485 ; fully-qualified # 💅 E0.6 nail polish -#1F485 1F3FB ; fully-qualified # 💅🏻 E1.0 nail polish: light skin tone -#1F485 1F3FC ; fully-qualified # 💅🏼 E1.0 nail polish: medium-light skin tone -#1F485 1F3FD ; fully-qualified # 💅🏽 E1.0 nail polish: medium skin tone -#1F485 1F3FE ; fully-qualified # 💅🏾 E1.0 nail polish: medium-dark skin tone -#1F485 1F3FF ; fully-qualified # 💅🏿 E1.0 nail polish: dark skin tone -#1F933 ; fully-qualified # 🤳 E3.0 selfie -#1F933 1F3FB ; fully-qualified # 🤳🏻 E3.0 selfie: light skin tone -#1F933 1F3FC ; fully-qualified # 🤳🏼 E3.0 selfie: medium-light skin tone -#1F933 1F3FD ; fully-qualified # 🤳🏽 E3.0 selfie: medium skin tone -#1F933 1F3FE ; fully-qualified # 🤳🏾 E3.0 selfie: medium-dark skin tone -#1F933 1F3FF ; fully-qualified # 🤳🏿 E3.0 selfie: dark skin tone -# -## subgroup: body-parts -#1F4AA ; fully-qualified # 💪 E0.6 flexed biceps -#1F4AA 1F3FB ; fully-qualified # 💪🏻 E1.0 flexed biceps: light skin tone -#1F4AA 1F3FC ; fully-qualified # 💪🏼 E1.0 flexed biceps: medium-light skin tone -#1F4AA 1F3FD ; fully-qualified # 💪🏽 E1.0 flexed biceps: medium skin tone -#1F4AA 1F3FE ; fully-qualified # 💪🏾 E1.0 flexed biceps: medium-dark skin tone -#1F4AA 1F3FF ; fully-qualified # 💪🏿 E1.0 flexed biceps: dark skin tone -#1F9BE ; fully-qualified # 🦾 E12.0 mechanical arm -#1F9BF ; fully-qualified # 🦿 E12.0 mechanical leg -#1F9B5 ; fully-qualified # 🦵 E11.0 leg -#1F9B5 1F3FB ; fully-qualified # 🦵🏻 E11.0 leg: light skin tone -#1F9B5 1F3FC ; fully-qualified # 🦵🏼 E11.0 leg: medium-light skin tone -#1F9B5 1F3FD ; fully-qualified # 🦵🏽 E11.0 leg: medium skin tone -#1F9B5 1F3FE ; fully-qualified # 🦵🏾 E11.0 leg: medium-dark skin tone -#1F9B5 1F3FF ; fully-qualified # 🦵🏿 E11.0 leg: dark skin tone -#1F9B6 ; fully-qualified # 🦶 E11.0 foot -#1F9B6 1F3FB ; fully-qualified # 🦶🏻 E11.0 foot: light skin tone -#1F9B6 1F3FC ; fully-qualified # 🦶🏼 E11.0 foot: medium-light skin tone -#1F9B6 1F3FD ; fully-qualified # 🦶🏽 E11.0 foot: medium skin tone -#1F9B6 1F3FE ; fully-qualified # 🦶🏾 E11.0 foot: medium-dark skin tone -#1F9B6 1F3FF ; fully-qualified # 🦶🏿 E11.0 foot: dark skin tone -#1F442 ; fully-qualified # 👂 E0.6 ear -#1F442 1F3FB ; fully-qualified # 👂🏻 E1.0 ear: light skin tone -#1F442 1F3FC ; fully-qualified # 👂🏼 E1.0 ear: medium-light skin tone -#1F442 1F3FD ; fully-qualified # 👂🏽 E1.0 ear: medium skin tone -#1F442 1F3FE ; fully-qualified # 👂🏾 E1.0 ear: medium-dark skin tone -#1F442 1F3FF ; fully-qualified # 👂🏿 E1.0 ear: dark skin tone -#1F9BB ; fully-qualified # 🦻 E12.0 ear with hearing aid -#1F9BB 1F3FB ; fully-qualified # 🦻🏻 E12.0 ear with hearing aid: light skin tone -#1F9BB 1F3FC ; fully-qualified # 🦻🏼 E12.0 ear with hearing aid: medium-light skin tone -#1F9BB 1F3FD ; fully-qualified # 🦻🏽 E12.0 ear with hearing aid: medium skin tone -#1F9BB 1F3FE ; fully-qualified # 🦻🏾 E12.0 ear with hearing aid: medium-dark skin tone -#1F9BB 1F3FF ; fully-qualified # 🦻🏿 E12.0 ear with hearing aid: dark skin tone -#1F443 ; fully-qualified # 👃 E0.6 nose -#1F443 1F3FB ; fully-qualified # 👃🏻 E1.0 nose: light skin tone -#1F443 1F3FC ; fully-qualified # 👃🏼 E1.0 nose: medium-light skin tone -#1F443 1F3FD ; fully-qualified # 👃🏽 E1.0 nose: medium skin tone -#1F443 1F3FE ; fully-qualified # 👃🏾 E1.0 nose: medium-dark skin tone -#1F443 1F3FF ; fully-qualified # 👃🏿 E1.0 nose: dark skin tone -#1F9E0 ; fully-qualified # 🧠 E5.0 brain -#1FAC0 ; fully-qualified # 🫀 E13.0 anatomical heart -#1FAC1 ; fully-qualified # 🫁 E13.0 lungs -#1F9B7 ; fully-qualified # 🦷 E11.0 tooth -#1F9B4 ; fully-qualified # 🦴 E11.0 bone -#1F440 ; fully-qualified # 👀 E0.6 eyes -#1F441 FE0F ; fully-qualified # 👁️ E0.7 eye -#1F441 ; unqualified # 👁 E0.7 eye -#1F445 ; fully-qualified # 👅 E0.6 tongue -#1F444 ; fully-qualified # 👄 E0.6 mouth -#1FAE6 ; fully-qualified # 🫦 E14.0 biting lip -# -## subgroup: person -#1F476 ; fully-qualified # 👶 E0.6 baby -#1F476 1F3FB ; fully-qualified # 👶🏻 E1.0 baby: light skin tone -#1F476 1F3FC ; fully-qualified # 👶🏼 E1.0 baby: medium-light skin tone -#1F476 1F3FD ; fully-qualified # 👶🏽 E1.0 baby: medium skin tone -#1F476 1F3FE ; fully-qualified # 👶🏾 E1.0 baby: medium-dark skin tone -#1F476 1F3FF ; fully-qualified # 👶🏿 E1.0 baby: dark skin tone -#1F9D2 ; fully-qualified # 🧒 E5.0 child -#1F9D2 1F3FB ; fully-qualified # 🧒🏻 E5.0 child: light skin tone -#1F9D2 1F3FC ; fully-qualified # 🧒🏼 E5.0 child: medium-light skin tone -#1F9D2 1F3FD ; fully-qualified # 🧒🏽 E5.0 child: medium skin tone -#1F9D2 1F3FE ; fully-qualified # 🧒🏾 E5.0 child: medium-dark skin tone -#1F9D2 1F3FF ; fully-qualified # 🧒🏿 E5.0 child: dark skin tone -#1F466 ; fully-qualified # 👦 E0.6 boy -#1F466 1F3FB ; fully-qualified # 👦🏻 E1.0 boy: light skin tone -#1F466 1F3FC ; fully-qualified # 👦🏼 E1.0 boy: medium-light skin tone -#1F466 1F3FD ; fully-qualified # 👦🏽 E1.0 boy: medium skin tone -#1F466 1F3FE ; fully-qualified # 👦🏾 E1.0 boy: medium-dark skin tone -#1F466 1F3FF ; fully-qualified # 👦🏿 E1.0 boy: dark skin tone -#1F467 ; fully-qualified # 👧 E0.6 girl -#1F467 1F3FB ; fully-qualified # 👧🏻 E1.0 girl: light skin tone -#1F467 1F3FC ; fully-qualified # 👧🏼 E1.0 girl: medium-light skin tone -#1F467 1F3FD ; fully-qualified # 👧🏽 E1.0 girl: medium skin tone -#1F467 1F3FE ; fully-qualified # 👧🏾 E1.0 girl: medium-dark skin tone -#1F467 1F3FF ; fully-qualified # 👧🏿 E1.0 girl: dark skin tone -#1F9D1 ; fully-qualified # 🧑 E5.0 person -#1F9D1 1F3FB ; fully-qualified # 🧑🏻 E5.0 person: light skin tone -#1F9D1 1F3FC ; fully-qualified # 🧑🏼 E5.0 person: medium-light skin tone -#1F9D1 1F3FD ; fully-qualified # 🧑🏽 E5.0 person: medium skin tone -#1F9D1 1F3FE ; fully-qualified # 🧑🏾 E5.0 person: medium-dark skin tone -#1F9D1 1F3FF ; fully-qualified # 🧑🏿 E5.0 person: dark skin tone -#1F471 ; fully-qualified # 👱 E0.6 person: blond hair -#1F471 1F3FB ; fully-qualified # 👱🏻 E1.0 person: light skin tone, blond hair -#1F471 1F3FC ; fully-qualified # 👱🏼 E1.0 person: medium-light skin tone, blond hair -#1F471 1F3FD ; fully-qualified # 👱🏽 E1.0 person: medium skin tone, blond hair -#1F471 1F3FE ; fully-qualified # 👱🏾 E1.0 person: medium-dark skin tone, blond hair -#1F471 1F3FF ; fully-qualified # 👱🏿 E1.0 person: dark skin tone, blond hair -#1F468 ; fully-qualified # 👨 E0.6 man -#1F468 1F3FB ; fully-qualified # 👨🏻 E1.0 man: light skin tone -#1F468 1F3FC ; fully-qualified # 👨🏼 E1.0 man: medium-light skin tone -#1F468 1F3FD ; fully-qualified # 👨🏽 E1.0 man: medium skin tone -#1F468 1F3FE ; fully-qualified # 👨🏾 E1.0 man: medium-dark skin tone -#1F468 1F3FF ; fully-qualified # 👨🏿 E1.0 man: dark skin tone -#1F9D4 ; fully-qualified # 🧔 E5.0 person: beard -#1F9D4 1F3FB ; fully-qualified # 🧔🏻 E5.0 person: light skin tone, beard -#1F9D4 1F3FC ; fully-qualified # 🧔🏼 E5.0 person: medium-light skin tone, beard -#1F9D4 1F3FD ; fully-qualified # 🧔🏽 E5.0 person: medium skin tone, beard -#1F9D4 1F3FE ; fully-qualified # 🧔🏾 E5.0 person: medium-dark skin tone, beard -#1F9D4 1F3FF ; fully-qualified # 🧔🏿 E5.0 person: dark skin tone, beard -#1F9D4 200D 2642 FE0F ; fully-qualified # 🧔‍♂️ E13.1 man: beard -#1F9D4 200D 2642 ; minimally-qualified # 🧔‍♂ E13.1 man: beard -#1F9D4 1F3FB 200D 2642 FE0F ; fully-qualified # 🧔🏻‍♂️ E13.1 man: light skin tone, beard -#1F9D4 1F3FB 200D 2642 ; minimally-qualified # 🧔🏻‍♂ E13.1 man: light skin tone, beard -#1F9D4 1F3FC 200D 2642 FE0F ; fully-qualified # 🧔🏼‍♂️ E13.1 man: medium-light skin tone, beard -#1F9D4 1F3FC 200D 2642 ; minimally-qualified # 🧔🏼‍♂ E13.1 man: medium-light skin tone, beard -#1F9D4 1F3FD 200D 2642 FE0F ; fully-qualified # 🧔🏽‍♂️ E13.1 man: medium skin tone, beard -#1F9D4 1F3FD 200D 2642 ; minimally-qualified # 🧔🏽‍♂ E13.1 man: medium skin tone, beard -#1F9D4 1F3FE 200D 2642 FE0F ; fully-qualified # 🧔🏾‍♂️ E13.1 man: medium-dark skin tone, beard -#1F9D4 1F3FE 200D 2642 ; minimally-qualified # 🧔🏾‍♂ E13.1 man: medium-dark skin tone, beard -#1F9D4 1F3FF 200D 2642 FE0F ; fully-qualified # 🧔🏿‍♂️ E13.1 man: dark skin tone, beard -#1F9D4 1F3FF 200D 2642 ; minimally-qualified # 🧔🏿‍♂ E13.1 man: dark skin tone, beard -#1F9D4 200D 2640 FE0F ; fully-qualified # 🧔‍♀️ E13.1 woman: beard -#1F9D4 200D 2640 ; minimally-qualified # 🧔‍♀ E13.1 woman: beard -#1F9D4 1F3FB 200D 2640 FE0F ; fully-qualified # 🧔🏻‍♀️ E13.1 woman: light skin tone, beard -#1F9D4 1F3FB 200D 2640 ; minimally-qualified # 🧔🏻‍♀ E13.1 woman: light skin tone, beard -#1F9D4 1F3FC 200D 2640 FE0F ; fully-qualified # 🧔🏼‍♀️ E13.1 woman: medium-light skin tone, beard -#1F9D4 1F3FC 200D 2640 ; minimally-qualified # 🧔🏼‍♀ E13.1 woman: medium-light skin tone, beard -#1F9D4 1F3FD 200D 2640 FE0F ; fully-qualified # 🧔🏽‍♀️ E13.1 woman: medium skin tone, beard -#1F9D4 1F3FD 200D 2640 ; minimally-qualified # 🧔🏽‍♀ E13.1 woman: medium skin tone, beard -#1F9D4 1F3FE 200D 2640 FE0F ; fully-qualified # 🧔🏾‍♀️ E13.1 woman: medium-dark skin tone, beard -#1F9D4 1F3FE 200D 2640 ; minimally-qualified # 🧔🏾‍♀ E13.1 woman: medium-dark skin tone, beard -#1F9D4 1F3FF 200D 2640 FE0F ; fully-qualified # 🧔🏿‍♀️ E13.1 woman: dark skin tone, beard -#1F9D4 1F3FF 200D 2640 ; minimally-qualified # 🧔🏿‍♀ E13.1 woman: dark skin tone, beard -#1F468 200D 1F9B0 ; fully-qualified # 👨‍🦰 E11.0 man: red hair -#1F468 1F3FB 200D 1F9B0 ; fully-qualified # 👨🏻‍🦰 E11.0 man: light skin tone, red hair -#1F468 1F3FC 200D 1F9B0 ; fully-qualified # 👨🏼‍🦰 E11.0 man: medium-light skin tone, red hair -#1F468 1F3FD 200D 1F9B0 ; fully-qualified # 👨🏽‍🦰 E11.0 man: medium skin tone, red hair -#1F468 1F3FE 200D 1F9B0 ; fully-qualified # 👨🏾‍🦰 E11.0 man: medium-dark skin tone, red hair -#1F468 1F3FF 200D 1F9B0 ; fully-qualified # 👨🏿‍🦰 E11.0 man: dark skin tone, red hair -#1F468 200D 1F9B1 ; fully-qualified # 👨‍🦱 E11.0 man: curly hair -#1F468 1F3FB 200D 1F9B1 ; fully-qualified # 👨🏻‍🦱 E11.0 man: light skin tone, curly hair -#1F468 1F3FC 200D 1F9B1 ; fully-qualified # 👨🏼‍🦱 E11.0 man: medium-light skin tone, curly hair -#1F468 1F3FD 200D 1F9B1 ; fully-qualified # 👨🏽‍🦱 E11.0 man: medium skin tone, curly hair -#1F468 1F3FE 200D 1F9B1 ; fully-qualified # 👨🏾‍🦱 E11.0 man: medium-dark skin tone, curly hair -#1F468 1F3FF 200D 1F9B1 ; fully-qualified # 👨🏿‍🦱 E11.0 man: dark skin tone, curly hair -#1F468 200D 1F9B3 ; fully-qualified # 👨‍🦳 E11.0 man: white hair -#1F468 1F3FB 200D 1F9B3 ; fully-qualified # 👨🏻‍🦳 E11.0 man: light skin tone, white hair -#1F468 1F3FC 200D 1F9B3 ; fully-qualified # 👨🏼‍🦳 E11.0 man: medium-light skin tone, white hair -#1F468 1F3FD 200D 1F9B3 ; fully-qualified # 👨🏽‍🦳 E11.0 man: medium skin tone, white hair -#1F468 1F3FE 200D 1F9B3 ; fully-qualified # 👨🏾‍🦳 E11.0 man: medium-dark skin tone, white hair -#1F468 1F3FF 200D 1F9B3 ; fully-qualified # 👨🏿‍🦳 E11.0 man: dark skin tone, white hair -#1F468 200D 1F9B2 ; fully-qualified # 👨‍🦲 E11.0 man: bald -#1F468 1F3FB 200D 1F9B2 ; fully-qualified # 👨🏻‍🦲 E11.0 man: light skin tone, bald -#1F468 1F3FC 200D 1F9B2 ; fully-qualified # 👨🏼‍🦲 E11.0 man: medium-light skin tone, bald -#1F468 1F3FD 200D 1F9B2 ; fully-qualified # 👨🏽‍🦲 E11.0 man: medium skin tone, bald -#1F468 1F3FE 200D 1F9B2 ; fully-qualified # 👨🏾‍🦲 E11.0 man: medium-dark skin tone, bald -#1F468 1F3FF 200D 1F9B2 ; fully-qualified # 👨🏿‍🦲 E11.0 man: dark skin tone, bald -#1F469 ; fully-qualified # 👩 E0.6 woman -#1F469 1F3FB ; fully-qualified # 👩🏻 E1.0 woman: light skin tone -#1F469 1F3FC ; fully-qualified # 👩🏼 E1.0 woman: medium-light skin tone -#1F469 1F3FD ; fully-qualified # 👩🏽 E1.0 woman: medium skin tone -#1F469 1F3FE ; fully-qualified # 👩🏾 E1.0 woman: medium-dark skin tone -#1F469 1F3FF ; fully-qualified # 👩🏿 E1.0 woman: dark skin tone -#1F469 200D 1F9B0 ; fully-qualified # 👩‍🦰 E11.0 woman: red hair -#1F469 1F3FB 200D 1F9B0 ; fully-qualified # 👩🏻‍🦰 E11.0 woman: light skin tone, red hair -#1F469 1F3FC 200D 1F9B0 ; fully-qualified # 👩🏼‍🦰 E11.0 woman: medium-light skin tone, red hair -#1F469 1F3FD 200D 1F9B0 ; fully-qualified # 👩🏽‍🦰 E11.0 woman: medium skin tone, red hair -#1F469 1F3FE 200D 1F9B0 ; fully-qualified # 👩🏾‍🦰 E11.0 woman: medium-dark skin tone, red hair -#1F469 1F3FF 200D 1F9B0 ; fully-qualified # 👩🏿‍🦰 E11.0 woman: dark skin tone, red hair -#1F9D1 200D 1F9B0 ; fully-qualified # 🧑‍🦰 E12.1 person: red hair -#1F9D1 1F3FB 200D 1F9B0 ; fully-qualified # 🧑🏻‍🦰 E12.1 person: light skin tone, red hair -#1F9D1 1F3FC 200D 1F9B0 ; fully-qualified # 🧑🏼‍🦰 E12.1 person: medium-light skin tone, red hair -#1F9D1 1F3FD 200D 1F9B0 ; fully-qualified # 🧑🏽‍🦰 E12.1 person: medium skin tone, red hair -#1F9D1 1F3FE 200D 1F9B0 ; fully-qualified # 🧑🏾‍🦰 E12.1 person: medium-dark skin tone, red hair -#1F9D1 1F3FF 200D 1F9B0 ; fully-qualified # 🧑🏿‍🦰 E12.1 person: dark skin tone, red hair -#1F469 200D 1F9B1 ; fully-qualified # 👩‍🦱 E11.0 woman: curly hair -#1F469 1F3FB 200D 1F9B1 ; fully-qualified # 👩🏻‍🦱 E11.0 woman: light skin tone, curly hair -#1F469 1F3FC 200D 1F9B1 ; fully-qualified # 👩🏼‍🦱 E11.0 woman: medium-light skin tone, curly hair -#1F469 1F3FD 200D 1F9B1 ; fully-qualified # 👩🏽‍🦱 E11.0 woman: medium skin tone, curly hair -#1F469 1F3FE 200D 1F9B1 ; fully-qualified # 👩🏾‍🦱 E11.0 woman: medium-dark skin tone, curly hair -#1F469 1F3FF 200D 1F9B1 ; fully-qualified # 👩🏿‍🦱 E11.0 woman: dark skin tone, curly hair -#1F9D1 200D 1F9B1 ; fully-qualified # 🧑‍🦱 E12.1 person: curly hair -#1F9D1 1F3FB 200D 1F9B1 ; fully-qualified # 🧑🏻‍🦱 E12.1 person: light skin tone, curly hair -#1F9D1 1F3FC 200D 1F9B1 ; fully-qualified # 🧑🏼‍🦱 E12.1 person: medium-light skin tone, curly hair -#1F9D1 1F3FD 200D 1F9B1 ; fully-qualified # 🧑🏽‍🦱 E12.1 person: medium skin tone, curly hair -#1F9D1 1F3FE 200D 1F9B1 ; fully-qualified # 🧑🏾‍🦱 E12.1 person: medium-dark skin tone, curly hair -#1F9D1 1F3FF 200D 1F9B1 ; fully-qualified # 🧑🏿‍🦱 E12.1 person: dark skin tone, curly hair -#1F469 200D 1F9B3 ; fully-qualified # 👩‍🦳 E11.0 woman: white hair -#1F469 1F3FB 200D 1F9B3 ; fully-qualified # 👩🏻‍🦳 E11.0 woman: light skin tone, white hair -#1F469 1F3FC 200D 1F9B3 ; fully-qualified # 👩🏼‍🦳 E11.0 woman: medium-light skin tone, white hair -#1F469 1F3FD 200D 1F9B3 ; fully-qualified # 👩🏽‍🦳 E11.0 woman: medium skin tone, white hair -#1F469 1F3FE 200D 1F9B3 ; fully-qualified # 👩🏾‍🦳 E11.0 woman: medium-dark skin tone, white hair -#1F469 1F3FF 200D 1F9B3 ; fully-qualified # 👩🏿‍🦳 E11.0 woman: dark skin tone, white hair -#1F9D1 200D 1F9B3 ; fully-qualified # 🧑‍🦳 E12.1 person: white hair -#1F9D1 1F3FB 200D 1F9B3 ; fully-qualified # 🧑🏻‍🦳 E12.1 person: light skin tone, white hair -#1F9D1 1F3FC 200D 1F9B3 ; fully-qualified # 🧑🏼‍🦳 E12.1 person: medium-light skin tone, white hair -#1F9D1 1F3FD 200D 1F9B3 ; fully-qualified # 🧑🏽‍🦳 E12.1 person: medium skin tone, white hair -#1F9D1 1F3FE 200D 1F9B3 ; fully-qualified # 🧑🏾‍🦳 E12.1 person: medium-dark skin tone, white hair -#1F9D1 1F3FF 200D 1F9B3 ; fully-qualified # 🧑🏿‍🦳 E12.1 person: dark skin tone, white hair -#1F469 200D 1F9B2 ; fully-qualified # 👩‍🦲 E11.0 woman: bald -#1F469 1F3FB 200D 1F9B2 ; fully-qualified # 👩🏻‍🦲 E11.0 woman: light skin tone, bald -#1F469 1F3FC 200D 1F9B2 ; fully-qualified # 👩🏼‍🦲 E11.0 woman: medium-light skin tone, bald -#1F469 1F3FD 200D 1F9B2 ; fully-qualified # 👩🏽‍🦲 E11.0 woman: medium skin tone, bald -#1F469 1F3FE 200D 1F9B2 ; fully-qualified # 👩🏾‍🦲 E11.0 woman: medium-dark skin tone, bald -#1F469 1F3FF 200D 1F9B2 ; fully-qualified # 👩🏿‍🦲 E11.0 woman: dark skin tone, bald -#1F9D1 200D 1F9B2 ; fully-qualified # 🧑‍🦲 E12.1 person: bald -#1F9D1 1F3FB 200D 1F9B2 ; fully-qualified # 🧑🏻‍🦲 E12.1 person: light skin tone, bald -#1F9D1 1F3FC 200D 1F9B2 ; fully-qualified # 🧑🏼‍🦲 E12.1 person: medium-light skin tone, bald -#1F9D1 1F3FD 200D 1F9B2 ; fully-qualified # 🧑🏽‍🦲 E12.1 person: medium skin tone, bald -#1F9D1 1F3FE 200D 1F9B2 ; fully-qualified # 🧑🏾‍🦲 E12.1 person: medium-dark skin tone, bald -#1F9D1 1F3FF 200D 1F9B2 ; fully-qualified # 🧑🏿‍🦲 E12.1 person: dark skin tone, bald -#1F471 200D 2640 FE0F ; fully-qualified # 👱‍♀️ E4.0 woman: blond hair -#1F471 200D 2640 ; minimally-qualified # 👱‍♀ E4.0 woman: blond hair -#1F471 1F3FB 200D 2640 FE0F ; fully-qualified # 👱🏻‍♀️ E4.0 woman: light skin tone, blond hair -#1F471 1F3FB 200D 2640 ; minimally-qualified # 👱🏻‍♀ E4.0 woman: light skin tone, blond hair -#1F471 1F3FC 200D 2640 FE0F ; fully-qualified # 👱🏼‍♀️ E4.0 woman: medium-light skin tone, blond hair -#1F471 1F3FC 200D 2640 ; minimally-qualified # 👱🏼‍♀ E4.0 woman: medium-light skin tone, blond hair -#1F471 1F3FD 200D 2640 FE0F ; fully-qualified # 👱🏽‍♀️ E4.0 woman: medium skin tone, blond hair -#1F471 1F3FD 200D 2640 ; minimally-qualified # 👱🏽‍♀ E4.0 woman: medium skin tone, blond hair -#1F471 1F3FE 200D 2640 FE0F ; fully-qualified # 👱🏾‍♀️ E4.0 woman: medium-dark skin tone, blond hair -#1F471 1F3FE 200D 2640 ; minimally-qualified # 👱🏾‍♀ E4.0 woman: medium-dark skin tone, blond hair -#1F471 1F3FF 200D 2640 FE0F ; fully-qualified # 👱🏿‍♀️ E4.0 woman: dark skin tone, blond hair -#1F471 1F3FF 200D 2640 ; minimally-qualified # 👱🏿‍♀ E4.0 woman: dark skin tone, blond hair -#1F471 200D 2642 FE0F ; fully-qualified # 👱‍♂️ E4.0 man: blond hair -#1F471 200D 2642 ; minimally-qualified # 👱‍♂ E4.0 man: blond hair -#1F471 1F3FB 200D 2642 FE0F ; fully-qualified # 👱🏻‍♂️ E4.0 man: light skin tone, blond hair -#1F471 1F3FB 200D 2642 ; minimally-qualified # 👱🏻‍♂ E4.0 man: light skin tone, blond hair -#1F471 1F3FC 200D 2642 FE0F ; fully-qualified # 👱🏼‍♂️ E4.0 man: medium-light skin tone, blond hair -#1F471 1F3FC 200D 2642 ; minimally-qualified # 👱🏼‍♂ E4.0 man: medium-light skin tone, blond hair -#1F471 1F3FD 200D 2642 FE0F ; fully-qualified # 👱🏽‍♂️ E4.0 man: medium skin tone, blond hair -#1F471 1F3FD 200D 2642 ; minimally-qualified # 👱🏽‍♂ E4.0 man: medium skin tone, blond hair -#1F471 1F3FE 200D 2642 FE0F ; fully-qualified # 👱🏾‍♂️ E4.0 man: medium-dark skin tone, blond hair -#1F471 1F3FE 200D 2642 ; minimally-qualified # 👱🏾‍♂ E4.0 man: medium-dark skin tone, blond hair -#1F471 1F3FF 200D 2642 FE0F ; fully-qualified # 👱🏿‍♂️ E4.0 man: dark skin tone, blond hair -#1F471 1F3FF 200D 2642 ; minimally-qualified # 👱🏿‍♂ E4.0 man: dark skin tone, blond hair -#1F9D3 ; fully-qualified # 🧓 E5.0 older person -#1F9D3 1F3FB ; fully-qualified # 🧓🏻 E5.0 older person: light skin tone -#1F9D3 1F3FC ; fully-qualified # 🧓🏼 E5.0 older person: medium-light skin tone -#1F9D3 1F3FD ; fully-qualified # 🧓🏽 E5.0 older person: medium skin tone -#1F9D3 1F3FE ; fully-qualified # 🧓🏾 E5.0 older person: medium-dark skin tone -#1F9D3 1F3FF ; fully-qualified # 🧓🏿 E5.0 older person: dark skin tone -#1F474 ; fully-qualified # 👴 E0.6 old man -#1F474 1F3FB ; fully-qualified # 👴🏻 E1.0 old man: light skin tone -#1F474 1F3FC ; fully-qualified # 👴🏼 E1.0 old man: medium-light skin tone -#1F474 1F3FD ; fully-qualified # 👴🏽 E1.0 old man: medium skin tone -#1F474 1F3FE ; fully-qualified # 👴🏾 E1.0 old man: medium-dark skin tone -#1F474 1F3FF ; fully-qualified # 👴🏿 E1.0 old man: dark skin tone -#1F475 ; fully-qualified # 👵 E0.6 old woman -#1F475 1F3FB ; fully-qualified # 👵🏻 E1.0 old woman: light skin tone -#1F475 1F3FC ; fully-qualified # 👵🏼 E1.0 old woman: medium-light skin tone -#1F475 1F3FD ; fully-qualified # 👵🏽 E1.0 old woman: medium skin tone -#1F475 1F3FE ; fully-qualified # 👵🏾 E1.0 old woman: medium-dark skin tone -#1F475 1F3FF ; fully-qualified # 👵🏿 E1.0 old woman: dark skin tone -# -## subgroup: person-gesture -#1F64D ; fully-qualified # 🙍 E0.6 person frowning -#1F64D 1F3FB ; fully-qualified # 🙍🏻 E1.0 person frowning: light skin tone -#1F64D 1F3FC ; fully-qualified # 🙍🏼 E1.0 person frowning: medium-light skin tone -#1F64D 1F3FD ; fully-qualified # 🙍🏽 E1.0 person frowning: medium skin tone -#1F64D 1F3FE ; fully-qualified # 🙍🏾 E1.0 person frowning: medium-dark skin tone -#1F64D 1F3FF ; fully-qualified # 🙍🏿 E1.0 person frowning: dark skin tone -#1F64D 200D 2642 FE0F ; fully-qualified # 🙍‍♂️ E4.0 man frowning -#1F64D 200D 2642 ; minimally-qualified # 🙍‍♂ E4.0 man frowning -#1F64D 1F3FB 200D 2642 FE0F ; fully-qualified # 🙍🏻‍♂️ E4.0 man frowning: light skin tone -#1F64D 1F3FB 200D 2642 ; minimally-qualified # 🙍🏻‍♂ E4.0 man frowning: light skin tone -#1F64D 1F3FC 200D 2642 FE0F ; fully-qualified # 🙍🏼‍♂️ E4.0 man frowning: medium-light skin tone -#1F64D 1F3FC 200D 2642 ; minimally-qualified # 🙍🏼‍♂ E4.0 man frowning: medium-light skin tone -#1F64D 1F3FD 200D 2642 FE0F ; fully-qualified # 🙍🏽‍♂️ E4.0 man frowning: medium skin tone -#1F64D 1F3FD 200D 2642 ; minimally-qualified # 🙍🏽‍♂ E4.0 man frowning: medium skin tone -#1F64D 1F3FE 200D 2642 FE0F ; fully-qualified # 🙍🏾‍♂️ E4.0 man frowning: medium-dark skin tone -#1F64D 1F3FE 200D 2642 ; minimally-qualified # 🙍🏾‍♂ E4.0 man frowning: medium-dark skin tone -#1F64D 1F3FF 200D 2642 FE0F ; fully-qualified # 🙍🏿‍♂️ E4.0 man frowning: dark skin tone -#1F64D 1F3FF 200D 2642 ; minimally-qualified # 🙍🏿‍♂ E4.0 man frowning: dark skin tone -#1F64D 200D 2640 FE0F ; fully-qualified # 🙍‍♀️ E4.0 woman frowning -#1F64D 200D 2640 ; minimally-qualified # 🙍‍♀ E4.0 woman frowning -#1F64D 1F3FB 200D 2640 FE0F ; fully-qualified # 🙍🏻‍♀️ E4.0 woman frowning: light skin tone -#1F64D 1F3FB 200D 2640 ; minimally-qualified # 🙍🏻‍♀ E4.0 woman frowning: light skin tone -#1F64D 1F3FC 200D 2640 FE0F ; fully-qualified # 🙍🏼‍♀️ E4.0 woman frowning: medium-light skin tone -#1F64D 1F3FC 200D 2640 ; minimally-qualified # 🙍🏼‍♀ E4.0 woman frowning: medium-light skin tone -#1F64D 1F3FD 200D 2640 FE0F ; fully-qualified # 🙍🏽‍♀️ E4.0 woman frowning: medium skin tone -#1F64D 1F3FD 200D 2640 ; minimally-qualified # 🙍🏽‍♀ E4.0 woman frowning: medium skin tone -#1F64D 1F3FE 200D 2640 FE0F ; fully-qualified # 🙍🏾‍♀️ E4.0 woman frowning: medium-dark skin tone -#1F64D 1F3FE 200D 2640 ; minimally-qualified # 🙍🏾‍♀ E4.0 woman frowning: medium-dark skin tone -#1F64D 1F3FF 200D 2640 FE0F ; fully-qualified # 🙍🏿‍♀️ E4.0 woman frowning: dark skin tone -#1F64D 1F3FF 200D 2640 ; minimally-qualified # 🙍🏿‍♀ E4.0 woman frowning: dark skin tone -#1F64E ; fully-qualified # 🙎 E0.6 person pouting -#1F64E 1F3FB ; fully-qualified # 🙎🏻 E1.0 person pouting: light skin tone -#1F64E 1F3FC ; fully-qualified # 🙎🏼 E1.0 person pouting: medium-light skin tone -#1F64E 1F3FD ; fully-qualified # 🙎🏽 E1.0 person pouting: medium skin tone -#1F64E 1F3FE ; fully-qualified # 🙎🏾 E1.0 person pouting: medium-dark skin tone -#1F64E 1F3FF ; fully-qualified # 🙎🏿 E1.0 person pouting: dark skin tone -#1F64E 200D 2642 FE0F ; fully-qualified # 🙎‍♂️ E4.0 man pouting -#1F64E 200D 2642 ; minimally-qualified # 🙎‍♂ E4.0 man pouting -#1F64E 1F3FB 200D 2642 FE0F ; fully-qualified # 🙎🏻‍♂️ E4.0 man pouting: light skin tone -#1F64E 1F3FB 200D 2642 ; minimally-qualified # 🙎🏻‍♂ E4.0 man pouting: light skin tone -#1F64E 1F3FC 200D 2642 FE0F ; fully-qualified # 🙎🏼‍♂️ E4.0 man pouting: medium-light skin tone -#1F64E 1F3FC 200D 2642 ; minimally-qualified # 🙎🏼‍♂ E4.0 man pouting: medium-light skin tone -#1F64E 1F3FD 200D 2642 FE0F ; fully-qualified # 🙎🏽‍♂️ E4.0 man pouting: medium skin tone -#1F64E 1F3FD 200D 2642 ; minimally-qualified # 🙎🏽‍♂ E4.0 man pouting: medium skin tone -#1F64E 1F3FE 200D 2642 FE0F ; fully-qualified # 🙎🏾‍♂️ E4.0 man pouting: medium-dark skin tone -#1F64E 1F3FE 200D 2642 ; minimally-qualified # 🙎🏾‍♂ E4.0 man pouting: medium-dark skin tone -#1F64E 1F3FF 200D 2642 FE0F ; fully-qualified # 🙎🏿‍♂️ E4.0 man pouting: dark skin tone -#1F64E 1F3FF 200D 2642 ; minimally-qualified # 🙎🏿‍♂ E4.0 man pouting: dark skin tone -#1F64E 200D 2640 FE0F ; fully-qualified # 🙎‍♀️ E4.0 woman pouting -#1F64E 200D 2640 ; minimally-qualified # 🙎‍♀ E4.0 woman pouting -#1F64E 1F3FB 200D 2640 FE0F ; fully-qualified # 🙎🏻‍♀️ E4.0 woman pouting: light skin tone -#1F64E 1F3FB 200D 2640 ; minimally-qualified # 🙎🏻‍♀ E4.0 woman pouting: light skin tone -#1F64E 1F3FC 200D 2640 FE0F ; fully-qualified # 🙎🏼‍♀️ E4.0 woman pouting: medium-light skin tone -#1F64E 1F3FC 200D 2640 ; minimally-qualified # 🙎🏼‍♀ E4.0 woman pouting: medium-light skin tone -#1F64E 1F3FD 200D 2640 FE0F ; fully-qualified # 🙎🏽‍♀️ E4.0 woman pouting: medium skin tone -#1F64E 1F3FD 200D 2640 ; minimally-qualified # 🙎🏽‍♀ E4.0 woman pouting: medium skin tone -#1F64E 1F3FE 200D 2640 FE0F ; fully-qualified # 🙎🏾‍♀️ E4.0 woman pouting: medium-dark skin tone -#1F64E 1F3FE 200D 2640 ; minimally-qualified # 🙎🏾‍♀ E4.0 woman pouting: medium-dark skin tone -#1F64E 1F3FF 200D 2640 FE0F ; fully-qualified # 🙎🏿‍♀️ E4.0 woman pouting: dark skin tone -#1F64E 1F3FF 200D 2640 ; minimally-qualified # 🙎🏿‍♀ E4.0 woman pouting: dark skin tone -#1F645 ; fully-qualified # 🙅 E0.6 person gesturing NO -#1F645 1F3FB ; fully-qualified # 🙅🏻 E1.0 person gesturing NO: light skin tone -#1F645 1F3FC ; fully-qualified # 🙅🏼 E1.0 person gesturing NO: medium-light skin tone -#1F645 1F3FD ; fully-qualified # 🙅🏽 E1.0 person gesturing NO: medium skin tone -#1F645 1F3FE ; fully-qualified # 🙅🏾 E1.0 person gesturing NO: medium-dark skin tone -#1F645 1F3FF ; fully-qualified # 🙅🏿 E1.0 person gesturing NO: dark skin tone -#1F645 200D 2642 FE0F ; fully-qualified # 🙅‍♂️ E4.0 man gesturing NO -#1F645 200D 2642 ; minimally-qualified # 🙅‍♂ E4.0 man gesturing NO -#1F645 1F3FB 200D 2642 FE0F ; fully-qualified # 🙅🏻‍♂️ E4.0 man gesturing NO: light skin tone -#1F645 1F3FB 200D 2642 ; minimally-qualified # 🙅🏻‍♂ E4.0 man gesturing NO: light skin tone -#1F645 1F3FC 200D 2642 FE0F ; fully-qualified # 🙅🏼‍♂️ E4.0 man gesturing NO: medium-light skin tone -#1F645 1F3FC 200D 2642 ; minimally-qualified # 🙅🏼‍♂ E4.0 man gesturing NO: medium-light skin tone -#1F645 1F3FD 200D 2642 FE0F ; fully-qualified # 🙅🏽‍♂️ E4.0 man gesturing NO: medium skin tone -#1F645 1F3FD 200D 2642 ; minimally-qualified # 🙅🏽‍♂ E4.0 man gesturing NO: medium skin tone -#1F645 1F3FE 200D 2642 FE0F ; fully-qualified # 🙅🏾‍♂️ E4.0 man gesturing NO: medium-dark skin tone -#1F645 1F3FE 200D 2642 ; minimally-qualified # 🙅🏾‍♂ E4.0 man gesturing NO: medium-dark skin tone -#1F645 1F3FF 200D 2642 FE0F ; fully-qualified # 🙅🏿‍♂️ E4.0 man gesturing NO: dark skin tone -#1F645 1F3FF 200D 2642 ; minimally-qualified # 🙅🏿‍♂ E4.0 man gesturing NO: dark skin tone -#1F645 200D 2640 FE0F ; fully-qualified # 🙅‍♀️ E4.0 woman gesturing NO -#1F645 200D 2640 ; minimally-qualified # 🙅‍♀ E4.0 woman gesturing NO -#1F645 1F3FB 200D 2640 FE0F ; fully-qualified # 🙅🏻‍♀️ E4.0 woman gesturing NO: light skin tone -#1F645 1F3FB 200D 2640 ; minimally-qualified # 🙅🏻‍♀ E4.0 woman gesturing NO: light skin tone -#1F645 1F3FC 200D 2640 FE0F ; fully-qualified # 🙅🏼‍♀️ E4.0 woman gesturing NO: medium-light skin tone -#1F645 1F3FC 200D 2640 ; minimally-qualified # 🙅🏼‍♀ E4.0 woman gesturing NO: medium-light skin tone -#1F645 1F3FD 200D 2640 FE0F ; fully-qualified # 🙅🏽‍♀️ E4.0 woman gesturing NO: medium skin tone -#1F645 1F3FD 200D 2640 ; minimally-qualified # 🙅🏽‍♀ E4.0 woman gesturing NO: medium skin tone -#1F645 1F3FE 200D 2640 FE0F ; fully-qualified # 🙅🏾‍♀️ E4.0 woman gesturing NO: medium-dark skin tone -#1F645 1F3FE 200D 2640 ; minimally-qualified # 🙅🏾‍♀ E4.0 woman gesturing NO: medium-dark skin tone -#1F645 1F3FF 200D 2640 FE0F ; fully-qualified # 🙅🏿‍♀️ E4.0 woman gesturing NO: dark skin tone -#1F645 1F3FF 200D 2640 ; minimally-qualified # 🙅🏿‍♀ E4.0 woman gesturing NO: dark skin tone -#1F646 ; fully-qualified # 🙆 E0.6 person gesturing OK -#1F646 1F3FB ; fully-qualified # 🙆🏻 E1.0 person gesturing OK: light skin tone -#1F646 1F3FC ; fully-qualified # 🙆🏼 E1.0 person gesturing OK: medium-light skin tone -#1F646 1F3FD ; fully-qualified # 🙆🏽 E1.0 person gesturing OK: medium skin tone -#1F646 1F3FE ; fully-qualified # 🙆🏾 E1.0 person gesturing OK: medium-dark skin tone -#1F646 1F3FF ; fully-qualified # 🙆🏿 E1.0 person gesturing OK: dark skin tone -#1F646 200D 2642 FE0F ; fully-qualified # 🙆‍♂️ E4.0 man gesturing OK -#1F646 200D 2642 ; minimally-qualified # 🙆‍♂ E4.0 man gesturing OK -#1F646 1F3FB 200D 2642 FE0F ; fully-qualified # 🙆🏻‍♂️ E4.0 man gesturing OK: light skin tone -#1F646 1F3FB 200D 2642 ; minimally-qualified # 🙆🏻‍♂ E4.0 man gesturing OK: light skin tone -#1F646 1F3FC 200D 2642 FE0F ; fully-qualified # 🙆🏼‍♂️ E4.0 man gesturing OK: medium-light skin tone -#1F646 1F3FC 200D 2642 ; minimally-qualified # 🙆🏼‍♂ E4.0 man gesturing OK: medium-light skin tone -#1F646 1F3FD 200D 2642 FE0F ; fully-qualified # 🙆🏽‍♂️ E4.0 man gesturing OK: medium skin tone -#1F646 1F3FD 200D 2642 ; minimally-qualified # 🙆🏽‍♂ E4.0 man gesturing OK: medium skin tone -#1F646 1F3FE 200D 2642 FE0F ; fully-qualified # 🙆🏾‍♂️ E4.0 man gesturing OK: medium-dark skin tone -#1F646 1F3FE 200D 2642 ; minimally-qualified # 🙆🏾‍♂ E4.0 man gesturing OK: medium-dark skin tone -#1F646 1F3FF 200D 2642 FE0F ; fully-qualified # 🙆🏿‍♂️ E4.0 man gesturing OK: dark skin tone -#1F646 1F3FF 200D 2642 ; minimally-qualified # 🙆🏿‍♂ E4.0 man gesturing OK: dark skin tone -#1F646 200D 2640 FE0F ; fully-qualified # 🙆‍♀️ E4.0 woman gesturing OK -#1F646 200D 2640 ; minimally-qualified # 🙆‍♀ E4.0 woman gesturing OK -#1F646 1F3FB 200D 2640 FE0F ; fully-qualified # 🙆🏻‍♀️ E4.0 woman gesturing OK: light skin tone -#1F646 1F3FB 200D 2640 ; minimally-qualified # 🙆🏻‍♀ E4.0 woman gesturing OK: light skin tone -#1F646 1F3FC 200D 2640 FE0F ; fully-qualified # 🙆🏼‍♀️ E4.0 woman gesturing OK: medium-light skin tone -#1F646 1F3FC 200D 2640 ; minimally-qualified # 🙆🏼‍♀ E4.0 woman gesturing OK: medium-light skin tone -#1F646 1F3FD 200D 2640 FE0F ; fully-qualified # 🙆🏽‍♀️ E4.0 woman gesturing OK: medium skin tone -#1F646 1F3FD 200D 2640 ; minimally-qualified # 🙆🏽‍♀ E4.0 woman gesturing OK: medium skin tone -#1F646 1F3FE 200D 2640 FE0F ; fully-qualified # 🙆🏾‍♀️ E4.0 woman gesturing OK: medium-dark skin tone -#1F646 1F3FE 200D 2640 ; minimally-qualified # 🙆🏾‍♀ E4.0 woman gesturing OK: medium-dark skin tone -#1F646 1F3FF 200D 2640 FE0F ; fully-qualified # 🙆🏿‍♀️ E4.0 woman gesturing OK: dark skin tone -#1F646 1F3FF 200D 2640 ; minimally-qualified # 🙆🏿‍♀ E4.0 woman gesturing OK: dark skin tone -#1F481 ; fully-qualified # 💁 E0.6 person tipping hand -#1F481 1F3FB ; fully-qualified # 💁🏻 E1.0 person tipping hand: light skin tone -#1F481 1F3FC ; fully-qualified # 💁🏼 E1.0 person tipping hand: medium-light skin tone -#1F481 1F3FD ; fully-qualified # 💁🏽 E1.0 person tipping hand: medium skin tone -#1F481 1F3FE ; fully-qualified # 💁🏾 E1.0 person tipping hand: medium-dark skin tone -#1F481 1F3FF ; fully-qualified # 💁🏿 E1.0 person tipping hand: dark skin tone -#1F481 200D 2642 FE0F ; fully-qualified # 💁‍♂️ E4.0 man tipping hand -#1F481 200D 2642 ; minimally-qualified # 💁‍♂ E4.0 man tipping hand -#1F481 1F3FB 200D 2642 FE0F ; fully-qualified # 💁🏻‍♂️ E4.0 man tipping hand: light skin tone -#1F481 1F3FB 200D 2642 ; minimally-qualified # 💁🏻‍♂ E4.0 man tipping hand: light skin tone -#1F481 1F3FC 200D 2642 FE0F ; fully-qualified # 💁🏼‍♂️ E4.0 man tipping hand: medium-light skin tone -#1F481 1F3FC 200D 2642 ; minimally-qualified # 💁🏼‍♂ E4.0 man tipping hand: medium-light skin tone -#1F481 1F3FD 200D 2642 FE0F ; fully-qualified # 💁🏽‍♂️ E4.0 man tipping hand: medium skin tone -#1F481 1F3FD 200D 2642 ; minimally-qualified # 💁🏽‍♂ E4.0 man tipping hand: medium skin tone -#1F481 1F3FE 200D 2642 FE0F ; fully-qualified # 💁🏾‍♂️ E4.0 man tipping hand: medium-dark skin tone -#1F481 1F3FE 200D 2642 ; minimally-qualified # 💁🏾‍♂ E4.0 man tipping hand: medium-dark skin tone -#1F481 1F3FF 200D 2642 FE0F ; fully-qualified # 💁🏿‍♂️ E4.0 man tipping hand: dark skin tone -#1F481 1F3FF 200D 2642 ; minimally-qualified # 💁🏿‍♂ E4.0 man tipping hand: dark skin tone -#1F481 200D 2640 FE0F ; fully-qualified # 💁‍♀️ E4.0 woman tipping hand -#1F481 200D 2640 ; minimally-qualified # 💁‍♀ E4.0 woman tipping hand -#1F481 1F3FB 200D 2640 FE0F ; fully-qualified # 💁🏻‍♀️ E4.0 woman tipping hand: light skin tone -#1F481 1F3FB 200D 2640 ; minimally-qualified # 💁🏻‍♀ E4.0 woman tipping hand: light skin tone -#1F481 1F3FC 200D 2640 FE0F ; fully-qualified # 💁🏼‍♀️ E4.0 woman tipping hand: medium-light skin tone -#1F481 1F3FC 200D 2640 ; minimally-qualified # 💁🏼‍♀ E4.0 woman tipping hand: medium-light skin tone -#1F481 1F3FD 200D 2640 FE0F ; fully-qualified # 💁🏽‍♀️ E4.0 woman tipping hand: medium skin tone -#1F481 1F3FD 200D 2640 ; minimally-qualified # 💁🏽‍♀ E4.0 woman tipping hand: medium skin tone -#1F481 1F3FE 200D 2640 FE0F ; fully-qualified # 💁🏾‍♀️ E4.0 woman tipping hand: medium-dark skin tone -#1F481 1F3FE 200D 2640 ; minimally-qualified # 💁🏾‍♀ E4.0 woman tipping hand: medium-dark skin tone -#1F481 1F3FF 200D 2640 FE0F ; fully-qualified # 💁🏿‍♀️ E4.0 woman tipping hand: dark skin tone -#1F481 1F3FF 200D 2640 ; minimally-qualified # 💁🏿‍♀ E4.0 woman tipping hand: dark skin tone -#1F64B ; fully-qualified # 🙋 E0.6 person raising hand -#1F64B 1F3FB ; fully-qualified # 🙋🏻 E1.0 person raising hand: light skin tone -#1F64B 1F3FC ; fully-qualified # 🙋🏼 E1.0 person raising hand: medium-light skin tone -#1F64B 1F3FD ; fully-qualified # 🙋🏽 E1.0 person raising hand: medium skin tone -#1F64B 1F3FE ; fully-qualified # 🙋🏾 E1.0 person raising hand: medium-dark skin tone -#1F64B 1F3FF ; fully-qualified # 🙋🏿 E1.0 person raising hand: dark skin tone -#1F64B 200D 2642 FE0F ; fully-qualified # 🙋‍♂️ E4.0 man raising hand -#1F64B 200D 2642 ; minimally-qualified # 🙋‍♂ E4.0 man raising hand -#1F64B 1F3FB 200D 2642 FE0F ; fully-qualified # 🙋🏻‍♂️ E4.0 man raising hand: light skin tone -#1F64B 1F3FB 200D 2642 ; minimally-qualified # 🙋🏻‍♂ E4.0 man raising hand: light skin tone -#1F64B 1F3FC 200D 2642 FE0F ; fully-qualified # 🙋🏼‍♂️ E4.0 man raising hand: medium-light skin tone -#1F64B 1F3FC 200D 2642 ; minimally-qualified # 🙋🏼‍♂ E4.0 man raising hand: medium-light skin tone -#1F64B 1F3FD 200D 2642 FE0F ; fully-qualified # 🙋🏽‍♂️ E4.0 man raising hand: medium skin tone -#1F64B 1F3FD 200D 2642 ; minimally-qualified # 🙋🏽‍♂ E4.0 man raising hand: medium skin tone -#1F64B 1F3FE 200D 2642 FE0F ; fully-qualified # 🙋🏾‍♂️ E4.0 man raising hand: medium-dark skin tone -#1F64B 1F3FE 200D 2642 ; minimally-qualified # 🙋🏾‍♂ E4.0 man raising hand: medium-dark skin tone -#1F64B 1F3FF 200D 2642 FE0F ; fully-qualified # 🙋🏿‍♂️ E4.0 man raising hand: dark skin tone -#1F64B 1F3FF 200D 2642 ; minimally-qualified # 🙋🏿‍♂ E4.0 man raising hand: dark skin tone -#1F64B 200D 2640 FE0F ; fully-qualified # 🙋‍♀️ E4.0 woman raising hand -#1F64B 200D 2640 ; minimally-qualified # 🙋‍♀ E4.0 woman raising hand -#1F64B 1F3FB 200D 2640 FE0F ; fully-qualified # 🙋🏻‍♀️ E4.0 woman raising hand: light skin tone -#1F64B 1F3FB 200D 2640 ; minimally-qualified # 🙋🏻‍♀ E4.0 woman raising hand: light skin tone -#1F64B 1F3FC 200D 2640 FE0F ; fully-qualified # 🙋🏼‍♀️ E4.0 woman raising hand: medium-light skin tone -#1F64B 1F3FC 200D 2640 ; minimally-qualified # 🙋🏼‍♀ E4.0 woman raising hand: medium-light skin tone -#1F64B 1F3FD 200D 2640 FE0F ; fully-qualified # 🙋🏽‍♀️ E4.0 woman raising hand: medium skin tone -#1F64B 1F3FD 200D 2640 ; minimally-qualified # 🙋🏽‍♀ E4.0 woman raising hand: medium skin tone -#1F64B 1F3FE 200D 2640 FE0F ; fully-qualified # 🙋🏾‍♀️ E4.0 woman raising hand: medium-dark skin tone -#1F64B 1F3FE 200D 2640 ; minimally-qualified # 🙋🏾‍♀ E4.0 woman raising hand: medium-dark skin tone -#1F64B 1F3FF 200D 2640 FE0F ; fully-qualified # 🙋🏿‍♀️ E4.0 woman raising hand: dark skin tone -#1F64B 1F3FF 200D 2640 ; minimally-qualified # 🙋🏿‍♀ E4.0 woman raising hand: dark skin tone -#1F9CF ; fully-qualified # 🧏 E12.0 deaf person -#1F9CF 1F3FB ; fully-qualified # 🧏🏻 E12.0 deaf person: light skin tone -#1F9CF 1F3FC ; fully-qualified # 🧏🏼 E12.0 deaf person: medium-light skin tone -#1F9CF 1F3FD ; fully-qualified # 🧏🏽 E12.0 deaf person: medium skin tone -#1F9CF 1F3FE ; fully-qualified # 🧏🏾 E12.0 deaf person: medium-dark skin tone -#1F9CF 1F3FF ; fully-qualified # 🧏🏿 E12.0 deaf person: dark skin tone -#1F9CF 200D 2642 FE0F ; fully-qualified # 🧏‍♂️ E12.0 deaf man -#1F9CF 200D 2642 ; minimally-qualified # 🧏‍♂ E12.0 deaf man -#1F9CF 1F3FB 200D 2642 FE0F ; fully-qualified # 🧏🏻‍♂️ E12.0 deaf man: light skin tone -#1F9CF 1F3FB 200D 2642 ; minimally-qualified # 🧏🏻‍♂ E12.0 deaf man: light skin tone -#1F9CF 1F3FC 200D 2642 FE0F ; fully-qualified # 🧏🏼‍♂️ E12.0 deaf man: medium-light skin tone -#1F9CF 1F3FC 200D 2642 ; minimally-qualified # 🧏🏼‍♂ E12.0 deaf man: medium-light skin tone -#1F9CF 1F3FD 200D 2642 FE0F ; fully-qualified # 🧏🏽‍♂️ E12.0 deaf man: medium skin tone -#1F9CF 1F3FD 200D 2642 ; minimally-qualified # 🧏🏽‍♂ E12.0 deaf man: medium skin tone -#1F9CF 1F3FE 200D 2642 FE0F ; fully-qualified # 🧏🏾‍♂️ E12.0 deaf man: medium-dark skin tone -#1F9CF 1F3FE 200D 2642 ; minimally-qualified # 🧏🏾‍♂ E12.0 deaf man: medium-dark skin tone -#1F9CF 1F3FF 200D 2642 FE0F ; fully-qualified # 🧏🏿‍♂️ E12.0 deaf man: dark skin tone -#1F9CF 1F3FF 200D 2642 ; minimally-qualified # 🧏🏿‍♂ E12.0 deaf man: dark skin tone -#1F9CF 200D 2640 FE0F ; fully-qualified # 🧏‍♀️ E12.0 deaf woman -#1F9CF 200D 2640 ; minimally-qualified # 🧏‍♀ E12.0 deaf woman -#1F9CF 1F3FB 200D 2640 FE0F ; fully-qualified # 🧏🏻‍♀️ E12.0 deaf woman: light skin tone -#1F9CF 1F3FB 200D 2640 ; minimally-qualified # 🧏🏻‍♀ E12.0 deaf woman: light skin tone -#1F9CF 1F3FC 200D 2640 FE0F ; fully-qualified # 🧏🏼‍♀️ E12.0 deaf woman: medium-light skin tone -#1F9CF 1F3FC 200D 2640 ; minimally-qualified # 🧏🏼‍♀ E12.0 deaf woman: medium-light skin tone -#1F9CF 1F3FD 200D 2640 FE0F ; fully-qualified # 🧏🏽‍♀️ E12.0 deaf woman: medium skin tone -#1F9CF 1F3FD 200D 2640 ; minimally-qualified # 🧏🏽‍♀ E12.0 deaf woman: medium skin tone -#1F9CF 1F3FE 200D 2640 FE0F ; fully-qualified # 🧏🏾‍♀️ E12.0 deaf woman: medium-dark skin tone -#1F9CF 1F3FE 200D 2640 ; minimally-qualified # 🧏🏾‍♀ E12.0 deaf woman: medium-dark skin tone -#1F9CF 1F3FF 200D 2640 FE0F ; fully-qualified # 🧏🏿‍♀️ E12.0 deaf woman: dark skin tone -#1F9CF 1F3FF 200D 2640 ; minimally-qualified # 🧏🏿‍♀ E12.0 deaf woman: dark skin tone -#1F647 ; fully-qualified # 🙇 E0.6 person bowing -#1F647 1F3FB ; fully-qualified # 🙇🏻 E1.0 person bowing: light skin tone -#1F647 1F3FC ; fully-qualified # 🙇🏼 E1.0 person bowing: medium-light skin tone -#1F647 1F3FD ; fully-qualified # 🙇🏽 E1.0 person bowing: medium skin tone -#1F647 1F3FE ; fully-qualified # 🙇🏾 E1.0 person bowing: medium-dark skin tone -#1F647 1F3FF ; fully-qualified # 🙇🏿 E1.0 person bowing: dark skin tone -#1F647 200D 2642 FE0F ; fully-qualified # 🙇‍♂️ E4.0 man bowing -#1F647 200D 2642 ; minimally-qualified # 🙇‍♂ E4.0 man bowing -#1F647 1F3FB 200D 2642 FE0F ; fully-qualified # 🙇🏻‍♂️ E4.0 man bowing: light skin tone -#1F647 1F3FB 200D 2642 ; minimally-qualified # 🙇🏻‍♂ E4.0 man bowing: light skin tone -#1F647 1F3FC 200D 2642 FE0F ; fully-qualified # 🙇🏼‍♂️ E4.0 man bowing: medium-light skin tone -#1F647 1F3FC 200D 2642 ; minimally-qualified # 🙇🏼‍♂ E4.0 man bowing: medium-light skin tone -#1F647 1F3FD 200D 2642 FE0F ; fully-qualified # 🙇🏽‍♂️ E4.0 man bowing: medium skin tone -#1F647 1F3FD 200D 2642 ; minimally-qualified # 🙇🏽‍♂ E4.0 man bowing: medium skin tone -#1F647 1F3FE 200D 2642 FE0F ; fully-qualified # 🙇🏾‍♂️ E4.0 man bowing: medium-dark skin tone -#1F647 1F3FE 200D 2642 ; minimally-qualified # 🙇🏾‍♂ E4.0 man bowing: medium-dark skin tone -#1F647 1F3FF 200D 2642 FE0F ; fully-qualified # 🙇🏿‍♂️ E4.0 man bowing: dark skin tone -#1F647 1F3FF 200D 2642 ; minimally-qualified # 🙇🏿‍♂ E4.0 man bowing: dark skin tone -#1F647 200D 2640 FE0F ; fully-qualified # 🙇‍♀️ E4.0 woman bowing -#1F647 200D 2640 ; minimally-qualified # 🙇‍♀ E4.0 woman bowing -#1F647 1F3FB 200D 2640 FE0F ; fully-qualified # 🙇🏻‍♀️ E4.0 woman bowing: light skin tone -#1F647 1F3FB 200D 2640 ; minimally-qualified # 🙇🏻‍♀ E4.0 woman bowing: light skin tone -#1F647 1F3FC 200D 2640 FE0F ; fully-qualified # 🙇🏼‍♀️ E4.0 woman bowing: medium-light skin tone -#1F647 1F3FC 200D 2640 ; minimally-qualified # 🙇🏼‍♀ E4.0 woman bowing: medium-light skin tone -#1F647 1F3FD 200D 2640 FE0F ; fully-qualified # 🙇🏽‍♀️ E4.0 woman bowing: medium skin tone -#1F647 1F3FD 200D 2640 ; minimally-qualified # 🙇🏽‍♀ E4.0 woman bowing: medium skin tone -#1F647 1F3FE 200D 2640 FE0F ; fully-qualified # 🙇🏾‍♀️ E4.0 woman bowing: medium-dark skin tone -#1F647 1F3FE 200D 2640 ; minimally-qualified # 🙇🏾‍♀ E4.0 woman bowing: medium-dark skin tone -#1F647 1F3FF 200D 2640 FE0F ; fully-qualified # 🙇🏿‍♀️ E4.0 woman bowing: dark skin tone -#1F647 1F3FF 200D 2640 ; minimally-qualified # 🙇🏿‍♀ E4.0 woman bowing: dark skin tone -#1F926 ; fully-qualified # 🤦 E3.0 person facepalming -#1F926 1F3FB ; fully-qualified # 🤦🏻 E3.0 person facepalming: light skin tone -#1F926 1F3FC ; fully-qualified # 🤦🏼 E3.0 person facepalming: medium-light skin tone -#1F926 1F3FD ; fully-qualified # 🤦🏽 E3.0 person facepalming: medium skin tone -#1F926 1F3FE ; fully-qualified # 🤦🏾 E3.0 person facepalming: medium-dark skin tone -#1F926 1F3FF ; fully-qualified # 🤦🏿 E3.0 person facepalming: dark skin tone -#1F926 200D 2642 FE0F ; fully-qualified # 🤦‍♂️ E4.0 man facepalming -#1F926 200D 2642 ; minimally-qualified # 🤦‍♂ E4.0 man facepalming -#1F926 1F3FB 200D 2642 FE0F ; fully-qualified # 🤦🏻‍♂️ E4.0 man facepalming: light skin tone -#1F926 1F3FB 200D 2642 ; minimally-qualified # 🤦🏻‍♂ E4.0 man facepalming: light skin tone -#1F926 1F3FC 200D 2642 FE0F ; fully-qualified # 🤦🏼‍♂️ E4.0 man facepalming: medium-light skin tone -#1F926 1F3FC 200D 2642 ; minimally-qualified # 🤦🏼‍♂ E4.0 man facepalming: medium-light skin tone -#1F926 1F3FD 200D 2642 FE0F ; fully-qualified # 🤦🏽‍♂️ E4.0 man facepalming: medium skin tone -#1F926 1F3FD 200D 2642 ; minimally-qualified # 🤦🏽‍♂ E4.0 man facepalming: medium skin tone -#1F926 1F3FE 200D 2642 FE0F ; fully-qualified # 🤦🏾‍♂️ E4.0 man facepalming: medium-dark skin tone -#1F926 1F3FE 200D 2642 ; minimally-qualified # 🤦🏾‍♂ E4.0 man facepalming: medium-dark skin tone -#1F926 1F3FF 200D 2642 FE0F ; fully-qualified # 🤦🏿‍♂️ E4.0 man facepalming: dark skin tone -#1F926 1F3FF 200D 2642 ; minimally-qualified # 🤦🏿‍♂ E4.0 man facepalming: dark skin tone -#1F926 200D 2640 FE0F ; fully-qualified # 🤦‍♀️ E4.0 woman facepalming -#1F926 200D 2640 ; minimally-qualified # 🤦‍♀ E4.0 woman facepalming -#1F926 1F3FB 200D 2640 FE0F ; fully-qualified # 🤦🏻‍♀️ E4.0 woman facepalming: light skin tone -#1F926 1F3FB 200D 2640 ; minimally-qualified # 🤦🏻‍♀ E4.0 woman facepalming: light skin tone -#1F926 1F3FC 200D 2640 FE0F ; fully-qualified # 🤦🏼‍♀️ E4.0 woman facepalming: medium-light skin tone -#1F926 1F3FC 200D 2640 ; minimally-qualified # 🤦🏼‍♀ E4.0 woman facepalming: medium-light skin tone -#1F926 1F3FD 200D 2640 FE0F ; fully-qualified # 🤦🏽‍♀️ E4.0 woman facepalming: medium skin tone -#1F926 1F3FD 200D 2640 ; minimally-qualified # 🤦🏽‍♀ E4.0 woman facepalming: medium skin tone -#1F926 1F3FE 200D 2640 FE0F ; fully-qualified # 🤦🏾‍♀️ E4.0 woman facepalming: medium-dark skin tone -#1F926 1F3FE 200D 2640 ; minimally-qualified # 🤦🏾‍♀ E4.0 woman facepalming: medium-dark skin tone -#1F926 1F3FF 200D 2640 FE0F ; fully-qualified # 🤦🏿‍♀️ E4.0 woman facepalming: dark skin tone -#1F926 1F3FF 200D 2640 ; minimally-qualified # 🤦🏿‍♀ E4.0 woman facepalming: dark skin tone -#1F937 ; fully-qualified # 🤷 E3.0 person shrugging -#1F937 1F3FB ; fully-qualified # 🤷🏻 E3.0 person shrugging: light skin tone -#1F937 1F3FC ; fully-qualified # 🤷🏼 E3.0 person shrugging: medium-light skin tone -#1F937 1F3FD ; fully-qualified # 🤷🏽 E3.0 person shrugging: medium skin tone -#1F937 1F3FE ; fully-qualified # 🤷🏾 E3.0 person shrugging: medium-dark skin tone -#1F937 1F3FF ; fully-qualified # 🤷🏿 E3.0 person shrugging: dark skin tone -#1F937 200D 2642 FE0F ; fully-qualified # 🤷‍♂️ E4.0 man shrugging -#1F937 200D 2642 ; minimally-qualified # 🤷‍♂ E4.0 man shrugging -#1F937 1F3FB 200D 2642 FE0F ; fully-qualified # 🤷🏻‍♂️ E4.0 man shrugging: light skin tone -#1F937 1F3FB 200D 2642 ; minimally-qualified # 🤷🏻‍♂ E4.0 man shrugging: light skin tone -#1F937 1F3FC 200D 2642 FE0F ; fully-qualified # 🤷🏼‍♂️ E4.0 man shrugging: medium-light skin tone -#1F937 1F3FC 200D 2642 ; minimally-qualified # 🤷🏼‍♂ E4.0 man shrugging: medium-light skin tone -#1F937 1F3FD 200D 2642 FE0F ; fully-qualified # 🤷🏽‍♂️ E4.0 man shrugging: medium skin tone -#1F937 1F3FD 200D 2642 ; minimally-qualified # 🤷🏽‍♂ E4.0 man shrugging: medium skin tone -#1F937 1F3FE 200D 2642 FE0F ; fully-qualified # 🤷🏾‍♂️ E4.0 man shrugging: medium-dark skin tone -#1F937 1F3FE 200D 2642 ; minimally-qualified # 🤷🏾‍♂ E4.0 man shrugging: medium-dark skin tone -#1F937 1F3FF 200D 2642 FE0F ; fully-qualified # 🤷🏿‍♂️ E4.0 man shrugging: dark skin tone -#1F937 1F3FF 200D 2642 ; minimally-qualified # 🤷🏿‍♂ E4.0 man shrugging: dark skin tone -#1F937 200D 2640 FE0F ; fully-qualified # 🤷‍♀️ E4.0 woman shrugging -#1F937 200D 2640 ; minimally-qualified # 🤷‍♀ E4.0 woman shrugging -#1F937 1F3FB 200D 2640 FE0F ; fully-qualified # 🤷🏻‍♀️ E4.0 woman shrugging: light skin tone -#1F937 1F3FB 200D 2640 ; minimally-qualified # 🤷🏻‍♀ E4.0 woman shrugging: light skin tone -#1F937 1F3FC 200D 2640 FE0F ; fully-qualified # 🤷🏼‍♀️ E4.0 woman shrugging: medium-light skin tone -#1F937 1F3FC 200D 2640 ; minimally-qualified # 🤷🏼‍♀ E4.0 woman shrugging: medium-light skin tone -#1F937 1F3FD 200D 2640 FE0F ; fully-qualified # 🤷🏽‍♀️ E4.0 woman shrugging: medium skin tone -#1F937 1F3FD 200D 2640 ; minimally-qualified # 🤷🏽‍♀ E4.0 woman shrugging: medium skin tone -#1F937 1F3FE 200D 2640 FE0F ; fully-qualified # 🤷🏾‍♀️ E4.0 woman shrugging: medium-dark skin tone -#1F937 1F3FE 200D 2640 ; minimally-qualified # 🤷🏾‍♀ E4.0 woman shrugging: medium-dark skin tone -#1F937 1F3FF 200D 2640 FE0F ; fully-qualified # 🤷🏿‍♀️ E4.0 woman shrugging: dark skin tone -#1F937 1F3FF 200D 2640 ; minimally-qualified # 🤷🏿‍♀ E4.0 woman shrugging: dark skin tone -# -## subgroup: person-role -#1F9D1 200D 2695 FE0F ; fully-qualified # 🧑‍⚕️ E12.1 health worker -#1F9D1 200D 2695 ; minimally-qualified # 🧑‍⚕ E12.1 health worker -#1F9D1 1F3FB 200D 2695 FE0F ; fully-qualified # 🧑🏻‍⚕️ E12.1 health worker: light skin tone -#1F9D1 1F3FB 200D 2695 ; minimally-qualified # 🧑🏻‍⚕ E12.1 health worker: light skin tone -#1F9D1 1F3FC 200D 2695 FE0F ; fully-qualified # 🧑🏼‍⚕️ E12.1 health worker: medium-light skin tone -#1F9D1 1F3FC 200D 2695 ; minimally-qualified # 🧑🏼‍⚕ E12.1 health worker: medium-light skin tone -#1F9D1 1F3FD 200D 2695 FE0F ; fully-qualified # 🧑🏽‍⚕️ E12.1 health worker: medium skin tone -#1F9D1 1F3FD 200D 2695 ; minimally-qualified # 🧑🏽‍⚕ E12.1 health worker: medium skin tone -#1F9D1 1F3FE 200D 2695 FE0F ; fully-qualified # 🧑🏾‍⚕️ E12.1 health worker: medium-dark skin tone -#1F9D1 1F3FE 200D 2695 ; minimally-qualified # 🧑🏾‍⚕ E12.1 health worker: medium-dark skin tone -#1F9D1 1F3FF 200D 2695 FE0F ; fully-qualified # 🧑🏿‍⚕️ E12.1 health worker: dark skin tone -#1F9D1 1F3FF 200D 2695 ; minimally-qualified # 🧑🏿‍⚕ E12.1 health worker: dark skin tone -#1F468 200D 2695 FE0F ; fully-qualified # 👨‍⚕️ E4.0 man health worker -#1F468 200D 2695 ; minimally-qualified # 👨‍⚕ E4.0 man health worker -#1F468 1F3FB 200D 2695 FE0F ; fully-qualified # 👨🏻‍⚕️ E4.0 man health worker: light skin tone -#1F468 1F3FB 200D 2695 ; minimally-qualified # 👨🏻‍⚕ E4.0 man health worker: light skin tone -#1F468 1F3FC 200D 2695 FE0F ; fully-qualified # 👨🏼‍⚕️ E4.0 man health worker: medium-light skin tone -#1F468 1F3FC 200D 2695 ; minimally-qualified # 👨🏼‍⚕ E4.0 man health worker: medium-light skin tone -#1F468 1F3FD 200D 2695 FE0F ; fully-qualified # 👨🏽‍⚕️ E4.0 man health worker: medium skin tone -#1F468 1F3FD 200D 2695 ; minimally-qualified # 👨🏽‍⚕ E4.0 man health worker: medium skin tone -#1F468 1F3FE 200D 2695 FE0F ; fully-qualified # 👨🏾‍⚕️ E4.0 man health worker: medium-dark skin tone -#1F468 1F3FE 200D 2695 ; minimally-qualified # 👨🏾‍⚕ E4.0 man health worker: medium-dark skin tone -#1F468 1F3FF 200D 2695 FE0F ; fully-qualified # 👨🏿‍⚕️ E4.0 man health worker: dark skin tone -#1F468 1F3FF 200D 2695 ; minimally-qualified # 👨🏿‍⚕ E4.0 man health worker: dark skin tone -#1F469 200D 2695 FE0F ; fully-qualified # 👩‍⚕️ E4.0 woman health worker -#1F469 200D 2695 ; minimally-qualified # 👩‍⚕ E4.0 woman health worker -#1F469 1F3FB 200D 2695 FE0F ; fully-qualified # 👩🏻‍⚕️ E4.0 woman health worker: light skin tone -#1F469 1F3FB 200D 2695 ; minimally-qualified # 👩🏻‍⚕ E4.0 woman health worker: light skin tone -#1F469 1F3FC 200D 2695 FE0F ; fully-qualified # 👩🏼‍⚕️ E4.0 woman health worker: medium-light skin tone -#1F469 1F3FC 200D 2695 ; minimally-qualified # 👩🏼‍⚕ E4.0 woman health worker: medium-light skin tone -#1F469 1F3FD 200D 2695 FE0F ; fully-qualified # 👩🏽‍⚕️ E4.0 woman health worker: medium skin tone -#1F469 1F3FD 200D 2695 ; minimally-qualified # 👩🏽‍⚕ E4.0 woman health worker: medium skin tone -#1F469 1F3FE 200D 2695 FE0F ; fully-qualified # 👩🏾‍⚕️ E4.0 woman health worker: medium-dark skin tone -#1F469 1F3FE 200D 2695 ; minimally-qualified # 👩🏾‍⚕ E4.0 woman health worker: medium-dark skin tone -#1F469 1F3FF 200D 2695 FE0F ; fully-qualified # 👩🏿‍⚕️ E4.0 woman health worker: dark skin tone -#1F469 1F3FF 200D 2695 ; minimally-qualified # 👩🏿‍⚕ E4.0 woman health worker: dark skin tone -#1F9D1 200D 1F393 ; fully-qualified # 🧑‍🎓 E12.1 student -#1F9D1 1F3FB 200D 1F393 ; fully-qualified # 🧑🏻‍🎓 E12.1 student: light skin tone -#1F9D1 1F3FC 200D 1F393 ; fully-qualified # 🧑🏼‍🎓 E12.1 student: medium-light skin tone -#1F9D1 1F3FD 200D 1F393 ; fully-qualified # 🧑🏽‍🎓 E12.1 student: medium skin tone -#1F9D1 1F3FE 200D 1F393 ; fully-qualified # 🧑🏾‍🎓 E12.1 student: medium-dark skin tone -#1F9D1 1F3FF 200D 1F393 ; fully-qualified # 🧑🏿‍🎓 E12.1 student: dark skin tone -#1F468 200D 1F393 ; fully-qualified # 👨‍🎓 E4.0 man student -#1F468 1F3FB 200D 1F393 ; fully-qualified # 👨🏻‍🎓 E4.0 man student: light skin tone -#1F468 1F3FC 200D 1F393 ; fully-qualified # 👨🏼‍🎓 E4.0 man student: medium-light skin tone -#1F468 1F3FD 200D 1F393 ; fully-qualified # 👨🏽‍🎓 E4.0 man student: medium skin tone -#1F468 1F3FE 200D 1F393 ; fully-qualified # 👨🏾‍🎓 E4.0 man student: medium-dark skin tone -#1F468 1F3FF 200D 1F393 ; fully-qualified # 👨🏿‍🎓 E4.0 man student: dark skin tone -#1F469 200D 1F393 ; fully-qualified # 👩‍🎓 E4.0 woman student -#1F469 1F3FB 200D 1F393 ; fully-qualified # 👩🏻‍🎓 E4.0 woman student: light skin tone -#1F469 1F3FC 200D 1F393 ; fully-qualified # 👩🏼‍🎓 E4.0 woman student: medium-light skin tone -#1F469 1F3FD 200D 1F393 ; fully-qualified # 👩🏽‍🎓 E4.0 woman student: medium skin tone -#1F469 1F3FE 200D 1F393 ; fully-qualified # 👩🏾‍🎓 E4.0 woman student: medium-dark skin tone -#1F469 1F3FF 200D 1F393 ; fully-qualified # 👩🏿‍🎓 E4.0 woman student: dark skin tone -#1F9D1 200D 1F3EB ; fully-qualified # 🧑‍🏫 E12.1 teacher -#1F9D1 1F3FB 200D 1F3EB ; fully-qualified # 🧑🏻‍🏫 E12.1 teacher: light skin tone -#1F9D1 1F3FC 200D 1F3EB ; fully-qualified # 🧑🏼‍🏫 E12.1 teacher: medium-light skin tone -#1F9D1 1F3FD 200D 1F3EB ; fully-qualified # 🧑🏽‍🏫 E12.1 teacher: medium skin tone -#1F9D1 1F3FE 200D 1F3EB ; fully-qualified # 🧑🏾‍🏫 E12.1 teacher: medium-dark skin tone -#1F9D1 1F3FF 200D 1F3EB ; fully-qualified # 🧑🏿‍🏫 E12.1 teacher: dark skin tone -#1F468 200D 1F3EB ; fully-qualified # 👨‍🏫 E4.0 man teacher -#1F468 1F3FB 200D 1F3EB ; fully-qualified # 👨🏻‍🏫 E4.0 man teacher: light skin tone -#1F468 1F3FC 200D 1F3EB ; fully-qualified # 👨🏼‍🏫 E4.0 man teacher: medium-light skin tone -#1F468 1F3FD 200D 1F3EB ; fully-qualified # 👨🏽‍🏫 E4.0 man teacher: medium skin tone -#1F468 1F3FE 200D 1F3EB ; fully-qualified # 👨🏾‍🏫 E4.0 man teacher: medium-dark skin tone -#1F468 1F3FF 200D 1F3EB ; fully-qualified # 👨🏿‍🏫 E4.0 man teacher: dark skin tone -#1F469 200D 1F3EB ; fully-qualified # 👩‍🏫 E4.0 woman teacher -#1F469 1F3FB 200D 1F3EB ; fully-qualified # 👩🏻‍🏫 E4.0 woman teacher: light skin tone -#1F469 1F3FC 200D 1F3EB ; fully-qualified # 👩🏼‍🏫 E4.0 woman teacher: medium-light skin tone -#1F469 1F3FD 200D 1F3EB ; fully-qualified # 👩🏽‍🏫 E4.0 woman teacher: medium skin tone -#1F469 1F3FE 200D 1F3EB ; fully-qualified # 👩🏾‍🏫 E4.0 woman teacher: medium-dark skin tone -#1F469 1F3FF 200D 1F3EB ; fully-qualified # 👩🏿‍🏫 E4.0 woman teacher: dark skin tone -#1F9D1 200D 2696 FE0F ; fully-qualified # 🧑‍⚖️ E12.1 judge -#1F9D1 200D 2696 ; minimally-qualified # 🧑‍⚖ E12.1 judge -#1F9D1 1F3FB 200D 2696 FE0F ; fully-qualified # 🧑🏻‍⚖️ E12.1 judge: light skin tone -#1F9D1 1F3FB 200D 2696 ; minimally-qualified # 🧑🏻‍⚖ E12.1 judge: light skin tone -#1F9D1 1F3FC 200D 2696 FE0F ; fully-qualified # 🧑🏼‍⚖️ E12.1 judge: medium-light skin tone -#1F9D1 1F3FC 200D 2696 ; minimally-qualified # 🧑🏼‍⚖ E12.1 judge: medium-light skin tone -#1F9D1 1F3FD 200D 2696 FE0F ; fully-qualified # 🧑🏽‍⚖️ E12.1 judge: medium skin tone -#1F9D1 1F3FD 200D 2696 ; minimally-qualified # 🧑🏽‍⚖ E12.1 judge: medium skin tone -#1F9D1 1F3FE 200D 2696 FE0F ; fully-qualified # 🧑🏾‍⚖️ E12.1 judge: medium-dark skin tone -#1F9D1 1F3FE 200D 2696 ; minimally-qualified # 🧑🏾‍⚖ E12.1 judge: medium-dark skin tone -#1F9D1 1F3FF 200D 2696 FE0F ; fully-qualified # 🧑🏿‍⚖️ E12.1 judge: dark skin tone -#1F9D1 1F3FF 200D 2696 ; minimally-qualified # 🧑🏿‍⚖ E12.1 judge: dark skin tone -#1F468 200D 2696 FE0F ; fully-qualified # 👨‍⚖️ E4.0 man judge -#1F468 200D 2696 ; minimally-qualified # 👨‍⚖ E4.0 man judge -#1F468 1F3FB 200D 2696 FE0F ; fully-qualified # 👨🏻‍⚖️ E4.0 man judge: light skin tone -#1F468 1F3FB 200D 2696 ; minimally-qualified # 👨🏻‍⚖ E4.0 man judge: light skin tone -#1F468 1F3FC 200D 2696 FE0F ; fully-qualified # 👨🏼‍⚖️ E4.0 man judge: medium-light skin tone -#1F468 1F3FC 200D 2696 ; minimally-qualified # 👨🏼‍⚖ E4.0 man judge: medium-light skin tone -#1F468 1F3FD 200D 2696 FE0F ; fully-qualified # 👨🏽‍⚖️ E4.0 man judge: medium skin tone -#1F468 1F3FD 200D 2696 ; minimally-qualified # 👨🏽‍⚖ E4.0 man judge: medium skin tone -#1F468 1F3FE 200D 2696 FE0F ; fully-qualified # 👨🏾‍⚖️ E4.0 man judge: medium-dark skin tone -#1F468 1F3FE 200D 2696 ; minimally-qualified # 👨🏾‍⚖ E4.0 man judge: medium-dark skin tone -#1F468 1F3FF 200D 2696 FE0F ; fully-qualified # 👨🏿‍⚖️ E4.0 man judge: dark skin tone -#1F468 1F3FF 200D 2696 ; minimally-qualified # 👨🏿‍⚖ E4.0 man judge: dark skin tone -#1F469 200D 2696 FE0F ; fully-qualified # 👩‍⚖️ E4.0 woman judge -#1F469 200D 2696 ; minimally-qualified # 👩‍⚖ E4.0 woman judge -#1F469 1F3FB 200D 2696 FE0F ; fully-qualified # 👩🏻‍⚖️ E4.0 woman judge: light skin tone -#1F469 1F3FB 200D 2696 ; minimally-qualified # 👩🏻‍⚖ E4.0 woman judge: light skin tone -#1F469 1F3FC 200D 2696 FE0F ; fully-qualified # 👩🏼‍⚖️ E4.0 woman judge: medium-light skin tone -#1F469 1F3FC 200D 2696 ; minimally-qualified # 👩🏼‍⚖ E4.0 woman judge: medium-light skin tone -#1F469 1F3FD 200D 2696 FE0F ; fully-qualified # 👩🏽‍⚖️ E4.0 woman judge: medium skin tone -#1F469 1F3FD 200D 2696 ; minimally-qualified # 👩🏽‍⚖ E4.0 woman judge: medium skin tone -#1F469 1F3FE 200D 2696 FE0F ; fully-qualified # 👩🏾‍⚖️ E4.0 woman judge: medium-dark skin tone -#1F469 1F3FE 200D 2696 ; minimally-qualified # 👩🏾‍⚖ E4.0 woman judge: medium-dark skin tone -#1F469 1F3FF 200D 2696 FE0F ; fully-qualified # 👩🏿‍⚖️ E4.0 woman judge: dark skin tone -#1F469 1F3FF 200D 2696 ; minimally-qualified # 👩🏿‍⚖ E4.0 woman judge: dark skin tone -#1F9D1 200D 1F33E ; fully-qualified # 🧑‍🌾 E12.1 farmer -#1F9D1 1F3FB 200D 1F33E ; fully-qualified # 🧑🏻‍🌾 E12.1 farmer: light skin tone -#1F9D1 1F3FC 200D 1F33E ; fully-qualified # 🧑🏼‍🌾 E12.1 farmer: medium-light skin tone -#1F9D1 1F3FD 200D 1F33E ; fully-qualified # 🧑🏽‍🌾 E12.1 farmer: medium skin tone -#1F9D1 1F3FE 200D 1F33E ; fully-qualified # 🧑🏾‍🌾 E12.1 farmer: medium-dark skin tone -#1F9D1 1F3FF 200D 1F33E ; fully-qualified # 🧑🏿‍🌾 E12.1 farmer: dark skin tone -#1F468 200D 1F33E ; fully-qualified # 👨‍🌾 E4.0 man farmer -#1F468 1F3FB 200D 1F33E ; fully-qualified # 👨🏻‍🌾 E4.0 man farmer: light skin tone -#1F468 1F3FC 200D 1F33E ; fully-qualified # 👨🏼‍🌾 E4.0 man farmer: medium-light skin tone -#1F468 1F3FD 200D 1F33E ; fully-qualified # 👨🏽‍🌾 E4.0 man farmer: medium skin tone -#1F468 1F3FE 200D 1F33E ; fully-qualified # 👨🏾‍🌾 E4.0 man farmer: medium-dark skin tone -#1F468 1F3FF 200D 1F33E ; fully-qualified # 👨🏿‍🌾 E4.0 man farmer: dark skin tone -#1F469 200D 1F33E ; fully-qualified # 👩‍🌾 E4.0 woman farmer -#1F469 1F3FB 200D 1F33E ; fully-qualified # 👩🏻‍🌾 E4.0 woman farmer: light skin tone -#1F469 1F3FC 200D 1F33E ; fully-qualified # 👩🏼‍🌾 E4.0 woman farmer: medium-light skin tone -#1F469 1F3FD 200D 1F33E ; fully-qualified # 👩🏽‍🌾 E4.0 woman farmer: medium skin tone -#1F469 1F3FE 200D 1F33E ; fully-qualified # 👩🏾‍🌾 E4.0 woman farmer: medium-dark skin tone -#1F469 1F3FF 200D 1F33E ; fully-qualified # 👩🏿‍🌾 E4.0 woman farmer: dark skin tone -#1F9D1 200D 1F373 ; fully-qualified # 🧑‍🍳 E12.1 cook -#1F9D1 1F3FB 200D 1F373 ; fully-qualified # 🧑🏻‍🍳 E12.1 cook: light skin tone -#1F9D1 1F3FC 200D 1F373 ; fully-qualified # 🧑🏼‍🍳 E12.1 cook: medium-light skin tone -#1F9D1 1F3FD 200D 1F373 ; fully-qualified # 🧑🏽‍🍳 E12.1 cook: medium skin tone -#1F9D1 1F3FE 200D 1F373 ; fully-qualified # 🧑🏾‍🍳 E12.1 cook: medium-dark skin tone -#1F9D1 1F3FF 200D 1F373 ; fully-qualified # 🧑🏿‍🍳 E12.1 cook: dark skin tone -#1F468 200D 1F373 ; fully-qualified # 👨‍🍳 E4.0 man cook -#1F468 1F3FB 200D 1F373 ; fully-qualified # 👨🏻‍🍳 E4.0 man cook: light skin tone -#1F468 1F3FC 200D 1F373 ; fully-qualified # 👨🏼‍🍳 E4.0 man cook: medium-light skin tone -#1F468 1F3FD 200D 1F373 ; fully-qualified # 👨🏽‍🍳 E4.0 man cook: medium skin tone -#1F468 1F3FE 200D 1F373 ; fully-qualified # 👨🏾‍🍳 E4.0 man cook: medium-dark skin tone -#1F468 1F3FF 200D 1F373 ; fully-qualified # 👨🏿‍🍳 E4.0 man cook: dark skin tone -#1F469 200D 1F373 ; fully-qualified # 👩‍🍳 E4.0 woman cook -#1F469 1F3FB 200D 1F373 ; fully-qualified # 👩🏻‍🍳 E4.0 woman cook: light skin tone -#1F469 1F3FC 200D 1F373 ; fully-qualified # 👩🏼‍🍳 E4.0 woman cook: medium-light skin tone -#1F469 1F3FD 200D 1F373 ; fully-qualified # 👩🏽‍🍳 E4.0 woman cook: medium skin tone -#1F469 1F3FE 200D 1F373 ; fully-qualified # 👩🏾‍🍳 E4.0 woman cook: medium-dark skin tone -#1F469 1F3FF 200D 1F373 ; fully-qualified # 👩🏿‍🍳 E4.0 woman cook: dark skin tone -#1F9D1 200D 1F527 ; fully-qualified # 🧑‍🔧 E12.1 mechanic -#1F9D1 1F3FB 200D 1F527 ; fully-qualified # 🧑🏻‍🔧 E12.1 mechanic: light skin tone -#1F9D1 1F3FC 200D 1F527 ; fully-qualified # 🧑🏼‍🔧 E12.1 mechanic: medium-light skin tone -#1F9D1 1F3FD 200D 1F527 ; fully-qualified # 🧑🏽‍🔧 E12.1 mechanic: medium skin tone -#1F9D1 1F3FE 200D 1F527 ; fully-qualified # 🧑🏾‍🔧 E12.1 mechanic: medium-dark skin tone -#1F9D1 1F3FF 200D 1F527 ; fully-qualified # 🧑🏿‍🔧 E12.1 mechanic: dark skin tone -#1F468 200D 1F527 ; fully-qualified # 👨‍🔧 E4.0 man mechanic -#1F468 1F3FB 200D 1F527 ; fully-qualified # 👨🏻‍🔧 E4.0 man mechanic: light skin tone -#1F468 1F3FC 200D 1F527 ; fully-qualified # 👨🏼‍🔧 E4.0 man mechanic: medium-light skin tone -#1F468 1F3FD 200D 1F527 ; fully-qualified # 👨🏽‍🔧 E4.0 man mechanic: medium skin tone -#1F468 1F3FE 200D 1F527 ; fully-qualified # 👨🏾‍🔧 E4.0 man mechanic: medium-dark skin tone -#1F468 1F3FF 200D 1F527 ; fully-qualified # 👨🏿‍🔧 E4.0 man mechanic: dark skin tone -#1F469 200D 1F527 ; fully-qualified # 👩‍🔧 E4.0 woman mechanic -#1F469 1F3FB 200D 1F527 ; fully-qualified # 👩🏻‍🔧 E4.0 woman mechanic: light skin tone -#1F469 1F3FC 200D 1F527 ; fully-qualified # 👩🏼‍🔧 E4.0 woman mechanic: medium-light skin tone -#1F469 1F3FD 200D 1F527 ; fully-qualified # 👩🏽‍🔧 E4.0 woman mechanic: medium skin tone -#1F469 1F3FE 200D 1F527 ; fully-qualified # 👩🏾‍🔧 E4.0 woman mechanic: medium-dark skin tone -#1F469 1F3FF 200D 1F527 ; fully-qualified # 👩🏿‍🔧 E4.0 woman mechanic: dark skin tone -#1F9D1 200D 1F3ED ; fully-qualified # 🧑‍🏭 E12.1 factory worker -#1F9D1 1F3FB 200D 1F3ED ; fully-qualified # 🧑🏻‍🏭 E12.1 factory worker: light skin tone -#1F9D1 1F3FC 200D 1F3ED ; fully-qualified # 🧑🏼‍🏭 E12.1 factory worker: medium-light skin tone -#1F9D1 1F3FD 200D 1F3ED ; fully-qualified # 🧑🏽‍🏭 E12.1 factory worker: medium skin tone -#1F9D1 1F3FE 200D 1F3ED ; fully-qualified # 🧑🏾‍🏭 E12.1 factory worker: medium-dark skin tone -#1F9D1 1F3FF 200D 1F3ED ; fully-qualified # 🧑🏿‍🏭 E12.1 factory worker: dark skin tone -#1F468 200D 1F3ED ; fully-qualified # 👨‍🏭 E4.0 man factory worker -#1F468 1F3FB 200D 1F3ED ; fully-qualified # 👨🏻‍🏭 E4.0 man factory worker: light skin tone -#1F468 1F3FC 200D 1F3ED ; fully-qualified # 👨🏼‍🏭 E4.0 man factory worker: medium-light skin tone -#1F468 1F3FD 200D 1F3ED ; fully-qualified # 👨🏽‍🏭 E4.0 man factory worker: medium skin tone -#1F468 1F3FE 200D 1F3ED ; fully-qualified # 👨🏾‍🏭 E4.0 man factory worker: medium-dark skin tone -#1F468 1F3FF 200D 1F3ED ; fully-qualified # 👨🏿‍🏭 E4.0 man factory worker: dark skin tone -#1F469 200D 1F3ED ; fully-qualified # 👩‍🏭 E4.0 woman factory worker -#1F469 1F3FB 200D 1F3ED ; fully-qualified # 👩🏻‍🏭 E4.0 woman factory worker: light skin tone -#1F469 1F3FC 200D 1F3ED ; fully-qualified # 👩🏼‍🏭 E4.0 woman factory worker: medium-light skin tone -#1F469 1F3FD 200D 1F3ED ; fully-qualified # 👩🏽‍🏭 E4.0 woman factory worker: medium skin tone -#1F469 1F3FE 200D 1F3ED ; fully-qualified # 👩🏾‍🏭 E4.0 woman factory worker: medium-dark skin tone -#1F469 1F3FF 200D 1F3ED ; fully-qualified # 👩🏿‍🏭 E4.0 woman factory worker: dark skin tone -#1F9D1 200D 1F4BC ; fully-qualified # 🧑‍💼 E12.1 office worker -#1F9D1 1F3FB 200D 1F4BC ; fully-qualified # 🧑🏻‍💼 E12.1 office worker: light skin tone -#1F9D1 1F3FC 200D 1F4BC ; fully-qualified # 🧑🏼‍💼 E12.1 office worker: medium-light skin tone -#1F9D1 1F3FD 200D 1F4BC ; fully-qualified # 🧑🏽‍💼 E12.1 office worker: medium skin tone -#1F9D1 1F3FE 200D 1F4BC ; fully-qualified # 🧑🏾‍💼 E12.1 office worker: medium-dark skin tone -#1F9D1 1F3FF 200D 1F4BC ; fully-qualified # 🧑🏿‍💼 E12.1 office worker: dark skin tone -#1F468 200D 1F4BC ; fully-qualified # 👨‍💼 E4.0 man office worker -#1F468 1F3FB 200D 1F4BC ; fully-qualified # 👨🏻‍💼 E4.0 man office worker: light skin tone -#1F468 1F3FC 200D 1F4BC ; fully-qualified # 👨🏼‍💼 E4.0 man office worker: medium-light skin tone -#1F468 1F3FD 200D 1F4BC ; fully-qualified # 👨🏽‍💼 E4.0 man office worker: medium skin tone -#1F468 1F3FE 200D 1F4BC ; fully-qualified # 👨🏾‍💼 E4.0 man office worker: medium-dark skin tone -#1F468 1F3FF 200D 1F4BC ; fully-qualified # 👨🏿‍💼 E4.0 man office worker: dark skin tone -#1F469 200D 1F4BC ; fully-qualified # 👩‍💼 E4.0 woman office worker -#1F469 1F3FB 200D 1F4BC ; fully-qualified # 👩🏻‍💼 E4.0 woman office worker: light skin tone -#1F469 1F3FC 200D 1F4BC ; fully-qualified # 👩🏼‍💼 E4.0 woman office worker: medium-light skin tone -#1F469 1F3FD 200D 1F4BC ; fully-qualified # 👩🏽‍💼 E4.0 woman office worker: medium skin tone -#1F469 1F3FE 200D 1F4BC ; fully-qualified # 👩🏾‍💼 E4.0 woman office worker: medium-dark skin tone -#1F469 1F3FF 200D 1F4BC ; fully-qualified # 👩🏿‍💼 E4.0 woman office worker: dark skin tone -#1F9D1 200D 1F52C ; fully-qualified # 🧑‍🔬 E12.1 scientist -#1F9D1 1F3FB 200D 1F52C ; fully-qualified # 🧑🏻‍🔬 E12.1 scientist: light skin tone -#1F9D1 1F3FC 200D 1F52C ; fully-qualified # 🧑🏼‍🔬 E12.1 scientist: medium-light skin tone -#1F9D1 1F3FD 200D 1F52C ; fully-qualified # 🧑🏽‍🔬 E12.1 scientist: medium skin tone -#1F9D1 1F3FE 200D 1F52C ; fully-qualified # 🧑🏾‍🔬 E12.1 scientist: medium-dark skin tone -#1F9D1 1F3FF 200D 1F52C ; fully-qualified # 🧑🏿‍🔬 E12.1 scientist: dark skin tone -#1F468 200D 1F52C ; fully-qualified # 👨‍🔬 E4.0 man scientist -#1F468 1F3FB 200D 1F52C ; fully-qualified # 👨🏻‍🔬 E4.0 man scientist: light skin tone -#1F468 1F3FC 200D 1F52C ; fully-qualified # 👨🏼‍🔬 E4.0 man scientist: medium-light skin tone -#1F468 1F3FD 200D 1F52C ; fully-qualified # 👨🏽‍🔬 E4.0 man scientist: medium skin tone -#1F468 1F3FE 200D 1F52C ; fully-qualified # 👨🏾‍🔬 E4.0 man scientist: medium-dark skin tone -#1F468 1F3FF 200D 1F52C ; fully-qualified # 👨🏿‍🔬 E4.0 man scientist: dark skin tone -#1F469 200D 1F52C ; fully-qualified # 👩‍🔬 E4.0 woman scientist -#1F469 1F3FB 200D 1F52C ; fully-qualified # 👩🏻‍🔬 E4.0 woman scientist: light skin tone -#1F469 1F3FC 200D 1F52C ; fully-qualified # 👩🏼‍🔬 E4.0 woman scientist: medium-light skin tone -#1F469 1F3FD 200D 1F52C ; fully-qualified # 👩🏽‍🔬 E4.0 woman scientist: medium skin tone -#1F469 1F3FE 200D 1F52C ; fully-qualified # 👩🏾‍🔬 E4.0 woman scientist: medium-dark skin tone -#1F469 1F3FF 200D 1F52C ; fully-qualified # 👩🏿‍🔬 E4.0 woman scientist: dark skin tone -#1F9D1 200D 1F4BB ; fully-qualified # 🧑‍💻 E12.1 technologist -#1F9D1 1F3FB 200D 1F4BB ; fully-qualified # 🧑🏻‍💻 E12.1 technologist: light skin tone -#1F9D1 1F3FC 200D 1F4BB ; fully-qualified # 🧑🏼‍💻 E12.1 technologist: medium-light skin tone -#1F9D1 1F3FD 200D 1F4BB ; fully-qualified # 🧑🏽‍💻 E12.1 technologist: medium skin tone -#1F9D1 1F3FE 200D 1F4BB ; fully-qualified # 🧑🏾‍💻 E12.1 technologist: medium-dark skin tone -#1F9D1 1F3FF 200D 1F4BB ; fully-qualified # 🧑🏿‍💻 E12.1 technologist: dark skin tone -#1F468 200D 1F4BB ; fully-qualified # 👨‍💻 E4.0 man technologist -#1F468 1F3FB 200D 1F4BB ; fully-qualified # 👨🏻‍💻 E4.0 man technologist: light skin tone -#1F468 1F3FC 200D 1F4BB ; fully-qualified # 👨🏼‍💻 E4.0 man technologist: medium-light skin tone -#1F468 1F3FD 200D 1F4BB ; fully-qualified # 👨🏽‍💻 E4.0 man technologist: medium skin tone -#1F468 1F3FE 200D 1F4BB ; fully-qualified # 👨🏾‍💻 E4.0 man technologist: medium-dark skin tone -#1F468 1F3FF 200D 1F4BB ; fully-qualified # 👨🏿‍💻 E4.0 man technologist: dark skin tone -#1F469 200D 1F4BB ; fully-qualified # 👩‍💻 E4.0 woman technologist -#1F469 1F3FB 200D 1F4BB ; fully-qualified # 👩🏻‍💻 E4.0 woman technologist: light skin tone -#1F469 1F3FC 200D 1F4BB ; fully-qualified # 👩🏼‍💻 E4.0 woman technologist: medium-light skin tone -#1F469 1F3FD 200D 1F4BB ; fully-qualified # 👩🏽‍💻 E4.0 woman technologist: medium skin tone -#1F469 1F3FE 200D 1F4BB ; fully-qualified # 👩🏾‍💻 E4.0 woman technologist: medium-dark skin tone -#1F469 1F3FF 200D 1F4BB ; fully-qualified # 👩🏿‍💻 E4.0 woman technologist: dark skin tone -#1F9D1 200D 1F3A4 ; fully-qualified # 🧑‍🎤 E12.1 singer -#1F9D1 1F3FB 200D 1F3A4 ; fully-qualified # 🧑🏻‍🎤 E12.1 singer: light skin tone -#1F9D1 1F3FC 200D 1F3A4 ; fully-qualified # 🧑🏼‍🎤 E12.1 singer: medium-light skin tone -#1F9D1 1F3FD 200D 1F3A4 ; fully-qualified # 🧑🏽‍🎤 E12.1 singer: medium skin tone -#1F9D1 1F3FE 200D 1F3A4 ; fully-qualified # 🧑🏾‍🎤 E12.1 singer: medium-dark skin tone -#1F9D1 1F3FF 200D 1F3A4 ; fully-qualified # 🧑🏿‍🎤 E12.1 singer: dark skin tone -#1F468 200D 1F3A4 ; fully-qualified # 👨‍🎤 E4.0 man singer -#1F468 1F3FB 200D 1F3A4 ; fully-qualified # 👨🏻‍🎤 E4.0 man singer: light skin tone -#1F468 1F3FC 200D 1F3A4 ; fully-qualified # 👨🏼‍🎤 E4.0 man singer: medium-light skin tone -#1F468 1F3FD 200D 1F3A4 ; fully-qualified # 👨🏽‍🎤 E4.0 man singer: medium skin tone -#1F468 1F3FE 200D 1F3A4 ; fully-qualified # 👨🏾‍🎤 E4.0 man singer: medium-dark skin tone -#1F468 1F3FF 200D 1F3A4 ; fully-qualified # 👨🏿‍🎤 E4.0 man singer: dark skin tone -#1F469 200D 1F3A4 ; fully-qualified # 👩‍🎤 E4.0 woman singer -#1F469 1F3FB 200D 1F3A4 ; fully-qualified # 👩🏻‍🎤 E4.0 woman singer: light skin tone -#1F469 1F3FC 200D 1F3A4 ; fully-qualified # 👩🏼‍🎤 E4.0 woman singer: medium-light skin tone -#1F469 1F3FD 200D 1F3A4 ; fully-qualified # 👩🏽‍🎤 E4.0 woman singer: medium skin tone -#1F469 1F3FE 200D 1F3A4 ; fully-qualified # 👩🏾‍🎤 E4.0 woman singer: medium-dark skin tone -#1F469 1F3FF 200D 1F3A4 ; fully-qualified # 👩🏿‍🎤 E4.0 woman singer: dark skin tone -#1F9D1 200D 1F3A8 ; fully-qualified # 🧑‍🎨 E12.1 artist -#1F9D1 1F3FB 200D 1F3A8 ; fully-qualified # 🧑🏻‍🎨 E12.1 artist: light skin tone -#1F9D1 1F3FC 200D 1F3A8 ; fully-qualified # 🧑🏼‍🎨 E12.1 artist: medium-light skin tone -#1F9D1 1F3FD 200D 1F3A8 ; fully-qualified # 🧑🏽‍🎨 E12.1 artist: medium skin tone -#1F9D1 1F3FE 200D 1F3A8 ; fully-qualified # 🧑🏾‍🎨 E12.1 artist: medium-dark skin tone -#1F9D1 1F3FF 200D 1F3A8 ; fully-qualified # 🧑🏿‍🎨 E12.1 artist: dark skin tone -#1F468 200D 1F3A8 ; fully-qualified # 👨‍🎨 E4.0 man artist -#1F468 1F3FB 200D 1F3A8 ; fully-qualified # 👨🏻‍🎨 E4.0 man artist: light skin tone -#1F468 1F3FC 200D 1F3A8 ; fully-qualified # 👨🏼‍🎨 E4.0 man artist: medium-light skin tone -#1F468 1F3FD 200D 1F3A8 ; fully-qualified # 👨🏽‍🎨 E4.0 man artist: medium skin tone -#1F468 1F3FE 200D 1F3A8 ; fully-qualified # 👨🏾‍🎨 E4.0 man artist: medium-dark skin tone -#1F468 1F3FF 200D 1F3A8 ; fully-qualified # 👨🏿‍🎨 E4.0 man artist: dark skin tone -#1F469 200D 1F3A8 ; fully-qualified # 👩‍🎨 E4.0 woman artist -#1F469 1F3FB 200D 1F3A8 ; fully-qualified # 👩🏻‍🎨 E4.0 woman artist: light skin tone -#1F469 1F3FC 200D 1F3A8 ; fully-qualified # 👩🏼‍🎨 E4.0 woman artist: medium-light skin tone -#1F469 1F3FD 200D 1F3A8 ; fully-qualified # 👩🏽‍🎨 E4.0 woman artist: medium skin tone -#1F469 1F3FE 200D 1F3A8 ; fully-qualified # 👩🏾‍🎨 E4.0 woman artist: medium-dark skin tone -#1F469 1F3FF 200D 1F3A8 ; fully-qualified # 👩🏿‍🎨 E4.0 woman artist: dark skin tone -#1F9D1 200D 2708 FE0F ; fully-qualified # 🧑‍✈️ E12.1 pilot -#1F9D1 200D 2708 ; minimally-qualified # 🧑‍✈ E12.1 pilot -#1F9D1 1F3FB 200D 2708 FE0F ; fully-qualified # 🧑🏻‍✈️ E12.1 pilot: light skin tone -#1F9D1 1F3FB 200D 2708 ; minimally-qualified # 🧑🏻‍✈ E12.1 pilot: light skin tone -#1F9D1 1F3FC 200D 2708 FE0F ; fully-qualified # 🧑🏼‍✈️ E12.1 pilot: medium-light skin tone -#1F9D1 1F3FC 200D 2708 ; minimally-qualified # 🧑🏼‍✈ E12.1 pilot: medium-light skin tone -#1F9D1 1F3FD 200D 2708 FE0F ; fully-qualified # 🧑🏽‍✈️ E12.1 pilot: medium skin tone -#1F9D1 1F3FD 200D 2708 ; minimally-qualified # 🧑🏽‍✈ E12.1 pilot: medium skin tone -#1F9D1 1F3FE 200D 2708 FE0F ; fully-qualified # 🧑🏾‍✈️ E12.1 pilot: medium-dark skin tone -#1F9D1 1F3FE 200D 2708 ; minimally-qualified # 🧑🏾‍✈ E12.1 pilot: medium-dark skin tone -#1F9D1 1F3FF 200D 2708 FE0F ; fully-qualified # 🧑🏿‍✈️ E12.1 pilot: dark skin tone -#1F9D1 1F3FF 200D 2708 ; minimally-qualified # 🧑🏿‍✈ E12.1 pilot: dark skin tone -#1F468 200D 2708 FE0F ; fully-qualified # 👨‍✈️ E4.0 man pilot -#1F468 200D 2708 ; minimally-qualified # 👨‍✈ E4.0 man pilot -#1F468 1F3FB 200D 2708 FE0F ; fully-qualified # 👨🏻‍✈️ E4.0 man pilot: light skin tone -#1F468 1F3FB 200D 2708 ; minimally-qualified # 👨🏻‍✈ E4.0 man pilot: light skin tone -#1F468 1F3FC 200D 2708 FE0F ; fully-qualified # 👨🏼‍✈️ E4.0 man pilot: medium-light skin tone -#1F468 1F3FC 200D 2708 ; minimally-qualified # 👨🏼‍✈ E4.0 man pilot: medium-light skin tone -#1F468 1F3FD 200D 2708 FE0F ; fully-qualified # 👨🏽‍✈️ E4.0 man pilot: medium skin tone -#1F468 1F3FD 200D 2708 ; minimally-qualified # 👨🏽‍✈ E4.0 man pilot: medium skin tone -#1F468 1F3FE 200D 2708 FE0F ; fully-qualified # 👨🏾‍✈️ E4.0 man pilot: medium-dark skin tone -#1F468 1F3FE 200D 2708 ; minimally-qualified # 👨🏾‍✈ E4.0 man pilot: medium-dark skin tone -#1F468 1F3FF 200D 2708 FE0F ; fully-qualified # 👨🏿‍✈️ E4.0 man pilot: dark skin tone -#1F468 1F3FF 200D 2708 ; minimally-qualified # 👨🏿‍✈ E4.0 man pilot: dark skin tone -#1F469 200D 2708 FE0F ; fully-qualified # 👩‍✈️ E4.0 woman pilot -#1F469 200D 2708 ; minimally-qualified # 👩‍✈ E4.0 woman pilot -#1F469 1F3FB 200D 2708 FE0F ; fully-qualified # 👩🏻‍✈️ E4.0 woman pilot: light skin tone -#1F469 1F3FB 200D 2708 ; minimally-qualified # 👩🏻‍✈ E4.0 woman pilot: light skin tone -#1F469 1F3FC 200D 2708 FE0F ; fully-qualified # 👩🏼‍✈️ E4.0 woman pilot: medium-light skin tone -#1F469 1F3FC 200D 2708 ; minimally-qualified # 👩🏼‍✈ E4.0 woman pilot: medium-light skin tone -#1F469 1F3FD 200D 2708 FE0F ; fully-qualified # 👩🏽‍✈️ E4.0 woman pilot: medium skin tone -#1F469 1F3FD 200D 2708 ; minimally-qualified # 👩🏽‍✈ E4.0 woman pilot: medium skin tone -#1F469 1F3FE 200D 2708 FE0F ; fully-qualified # 👩🏾‍✈️ E4.0 woman pilot: medium-dark skin tone -#1F469 1F3FE 200D 2708 ; minimally-qualified # 👩🏾‍✈ E4.0 woman pilot: medium-dark skin tone -#1F469 1F3FF 200D 2708 FE0F ; fully-qualified # 👩🏿‍✈️ E4.0 woman pilot: dark skin tone -#1F469 1F3FF 200D 2708 ; minimally-qualified # 👩🏿‍✈ E4.0 woman pilot: dark skin tone -#1F9D1 200D 1F680 ; fully-qualified # 🧑‍🚀 E12.1 astronaut -#1F9D1 1F3FB 200D 1F680 ; fully-qualified # 🧑🏻‍🚀 E12.1 astronaut: light skin tone -#1F9D1 1F3FC 200D 1F680 ; fully-qualified # 🧑🏼‍🚀 E12.1 astronaut: medium-light skin tone -#1F9D1 1F3FD 200D 1F680 ; fully-qualified # 🧑🏽‍🚀 E12.1 astronaut: medium skin tone -#1F9D1 1F3FE 200D 1F680 ; fully-qualified # 🧑🏾‍🚀 E12.1 astronaut: medium-dark skin tone -#1F9D1 1F3FF 200D 1F680 ; fully-qualified # 🧑🏿‍🚀 E12.1 astronaut: dark skin tone -#1F468 200D 1F680 ; fully-qualified # 👨‍🚀 E4.0 man astronaut -#1F468 1F3FB 200D 1F680 ; fully-qualified # 👨🏻‍🚀 E4.0 man astronaut: light skin tone -#1F468 1F3FC 200D 1F680 ; fully-qualified # 👨🏼‍🚀 E4.0 man astronaut: medium-light skin tone -#1F468 1F3FD 200D 1F680 ; fully-qualified # 👨🏽‍🚀 E4.0 man astronaut: medium skin tone -#1F468 1F3FE 200D 1F680 ; fully-qualified # 👨🏾‍🚀 E4.0 man astronaut: medium-dark skin tone -#1F468 1F3FF 200D 1F680 ; fully-qualified # 👨🏿‍🚀 E4.0 man astronaut: dark skin tone -#1F469 200D 1F680 ; fully-qualified # 👩‍🚀 E4.0 woman astronaut -#1F469 1F3FB 200D 1F680 ; fully-qualified # 👩🏻‍🚀 E4.0 woman astronaut: light skin tone -#1F469 1F3FC 200D 1F680 ; fully-qualified # 👩🏼‍🚀 E4.0 woman astronaut: medium-light skin tone -#1F469 1F3FD 200D 1F680 ; fully-qualified # 👩🏽‍🚀 E4.0 woman astronaut: medium skin tone -#1F469 1F3FE 200D 1F680 ; fully-qualified # 👩🏾‍🚀 E4.0 woman astronaut: medium-dark skin tone -#1F469 1F3FF 200D 1F680 ; fully-qualified # 👩🏿‍🚀 E4.0 woman astronaut: dark skin tone -#1F9D1 200D 1F692 ; fully-qualified # 🧑‍🚒 E12.1 firefighter -#1F9D1 1F3FB 200D 1F692 ; fully-qualified # 🧑🏻‍🚒 E12.1 firefighter: light skin tone -#1F9D1 1F3FC 200D 1F692 ; fully-qualified # 🧑🏼‍🚒 E12.1 firefighter: medium-light skin tone -#1F9D1 1F3FD 200D 1F692 ; fully-qualified # 🧑🏽‍🚒 E12.1 firefighter: medium skin tone -#1F9D1 1F3FE 200D 1F692 ; fully-qualified # 🧑🏾‍🚒 E12.1 firefighter: medium-dark skin tone -#1F9D1 1F3FF 200D 1F692 ; fully-qualified # 🧑🏿‍🚒 E12.1 firefighter: dark skin tone -#1F468 200D 1F692 ; fully-qualified # 👨‍🚒 E4.0 man firefighter -#1F468 1F3FB 200D 1F692 ; fully-qualified # 👨🏻‍🚒 E4.0 man firefighter: light skin tone -#1F468 1F3FC 200D 1F692 ; fully-qualified # 👨🏼‍🚒 E4.0 man firefighter: medium-light skin tone -#1F468 1F3FD 200D 1F692 ; fully-qualified # 👨🏽‍🚒 E4.0 man firefighter: medium skin tone -#1F468 1F3FE 200D 1F692 ; fully-qualified # 👨🏾‍🚒 E4.0 man firefighter: medium-dark skin tone -#1F468 1F3FF 200D 1F692 ; fully-qualified # 👨🏿‍🚒 E4.0 man firefighter: dark skin tone -#1F469 200D 1F692 ; fully-qualified # 👩‍🚒 E4.0 woman firefighter -#1F469 1F3FB 200D 1F692 ; fully-qualified # 👩🏻‍🚒 E4.0 woman firefighter: light skin tone -#1F469 1F3FC 200D 1F692 ; fully-qualified # 👩🏼‍🚒 E4.0 woman firefighter: medium-light skin tone -#1F469 1F3FD 200D 1F692 ; fully-qualified # 👩🏽‍🚒 E4.0 woman firefighter: medium skin tone -#1F469 1F3FE 200D 1F692 ; fully-qualified # 👩🏾‍🚒 E4.0 woman firefighter: medium-dark skin tone -#1F469 1F3FF 200D 1F692 ; fully-qualified # 👩🏿‍🚒 E4.0 woman firefighter: dark skin tone -#1F46E ; fully-qualified # 👮 E0.6 police officer -#1F46E 1F3FB ; fully-qualified # 👮🏻 E1.0 police officer: light skin tone -#1F46E 1F3FC ; fully-qualified # 👮🏼 E1.0 police officer: medium-light skin tone -#1F46E 1F3FD ; fully-qualified # 👮🏽 E1.0 police officer: medium skin tone -#1F46E 1F3FE ; fully-qualified # 👮🏾 E1.0 police officer: medium-dark skin tone -#1F46E 1F3FF ; fully-qualified # 👮🏿 E1.0 police officer: dark skin tone -#1F46E 200D 2642 FE0F ; fully-qualified # 👮‍♂️ E4.0 man police officer -#1F46E 200D 2642 ; minimally-qualified # 👮‍♂ E4.0 man police officer -#1F46E 1F3FB 200D 2642 FE0F ; fully-qualified # 👮🏻‍♂️ E4.0 man police officer: light skin tone -#1F46E 1F3FB 200D 2642 ; minimally-qualified # 👮🏻‍♂ E4.0 man police officer: light skin tone -#1F46E 1F3FC 200D 2642 FE0F ; fully-qualified # 👮🏼‍♂️ E4.0 man police officer: medium-light skin tone -#1F46E 1F3FC 200D 2642 ; minimally-qualified # 👮🏼‍♂ E4.0 man police officer: medium-light skin tone -#1F46E 1F3FD 200D 2642 FE0F ; fully-qualified # 👮🏽‍♂️ E4.0 man police officer: medium skin tone -#1F46E 1F3FD 200D 2642 ; minimally-qualified # 👮🏽‍♂ E4.0 man police officer: medium skin tone -#1F46E 1F3FE 200D 2642 FE0F ; fully-qualified # 👮🏾‍♂️ E4.0 man police officer: medium-dark skin tone -#1F46E 1F3FE 200D 2642 ; minimally-qualified # 👮🏾‍♂ E4.0 man police officer: medium-dark skin tone -#1F46E 1F3FF 200D 2642 FE0F ; fully-qualified # 👮🏿‍♂️ E4.0 man police officer: dark skin tone -#1F46E 1F3FF 200D 2642 ; minimally-qualified # 👮🏿‍♂ E4.0 man police officer: dark skin tone -#1F46E 200D 2640 FE0F ; fully-qualified # 👮‍♀️ E4.0 woman police officer -#1F46E 200D 2640 ; minimally-qualified # 👮‍♀ E4.0 woman police officer -#1F46E 1F3FB 200D 2640 FE0F ; fully-qualified # 👮🏻‍♀️ E4.0 woman police officer: light skin tone -#1F46E 1F3FB 200D 2640 ; minimally-qualified # 👮🏻‍♀ E4.0 woman police officer: light skin tone -#1F46E 1F3FC 200D 2640 FE0F ; fully-qualified # 👮🏼‍♀️ E4.0 woman police officer: medium-light skin tone -#1F46E 1F3FC 200D 2640 ; minimally-qualified # 👮🏼‍♀ E4.0 woman police officer: medium-light skin tone -#1F46E 1F3FD 200D 2640 FE0F ; fully-qualified # 👮🏽‍♀️ E4.0 woman police officer: medium skin tone -#1F46E 1F3FD 200D 2640 ; minimally-qualified # 👮🏽‍♀ E4.0 woman police officer: medium skin tone -#1F46E 1F3FE 200D 2640 FE0F ; fully-qualified # 👮🏾‍♀️ E4.0 woman police officer: medium-dark skin tone -#1F46E 1F3FE 200D 2640 ; minimally-qualified # 👮🏾‍♀ E4.0 woman police officer: medium-dark skin tone -#1F46E 1F3FF 200D 2640 FE0F ; fully-qualified # 👮🏿‍♀️ E4.0 woman police officer: dark skin tone -#1F46E 1F3FF 200D 2640 ; minimally-qualified # 👮🏿‍♀ E4.0 woman police officer: dark skin tone -#1F575 FE0F ; fully-qualified # 🕵️ E0.7 detective -#1F575 ; unqualified # 🕵 E0.7 detective -#1F575 1F3FB ; fully-qualified # 🕵🏻 E2.0 detective: light skin tone -#1F575 1F3FC ; fully-qualified # 🕵🏼 E2.0 detective: medium-light skin tone -#1F575 1F3FD ; fully-qualified # 🕵🏽 E2.0 detective: medium skin tone -#1F575 1F3FE ; fully-qualified # 🕵🏾 E2.0 detective: medium-dark skin tone -#1F575 1F3FF ; fully-qualified # 🕵🏿 E2.0 detective: dark skin tone -#1F575 FE0F 200D 2642 FE0F ; fully-qualified # 🕵️‍♂️ E4.0 man detective -#1F575 200D 2642 FE0F ; unqualified # 🕵‍♂️ E4.0 man detective -#1F575 FE0F 200D 2642 ; unqualified # 🕵️‍♂ E4.0 man detective -#1F575 200D 2642 ; unqualified # 🕵‍♂ E4.0 man detective -#1F575 1F3FB 200D 2642 FE0F ; fully-qualified # 🕵🏻‍♂️ E4.0 man detective: light skin tone -#1F575 1F3FB 200D 2642 ; minimally-qualified # 🕵🏻‍♂ E4.0 man detective: light skin tone -#1F575 1F3FC 200D 2642 FE0F ; fully-qualified # 🕵🏼‍♂️ E4.0 man detective: medium-light skin tone -#1F575 1F3FC 200D 2642 ; minimally-qualified # 🕵🏼‍♂ E4.0 man detective: medium-light skin tone -#1F575 1F3FD 200D 2642 FE0F ; fully-qualified # 🕵🏽‍♂️ E4.0 man detective: medium skin tone -#1F575 1F3FD 200D 2642 ; minimally-qualified # 🕵🏽‍♂ E4.0 man detective: medium skin tone -#1F575 1F3FE 200D 2642 FE0F ; fully-qualified # 🕵🏾‍♂️ E4.0 man detective: medium-dark skin tone -#1F575 1F3FE 200D 2642 ; minimally-qualified # 🕵🏾‍♂ E4.0 man detective: medium-dark skin tone -#1F575 1F3FF 200D 2642 FE0F ; fully-qualified # 🕵🏿‍♂️ E4.0 man detective: dark skin tone -#1F575 1F3FF 200D 2642 ; minimally-qualified # 🕵🏿‍♂ E4.0 man detective: dark skin tone -#1F575 FE0F 200D 2640 FE0F ; fully-qualified # 🕵️‍♀️ E4.0 woman detective -#1F575 200D 2640 FE0F ; unqualified # 🕵‍♀️ E4.0 woman detective -#1F575 FE0F 200D 2640 ; unqualified # 🕵️‍♀ E4.0 woman detective -#1F575 200D 2640 ; unqualified # 🕵‍♀ E4.0 woman detective -#1F575 1F3FB 200D 2640 FE0F ; fully-qualified # 🕵🏻‍♀️ E4.0 woman detective: light skin tone -#1F575 1F3FB 200D 2640 ; minimally-qualified # 🕵🏻‍♀ E4.0 woman detective: light skin tone -#1F575 1F3FC 200D 2640 FE0F ; fully-qualified # 🕵🏼‍♀️ E4.0 woman detective: medium-light skin tone -#1F575 1F3FC 200D 2640 ; minimally-qualified # 🕵🏼‍♀ E4.0 woman detective: medium-light skin tone -#1F575 1F3FD 200D 2640 FE0F ; fully-qualified # 🕵🏽‍♀️ E4.0 woman detective: medium skin tone -#1F575 1F3FD 200D 2640 ; minimally-qualified # 🕵🏽‍♀ E4.0 woman detective: medium skin tone -#1F575 1F3FE 200D 2640 FE0F ; fully-qualified # 🕵🏾‍♀️ E4.0 woman detective: medium-dark skin tone -#1F575 1F3FE 200D 2640 ; minimally-qualified # 🕵🏾‍♀ E4.0 woman detective: medium-dark skin tone -#1F575 1F3FF 200D 2640 FE0F ; fully-qualified # 🕵🏿‍♀️ E4.0 woman detective: dark skin tone -#1F575 1F3FF 200D 2640 ; minimally-qualified # 🕵🏿‍♀ E4.0 woman detective: dark skin tone -#1F482 ; fully-qualified # 💂 E0.6 guard -#1F482 1F3FB ; fully-qualified # 💂🏻 E1.0 guard: light skin tone -#1F482 1F3FC ; fully-qualified # 💂🏼 E1.0 guard: medium-light skin tone -#1F482 1F3FD ; fully-qualified # 💂🏽 E1.0 guard: medium skin tone -#1F482 1F3FE ; fully-qualified # 💂🏾 E1.0 guard: medium-dark skin tone -#1F482 1F3FF ; fully-qualified # 💂🏿 E1.0 guard: dark skin tone -#1F482 200D 2642 FE0F ; fully-qualified # 💂‍♂️ E4.0 man guard -#1F482 200D 2642 ; minimally-qualified # 💂‍♂ E4.0 man guard -#1F482 1F3FB 200D 2642 FE0F ; fully-qualified # 💂🏻‍♂️ E4.0 man guard: light skin tone -#1F482 1F3FB 200D 2642 ; minimally-qualified # 💂🏻‍♂ E4.0 man guard: light skin tone -#1F482 1F3FC 200D 2642 FE0F ; fully-qualified # 💂🏼‍♂️ E4.0 man guard: medium-light skin tone -#1F482 1F3FC 200D 2642 ; minimally-qualified # 💂🏼‍♂ E4.0 man guard: medium-light skin tone -#1F482 1F3FD 200D 2642 FE0F ; fully-qualified # 💂🏽‍♂️ E4.0 man guard: medium skin tone -#1F482 1F3FD 200D 2642 ; minimally-qualified # 💂🏽‍♂ E4.0 man guard: medium skin tone -#1F482 1F3FE 200D 2642 FE0F ; fully-qualified # 💂🏾‍♂️ E4.0 man guard: medium-dark skin tone -#1F482 1F3FE 200D 2642 ; minimally-qualified # 💂🏾‍♂ E4.0 man guard: medium-dark skin tone -#1F482 1F3FF 200D 2642 FE0F ; fully-qualified # 💂🏿‍♂️ E4.0 man guard: dark skin tone -#1F482 1F3FF 200D 2642 ; minimally-qualified # 💂🏿‍♂ E4.0 man guard: dark skin tone -#1F482 200D 2640 FE0F ; fully-qualified # 💂‍♀️ E4.0 woman guard -#1F482 200D 2640 ; minimally-qualified # 💂‍♀ E4.0 woman guard -#1F482 1F3FB 200D 2640 FE0F ; fully-qualified # 💂🏻‍♀️ E4.0 woman guard: light skin tone -#1F482 1F3FB 200D 2640 ; minimally-qualified # 💂🏻‍♀ E4.0 woman guard: light skin tone -#1F482 1F3FC 200D 2640 FE0F ; fully-qualified # 💂🏼‍♀️ E4.0 woman guard: medium-light skin tone -#1F482 1F3FC 200D 2640 ; minimally-qualified # 💂🏼‍♀ E4.0 woman guard: medium-light skin tone -#1F482 1F3FD 200D 2640 FE0F ; fully-qualified # 💂🏽‍♀️ E4.0 woman guard: medium skin tone -#1F482 1F3FD 200D 2640 ; minimally-qualified # 💂🏽‍♀ E4.0 woman guard: medium skin tone -#1F482 1F3FE 200D 2640 FE0F ; fully-qualified # 💂🏾‍♀️ E4.0 woman guard: medium-dark skin tone -#1F482 1F3FE 200D 2640 ; minimally-qualified # 💂🏾‍♀ E4.0 woman guard: medium-dark skin tone -#1F482 1F3FF 200D 2640 FE0F ; fully-qualified # 💂🏿‍♀️ E4.0 woman guard: dark skin tone -#1F482 1F3FF 200D 2640 ; minimally-qualified # 💂🏿‍♀ E4.0 woman guard: dark skin tone -#1F977 ; fully-qualified # 🥷 E13.0 ninja -#1F977 1F3FB ; fully-qualified # 🥷🏻 E13.0 ninja: light skin tone -#1F977 1F3FC ; fully-qualified # 🥷🏼 E13.0 ninja: medium-light skin tone -#1F977 1F3FD ; fully-qualified # 🥷🏽 E13.0 ninja: medium skin tone -#1F977 1F3FE ; fully-qualified # 🥷🏾 E13.0 ninja: medium-dark skin tone -#1F977 1F3FF ; fully-qualified # 🥷🏿 E13.0 ninja: dark skin tone -#1F477 ; fully-qualified # 👷 E0.6 construction worker -#1F477 1F3FB ; fully-qualified # 👷🏻 E1.0 construction worker: light skin tone -#1F477 1F3FC ; fully-qualified # 👷🏼 E1.0 construction worker: medium-light skin tone -#1F477 1F3FD ; fully-qualified # 👷🏽 E1.0 construction worker: medium skin tone -#1F477 1F3FE ; fully-qualified # 👷🏾 E1.0 construction worker: medium-dark skin tone -#1F477 1F3FF ; fully-qualified # 👷🏿 E1.0 construction worker: dark skin tone -#1F477 200D 2642 FE0F ; fully-qualified # 👷‍♂️ E4.0 man construction worker -#1F477 200D 2642 ; minimally-qualified # 👷‍♂ E4.0 man construction worker -#1F477 1F3FB 200D 2642 FE0F ; fully-qualified # 👷🏻‍♂️ E4.0 man construction worker: light skin tone -#1F477 1F3FB 200D 2642 ; minimally-qualified # 👷🏻‍♂ E4.0 man construction worker: light skin tone -#1F477 1F3FC 200D 2642 FE0F ; fully-qualified # 👷🏼‍♂️ E4.0 man construction worker: medium-light skin tone -#1F477 1F3FC 200D 2642 ; minimally-qualified # 👷🏼‍♂ E4.0 man construction worker: medium-light skin tone -#1F477 1F3FD 200D 2642 FE0F ; fully-qualified # 👷🏽‍♂️ E4.0 man construction worker: medium skin tone -#1F477 1F3FD 200D 2642 ; minimally-qualified # 👷🏽‍♂ E4.0 man construction worker: medium skin tone -#1F477 1F3FE 200D 2642 FE0F ; fully-qualified # 👷🏾‍♂️ E4.0 man construction worker: medium-dark skin tone -#1F477 1F3FE 200D 2642 ; minimally-qualified # 👷🏾‍♂ E4.0 man construction worker: medium-dark skin tone -#1F477 1F3FF 200D 2642 FE0F ; fully-qualified # 👷🏿‍♂️ E4.0 man construction worker: dark skin tone -#1F477 1F3FF 200D 2642 ; minimally-qualified # 👷🏿‍♂ E4.0 man construction worker: dark skin tone -#1F477 200D 2640 FE0F ; fully-qualified # 👷‍♀️ E4.0 woman construction worker -#1F477 200D 2640 ; minimally-qualified # 👷‍♀ E4.0 woman construction worker -#1F477 1F3FB 200D 2640 FE0F ; fully-qualified # 👷🏻‍♀️ E4.0 woman construction worker: light skin tone -#1F477 1F3FB 200D 2640 ; minimally-qualified # 👷🏻‍♀ E4.0 woman construction worker: light skin tone -#1F477 1F3FC 200D 2640 FE0F ; fully-qualified # 👷🏼‍♀️ E4.0 woman construction worker: medium-light skin tone -#1F477 1F3FC 200D 2640 ; minimally-qualified # 👷🏼‍♀ E4.0 woman construction worker: medium-light skin tone -#1F477 1F3FD 200D 2640 FE0F ; fully-qualified # 👷🏽‍♀️ E4.0 woman construction worker: medium skin tone -#1F477 1F3FD 200D 2640 ; minimally-qualified # 👷🏽‍♀ E4.0 woman construction worker: medium skin tone -#1F477 1F3FE 200D 2640 FE0F ; fully-qualified # 👷🏾‍♀️ E4.0 woman construction worker: medium-dark skin tone -#1F477 1F3FE 200D 2640 ; minimally-qualified # 👷🏾‍♀ E4.0 woman construction worker: medium-dark skin tone -#1F477 1F3FF 200D 2640 FE0F ; fully-qualified # 👷🏿‍♀️ E4.0 woman construction worker: dark skin tone -#1F477 1F3FF 200D 2640 ; minimally-qualified # 👷🏿‍♀ E4.0 woman construction worker: dark skin tone -#1FAC5 ; fully-qualified # 🫅 E14.0 person with crown -#1FAC5 1F3FB ; fully-qualified # 🫅🏻 E14.0 person with crown: light skin tone -#1FAC5 1F3FC ; fully-qualified # 🫅🏼 E14.0 person with crown: medium-light skin tone -#1FAC5 1F3FD ; fully-qualified # 🫅🏽 E14.0 person with crown: medium skin tone -#1FAC5 1F3FE ; fully-qualified # 🫅🏾 E14.0 person with crown: medium-dark skin tone -#1FAC5 1F3FF ; fully-qualified # 🫅🏿 E14.0 person with crown: dark skin tone -#1F934 ; fully-qualified # 🤴 E3.0 prince -#1F934 1F3FB ; fully-qualified # 🤴🏻 E3.0 prince: light skin tone -#1F934 1F3FC ; fully-qualified # 🤴🏼 E3.0 prince: medium-light skin tone -#1F934 1F3FD ; fully-qualified # 🤴🏽 E3.0 prince: medium skin tone -#1F934 1F3FE ; fully-qualified # 🤴🏾 E3.0 prince: medium-dark skin tone -#1F934 1F3FF ; fully-qualified # 🤴🏿 E3.0 prince: dark skin tone -#1F478 ; fully-qualified # 👸 E0.6 princess -#1F478 1F3FB ; fully-qualified # 👸🏻 E1.0 princess: light skin tone -#1F478 1F3FC ; fully-qualified # 👸🏼 E1.0 princess: medium-light skin tone -#1F478 1F3FD ; fully-qualified # 👸🏽 E1.0 princess: medium skin tone -#1F478 1F3FE ; fully-qualified # 👸🏾 E1.0 princess: medium-dark skin tone -#1F478 1F3FF ; fully-qualified # 👸🏿 E1.0 princess: dark skin tone -#1F473 ; fully-qualified # 👳 E0.6 person wearing turban -#1F473 1F3FB ; fully-qualified # 👳🏻 E1.0 person wearing turban: light skin tone -#1F473 1F3FC ; fully-qualified # 👳🏼 E1.0 person wearing turban: medium-light skin tone -#1F473 1F3FD ; fully-qualified # 👳🏽 E1.0 person wearing turban: medium skin tone -#1F473 1F3FE ; fully-qualified # 👳🏾 E1.0 person wearing turban: medium-dark skin tone -#1F473 1F3FF ; fully-qualified # 👳🏿 E1.0 person wearing turban: dark skin tone -#1F473 200D 2642 FE0F ; fully-qualified # 👳‍♂️ E4.0 man wearing turban -#1F473 200D 2642 ; minimally-qualified # 👳‍♂ E4.0 man wearing turban -#1F473 1F3FB 200D 2642 FE0F ; fully-qualified # 👳🏻‍♂️ E4.0 man wearing turban: light skin tone -#1F473 1F3FB 200D 2642 ; minimally-qualified # 👳🏻‍♂ E4.0 man wearing turban: light skin tone -#1F473 1F3FC 200D 2642 FE0F ; fully-qualified # 👳🏼‍♂️ E4.0 man wearing turban: medium-light skin tone -#1F473 1F3FC 200D 2642 ; minimally-qualified # 👳🏼‍♂ E4.0 man wearing turban: medium-light skin tone -#1F473 1F3FD 200D 2642 FE0F ; fully-qualified # 👳🏽‍♂️ E4.0 man wearing turban: medium skin tone -#1F473 1F3FD 200D 2642 ; minimally-qualified # 👳🏽‍♂ E4.0 man wearing turban: medium skin tone -#1F473 1F3FE 200D 2642 FE0F ; fully-qualified # 👳🏾‍♂️ E4.0 man wearing turban: medium-dark skin tone -#1F473 1F3FE 200D 2642 ; minimally-qualified # 👳🏾‍♂ E4.0 man wearing turban: medium-dark skin tone -#1F473 1F3FF 200D 2642 FE0F ; fully-qualified # 👳🏿‍♂️ E4.0 man wearing turban: dark skin tone -#1F473 1F3FF 200D 2642 ; minimally-qualified # 👳🏿‍♂ E4.0 man wearing turban: dark skin tone -#1F473 200D 2640 FE0F ; fully-qualified # 👳‍♀️ E4.0 woman wearing turban -#1F473 200D 2640 ; minimally-qualified # 👳‍♀ E4.0 woman wearing turban -#1F473 1F3FB 200D 2640 FE0F ; fully-qualified # 👳🏻‍♀️ E4.0 woman wearing turban: light skin tone -#1F473 1F3FB 200D 2640 ; minimally-qualified # 👳🏻‍♀ E4.0 woman wearing turban: light skin tone -#1F473 1F3FC 200D 2640 FE0F ; fully-qualified # 👳🏼‍♀️ E4.0 woman wearing turban: medium-light skin tone -#1F473 1F3FC 200D 2640 ; minimally-qualified # 👳🏼‍♀ E4.0 woman wearing turban: medium-light skin tone -#1F473 1F3FD 200D 2640 FE0F ; fully-qualified # 👳🏽‍♀️ E4.0 woman wearing turban: medium skin tone -#1F473 1F3FD 200D 2640 ; minimally-qualified # 👳🏽‍♀ E4.0 woman wearing turban: medium skin tone -#1F473 1F3FE 200D 2640 FE0F ; fully-qualified # 👳🏾‍♀️ E4.0 woman wearing turban: medium-dark skin tone -#1F473 1F3FE 200D 2640 ; minimally-qualified # 👳🏾‍♀ E4.0 woman wearing turban: medium-dark skin tone -#1F473 1F3FF 200D 2640 FE0F ; fully-qualified # 👳🏿‍♀️ E4.0 woman wearing turban: dark skin tone -#1F473 1F3FF 200D 2640 ; minimally-qualified # 👳🏿‍♀ E4.0 woman wearing turban: dark skin tone -#1F472 ; fully-qualified # 👲 E0.6 person with skullcap -#1F472 1F3FB ; fully-qualified # 👲🏻 E1.0 person with skullcap: light skin tone -#1F472 1F3FC ; fully-qualified # 👲🏼 E1.0 person with skullcap: medium-light skin tone -#1F472 1F3FD ; fully-qualified # 👲🏽 E1.0 person with skullcap: medium skin tone -#1F472 1F3FE ; fully-qualified # 👲🏾 E1.0 person with skullcap: medium-dark skin tone -#1F472 1F3FF ; fully-qualified # 👲🏿 E1.0 person with skullcap: dark skin tone -#1F9D5 ; fully-qualified # 🧕 E5.0 woman with headscarf -#1F9D5 1F3FB ; fully-qualified # 🧕🏻 E5.0 woman with headscarf: light skin tone -#1F9D5 1F3FC ; fully-qualified # 🧕🏼 E5.0 woman with headscarf: medium-light skin tone -#1F9D5 1F3FD ; fully-qualified # 🧕🏽 E5.0 woman with headscarf: medium skin tone -#1F9D5 1F3FE ; fully-qualified # 🧕🏾 E5.0 woman with headscarf: medium-dark skin tone -#1F9D5 1F3FF ; fully-qualified # 🧕🏿 E5.0 woman with headscarf: dark skin tone -#1F935 ; fully-qualified # 🤵 E3.0 person in tuxedo -#1F935 1F3FB ; fully-qualified # 🤵🏻 E3.0 person in tuxedo: light skin tone -#1F935 1F3FC ; fully-qualified # 🤵🏼 E3.0 person in tuxedo: medium-light skin tone -#1F935 1F3FD ; fully-qualified # 🤵🏽 E3.0 person in tuxedo: medium skin tone -#1F935 1F3FE ; fully-qualified # 🤵🏾 E3.0 person in tuxedo: medium-dark skin tone -#1F935 1F3FF ; fully-qualified # 🤵🏿 E3.0 person in tuxedo: dark skin tone -#1F935 200D 2642 FE0F ; fully-qualified # 🤵‍♂️ E13.0 man in tuxedo -#1F935 200D 2642 ; minimally-qualified # 🤵‍♂ E13.0 man in tuxedo -#1F935 1F3FB 200D 2642 FE0F ; fully-qualified # 🤵🏻‍♂️ E13.0 man in tuxedo: light skin tone -#1F935 1F3FB 200D 2642 ; minimally-qualified # 🤵🏻‍♂ E13.0 man in tuxedo: light skin tone -#1F935 1F3FC 200D 2642 FE0F ; fully-qualified # 🤵🏼‍♂️ E13.0 man in tuxedo: medium-light skin tone -#1F935 1F3FC 200D 2642 ; minimally-qualified # 🤵🏼‍♂ E13.0 man in tuxedo: medium-light skin tone -#1F935 1F3FD 200D 2642 FE0F ; fully-qualified # 🤵🏽‍♂️ E13.0 man in tuxedo: medium skin tone -#1F935 1F3FD 200D 2642 ; minimally-qualified # 🤵🏽‍♂ E13.0 man in tuxedo: medium skin tone -#1F935 1F3FE 200D 2642 FE0F ; fully-qualified # 🤵🏾‍♂️ E13.0 man in tuxedo: medium-dark skin tone -#1F935 1F3FE 200D 2642 ; minimally-qualified # 🤵🏾‍♂ E13.0 man in tuxedo: medium-dark skin tone -#1F935 1F3FF 200D 2642 FE0F ; fully-qualified # 🤵🏿‍♂️ E13.0 man in tuxedo: dark skin tone -#1F935 1F3FF 200D 2642 ; minimally-qualified # 🤵🏿‍♂ E13.0 man in tuxedo: dark skin tone -#1F935 200D 2640 FE0F ; fully-qualified # 🤵‍♀️ E13.0 woman in tuxedo -#1F935 200D 2640 ; minimally-qualified # 🤵‍♀ E13.0 woman in tuxedo -#1F935 1F3FB 200D 2640 FE0F ; fully-qualified # 🤵🏻‍♀️ E13.0 woman in tuxedo: light skin tone -#1F935 1F3FB 200D 2640 ; minimally-qualified # 🤵🏻‍♀ E13.0 woman in tuxedo: light skin tone -#1F935 1F3FC 200D 2640 FE0F ; fully-qualified # 🤵🏼‍♀️ E13.0 woman in tuxedo: medium-light skin tone -#1F935 1F3FC 200D 2640 ; minimally-qualified # 🤵🏼‍♀ E13.0 woman in tuxedo: medium-light skin tone -#1F935 1F3FD 200D 2640 FE0F ; fully-qualified # 🤵🏽‍♀️ E13.0 woman in tuxedo: medium skin tone -#1F935 1F3FD 200D 2640 ; minimally-qualified # 🤵🏽‍♀ E13.0 woman in tuxedo: medium skin tone -#1F935 1F3FE 200D 2640 FE0F ; fully-qualified # 🤵🏾‍♀️ E13.0 woman in tuxedo: medium-dark skin tone -#1F935 1F3FE 200D 2640 ; minimally-qualified # 🤵🏾‍♀ E13.0 woman in tuxedo: medium-dark skin tone -#1F935 1F3FF 200D 2640 FE0F ; fully-qualified # 🤵🏿‍♀️ E13.0 woman in tuxedo: dark skin tone -#1F935 1F3FF 200D 2640 ; minimally-qualified # 🤵🏿‍♀ E13.0 woman in tuxedo: dark skin tone -#1F470 ; fully-qualified # 👰 E0.6 person with veil -#1F470 1F3FB ; fully-qualified # 👰🏻 E1.0 person with veil: light skin tone -#1F470 1F3FC ; fully-qualified # 👰🏼 E1.0 person with veil: medium-light skin tone -#1F470 1F3FD ; fully-qualified # 👰🏽 E1.0 person with veil: medium skin tone -#1F470 1F3FE ; fully-qualified # 👰🏾 E1.0 person with veil: medium-dark skin tone -#1F470 1F3FF ; fully-qualified # 👰🏿 E1.0 person with veil: dark skin tone -#1F470 200D 2642 FE0F ; fully-qualified # 👰‍♂️ E13.0 man with veil -#1F470 200D 2642 ; minimally-qualified # 👰‍♂ E13.0 man with veil -#1F470 1F3FB 200D 2642 FE0F ; fully-qualified # 👰🏻‍♂️ E13.0 man with veil: light skin tone -#1F470 1F3FB 200D 2642 ; minimally-qualified # 👰🏻‍♂ E13.0 man with veil: light skin tone -#1F470 1F3FC 200D 2642 FE0F ; fully-qualified # 👰🏼‍♂️ E13.0 man with veil: medium-light skin tone -#1F470 1F3FC 200D 2642 ; minimally-qualified # 👰🏼‍♂ E13.0 man with veil: medium-light skin tone -#1F470 1F3FD 200D 2642 FE0F ; fully-qualified # 👰🏽‍♂️ E13.0 man with veil: medium skin tone -#1F470 1F3FD 200D 2642 ; minimally-qualified # 👰🏽‍♂ E13.0 man with veil: medium skin tone -#1F470 1F3FE 200D 2642 FE0F ; fully-qualified # 👰🏾‍♂️ E13.0 man with veil: medium-dark skin tone -#1F470 1F3FE 200D 2642 ; minimally-qualified # 👰🏾‍♂ E13.0 man with veil: medium-dark skin tone -#1F470 1F3FF 200D 2642 FE0F ; fully-qualified # 👰🏿‍♂️ E13.0 man with veil: dark skin tone -#1F470 1F3FF 200D 2642 ; minimally-qualified # 👰🏿‍♂ E13.0 man with veil: dark skin tone -#1F470 200D 2640 FE0F ; fully-qualified # 👰‍♀️ E13.0 woman with veil -#1F470 200D 2640 ; minimally-qualified # 👰‍♀ E13.0 woman with veil -#1F470 1F3FB 200D 2640 FE0F ; fully-qualified # 👰🏻‍♀️ E13.0 woman with veil: light skin tone -#1F470 1F3FB 200D 2640 ; minimally-qualified # 👰🏻‍♀ E13.0 woman with veil: light skin tone -#1F470 1F3FC 200D 2640 FE0F ; fully-qualified # 👰🏼‍♀️ E13.0 woman with veil: medium-light skin tone -#1F470 1F3FC 200D 2640 ; minimally-qualified # 👰🏼‍♀ E13.0 woman with veil: medium-light skin tone -#1F470 1F3FD 200D 2640 FE0F ; fully-qualified # 👰🏽‍♀️ E13.0 woman with veil: medium skin tone -#1F470 1F3FD 200D 2640 ; minimally-qualified # 👰🏽‍♀ E13.0 woman with veil: medium skin tone -#1F470 1F3FE 200D 2640 FE0F ; fully-qualified # 👰🏾‍♀️ E13.0 woman with veil: medium-dark skin tone -#1F470 1F3FE 200D 2640 ; minimally-qualified # 👰🏾‍♀ E13.0 woman with veil: medium-dark skin tone -#1F470 1F3FF 200D 2640 FE0F ; fully-qualified # 👰🏿‍♀️ E13.0 woman with veil: dark skin tone -#1F470 1F3FF 200D 2640 ; minimally-qualified # 👰🏿‍♀ E13.0 woman with veil: dark skin tone -#1F930 ; fully-qualified # 🤰 E3.0 pregnant woman -#1F930 1F3FB ; fully-qualified # 🤰🏻 E3.0 pregnant woman: light skin tone -#1F930 1F3FC ; fully-qualified # 🤰🏼 E3.0 pregnant woman: medium-light skin tone -#1F930 1F3FD ; fully-qualified # 🤰🏽 E3.0 pregnant woman: medium skin tone -#1F930 1F3FE ; fully-qualified # 🤰🏾 E3.0 pregnant woman: medium-dark skin tone -#1F930 1F3FF ; fully-qualified # 🤰🏿 E3.0 pregnant woman: dark skin tone -#1FAC3 ; fully-qualified # 🫃 E14.0 pregnant man -#1FAC3 1F3FB ; fully-qualified # 🫃🏻 E14.0 pregnant man: light skin tone -#1FAC3 1F3FC ; fully-qualified # 🫃🏼 E14.0 pregnant man: medium-light skin tone -#1FAC3 1F3FD ; fully-qualified # 🫃🏽 E14.0 pregnant man: medium skin tone -#1FAC3 1F3FE ; fully-qualified # 🫃🏾 E14.0 pregnant man: medium-dark skin tone -#1FAC3 1F3FF ; fully-qualified # 🫃🏿 E14.0 pregnant man: dark skin tone -#1FAC4 ; fully-qualified # 🫄 E14.0 pregnant person -#1FAC4 1F3FB ; fully-qualified # 🫄🏻 E14.0 pregnant person: light skin tone -#1FAC4 1F3FC ; fully-qualified # 🫄🏼 E14.0 pregnant person: medium-light skin tone -#1FAC4 1F3FD ; fully-qualified # 🫄🏽 E14.0 pregnant person: medium skin tone -#1FAC4 1F3FE ; fully-qualified # 🫄🏾 E14.0 pregnant person: medium-dark skin tone -#1FAC4 1F3FF ; fully-qualified # 🫄🏿 E14.0 pregnant person: dark skin tone -#1F931 ; fully-qualified # 🤱 E5.0 breast-feeding -#1F931 1F3FB ; fully-qualified # 🤱🏻 E5.0 breast-feeding: light skin tone -#1F931 1F3FC ; fully-qualified # 🤱🏼 E5.0 breast-feeding: medium-light skin tone -#1F931 1F3FD ; fully-qualified # 🤱🏽 E5.0 breast-feeding: medium skin tone -#1F931 1F3FE ; fully-qualified # 🤱🏾 E5.0 breast-feeding: medium-dark skin tone -#1F931 1F3FF ; fully-qualified # 🤱🏿 E5.0 breast-feeding: dark skin tone -#1F469 200D 1F37C ; fully-qualified # 👩‍🍼 E13.0 woman feeding baby -#1F469 1F3FB 200D 1F37C ; fully-qualified # 👩🏻‍🍼 E13.0 woman feeding baby: light skin tone -#1F469 1F3FC 200D 1F37C ; fully-qualified # 👩🏼‍🍼 E13.0 woman feeding baby: medium-light skin tone -#1F469 1F3FD 200D 1F37C ; fully-qualified # 👩🏽‍🍼 E13.0 woman feeding baby: medium skin tone -#1F469 1F3FE 200D 1F37C ; fully-qualified # 👩🏾‍🍼 E13.0 woman feeding baby: medium-dark skin tone -#1F469 1F3FF 200D 1F37C ; fully-qualified # 👩🏿‍🍼 E13.0 woman feeding baby: dark skin tone -#1F468 200D 1F37C ; fully-qualified # 👨‍🍼 E13.0 man feeding baby -#1F468 1F3FB 200D 1F37C ; fully-qualified # 👨🏻‍🍼 E13.0 man feeding baby: light skin tone -#1F468 1F3FC 200D 1F37C ; fully-qualified # 👨🏼‍🍼 E13.0 man feeding baby: medium-light skin tone -#1F468 1F3FD 200D 1F37C ; fully-qualified # 👨🏽‍🍼 E13.0 man feeding baby: medium skin tone -#1F468 1F3FE 200D 1F37C ; fully-qualified # 👨🏾‍🍼 E13.0 man feeding baby: medium-dark skin tone -#1F468 1F3FF 200D 1F37C ; fully-qualified # 👨🏿‍🍼 E13.0 man feeding baby: dark skin tone -#1F9D1 200D 1F37C ; fully-qualified # 🧑‍🍼 E13.0 person feeding baby -#1F9D1 1F3FB 200D 1F37C ; fully-qualified # 🧑🏻‍🍼 E13.0 person feeding baby: light skin tone -#1F9D1 1F3FC 200D 1F37C ; fully-qualified # 🧑🏼‍🍼 E13.0 person feeding baby: medium-light skin tone -#1F9D1 1F3FD 200D 1F37C ; fully-qualified # 🧑🏽‍🍼 E13.0 person feeding baby: medium skin tone -#1F9D1 1F3FE 200D 1F37C ; fully-qualified # 🧑🏾‍🍼 E13.0 person feeding baby: medium-dark skin tone -#1F9D1 1F3FF 200D 1F37C ; fully-qualified # 🧑🏿‍🍼 E13.0 person feeding baby: dark skin tone -# -## subgroup: person-fantasy -#1F47C ; fully-qualified # 👼 E0.6 baby angel -#1F47C 1F3FB ; fully-qualified # 👼🏻 E1.0 baby angel: light skin tone -#1F47C 1F3FC ; fully-qualified # 👼🏼 E1.0 baby angel: medium-light skin tone -#1F47C 1F3FD ; fully-qualified # 👼🏽 E1.0 baby angel: medium skin tone -#1F47C 1F3FE ; fully-qualified # 👼🏾 E1.0 baby angel: medium-dark skin tone -#1F47C 1F3FF ; fully-qualified # 👼🏿 E1.0 baby angel: dark skin tone -#1F385 ; fully-qualified # 🎅 E0.6 Santa Claus -#1F385 1F3FB ; fully-qualified # 🎅🏻 E1.0 Santa Claus: light skin tone -#1F385 1F3FC ; fully-qualified # 🎅🏼 E1.0 Santa Claus: medium-light skin tone -#1F385 1F3FD ; fully-qualified # 🎅🏽 E1.0 Santa Claus: medium skin tone -#1F385 1F3FE ; fully-qualified # 🎅🏾 E1.0 Santa Claus: medium-dark skin tone -#1F385 1F3FF ; fully-qualified # 🎅🏿 E1.0 Santa Claus: dark skin tone -#1F936 ; fully-qualified # 🤶 E3.0 Mrs. Claus -#1F936 1F3FB ; fully-qualified # 🤶🏻 E3.0 Mrs. Claus: light skin tone -#1F936 1F3FC ; fully-qualified # 🤶🏼 E3.0 Mrs. Claus: medium-light skin tone -#1F936 1F3FD ; fully-qualified # 🤶🏽 E3.0 Mrs. Claus: medium skin tone -#1F936 1F3FE ; fully-qualified # 🤶🏾 E3.0 Mrs. Claus: medium-dark skin tone -#1F936 1F3FF ; fully-qualified # 🤶🏿 E3.0 Mrs. Claus: dark skin tone -#1F9D1 200D 1F384 ; fully-qualified # 🧑‍🎄 E13.0 mx claus -#1F9D1 1F3FB 200D 1F384 ; fully-qualified # 🧑🏻‍🎄 E13.0 mx claus: light skin tone -#1F9D1 1F3FC 200D 1F384 ; fully-qualified # 🧑🏼‍🎄 E13.0 mx claus: medium-light skin tone -#1F9D1 1F3FD 200D 1F384 ; fully-qualified # 🧑🏽‍🎄 E13.0 mx claus: medium skin tone -#1F9D1 1F3FE 200D 1F384 ; fully-qualified # 🧑🏾‍🎄 E13.0 mx claus: medium-dark skin tone -#1F9D1 1F3FF 200D 1F384 ; fully-qualified # 🧑🏿‍🎄 E13.0 mx claus: dark skin tone -#1F9B8 ; fully-qualified # 🦸 E11.0 superhero -#1F9B8 1F3FB ; fully-qualified # 🦸🏻 E11.0 superhero: light skin tone -#1F9B8 1F3FC ; fully-qualified # 🦸🏼 E11.0 superhero: medium-light skin tone -#1F9B8 1F3FD ; fully-qualified # 🦸🏽 E11.0 superhero: medium skin tone -#1F9B8 1F3FE ; fully-qualified # 🦸🏾 E11.0 superhero: medium-dark skin tone -#1F9B8 1F3FF ; fully-qualified # 🦸🏿 E11.0 superhero: dark skin tone -#1F9B8 200D 2642 FE0F ; fully-qualified # 🦸‍♂️ E11.0 man superhero -#1F9B8 200D 2642 ; minimally-qualified # 🦸‍♂ E11.0 man superhero -#1F9B8 1F3FB 200D 2642 FE0F ; fully-qualified # 🦸🏻‍♂️ E11.0 man superhero: light skin tone -#1F9B8 1F3FB 200D 2642 ; minimally-qualified # 🦸🏻‍♂ E11.0 man superhero: light skin tone -#1F9B8 1F3FC 200D 2642 FE0F ; fully-qualified # 🦸🏼‍♂️ E11.0 man superhero: medium-light skin tone -#1F9B8 1F3FC 200D 2642 ; minimally-qualified # 🦸🏼‍♂ E11.0 man superhero: medium-light skin tone -#1F9B8 1F3FD 200D 2642 FE0F ; fully-qualified # 🦸🏽‍♂️ E11.0 man superhero: medium skin tone -#1F9B8 1F3FD 200D 2642 ; minimally-qualified # 🦸🏽‍♂ E11.0 man superhero: medium skin tone -#1F9B8 1F3FE 200D 2642 FE0F ; fully-qualified # 🦸🏾‍♂️ E11.0 man superhero: medium-dark skin tone -#1F9B8 1F3FE 200D 2642 ; minimally-qualified # 🦸🏾‍♂ E11.0 man superhero: medium-dark skin tone -#1F9B8 1F3FF 200D 2642 FE0F ; fully-qualified # 🦸🏿‍♂️ E11.0 man superhero: dark skin tone -#1F9B8 1F3FF 200D 2642 ; minimally-qualified # 🦸🏿‍♂ E11.0 man superhero: dark skin tone -#1F9B8 200D 2640 FE0F ; fully-qualified # 🦸‍♀️ E11.0 woman superhero -#1F9B8 200D 2640 ; minimally-qualified # 🦸‍♀ E11.0 woman superhero -#1F9B8 1F3FB 200D 2640 FE0F ; fully-qualified # 🦸🏻‍♀️ E11.0 woman superhero: light skin tone -#1F9B8 1F3FB 200D 2640 ; minimally-qualified # 🦸🏻‍♀ E11.0 woman superhero: light skin tone -#1F9B8 1F3FC 200D 2640 FE0F ; fully-qualified # 🦸🏼‍♀️ E11.0 woman superhero: medium-light skin tone -#1F9B8 1F3FC 200D 2640 ; minimally-qualified # 🦸🏼‍♀ E11.0 woman superhero: medium-light skin tone -#1F9B8 1F3FD 200D 2640 FE0F ; fully-qualified # 🦸🏽‍♀️ E11.0 woman superhero: medium skin tone -#1F9B8 1F3FD 200D 2640 ; minimally-qualified # 🦸🏽‍♀ E11.0 woman superhero: medium skin tone -#1F9B8 1F3FE 200D 2640 FE0F ; fully-qualified # 🦸🏾‍♀️ E11.0 woman superhero: medium-dark skin tone -#1F9B8 1F3FE 200D 2640 ; minimally-qualified # 🦸🏾‍♀ E11.0 woman superhero: medium-dark skin tone -#1F9B8 1F3FF 200D 2640 FE0F ; fully-qualified # 🦸🏿‍♀️ E11.0 woman superhero: dark skin tone -#1F9B8 1F3FF 200D 2640 ; minimally-qualified # 🦸🏿‍♀ E11.0 woman superhero: dark skin tone -#1F9B9 ; fully-qualified # 🦹 E11.0 supervillain -#1F9B9 1F3FB ; fully-qualified # 🦹🏻 E11.0 supervillain: light skin tone -#1F9B9 1F3FC ; fully-qualified # 🦹🏼 E11.0 supervillain: medium-light skin tone -#1F9B9 1F3FD ; fully-qualified # 🦹🏽 E11.0 supervillain: medium skin tone -#1F9B9 1F3FE ; fully-qualified # 🦹🏾 E11.0 supervillain: medium-dark skin tone -#1F9B9 1F3FF ; fully-qualified # 🦹🏿 E11.0 supervillain: dark skin tone -#1F9B9 200D 2642 FE0F ; fully-qualified # 🦹‍♂️ E11.0 man supervillain -#1F9B9 200D 2642 ; minimally-qualified # 🦹‍♂ E11.0 man supervillain -#1F9B9 1F3FB 200D 2642 FE0F ; fully-qualified # 🦹🏻‍♂️ E11.0 man supervillain: light skin tone -#1F9B9 1F3FB 200D 2642 ; minimally-qualified # 🦹🏻‍♂ E11.0 man supervillain: light skin tone -#1F9B9 1F3FC 200D 2642 FE0F ; fully-qualified # 🦹🏼‍♂️ E11.0 man supervillain: medium-light skin tone -#1F9B9 1F3FC 200D 2642 ; minimally-qualified # 🦹🏼‍♂ E11.0 man supervillain: medium-light skin tone -#1F9B9 1F3FD 200D 2642 FE0F ; fully-qualified # 🦹🏽‍♂️ E11.0 man supervillain: medium skin tone -#1F9B9 1F3FD 200D 2642 ; minimally-qualified # 🦹🏽‍♂ E11.0 man supervillain: medium skin tone -#1F9B9 1F3FE 200D 2642 FE0F ; fully-qualified # 🦹🏾‍♂️ E11.0 man supervillain: medium-dark skin tone -#1F9B9 1F3FE 200D 2642 ; minimally-qualified # 🦹🏾‍♂ E11.0 man supervillain: medium-dark skin tone -#1F9B9 1F3FF 200D 2642 FE0F ; fully-qualified # 🦹🏿‍♂️ E11.0 man supervillain: dark skin tone -#1F9B9 1F3FF 200D 2642 ; minimally-qualified # 🦹🏿‍♂ E11.0 man supervillain: dark skin tone -#1F9B9 200D 2640 FE0F ; fully-qualified # 🦹‍♀️ E11.0 woman supervillain -#1F9B9 200D 2640 ; minimally-qualified # 🦹‍♀ E11.0 woman supervillain -#1F9B9 1F3FB 200D 2640 FE0F ; fully-qualified # 🦹🏻‍♀️ E11.0 woman supervillain: light skin tone -#1F9B9 1F3FB 200D 2640 ; minimally-qualified # 🦹🏻‍♀ E11.0 woman supervillain: light skin tone -#1F9B9 1F3FC 200D 2640 FE0F ; fully-qualified # 🦹🏼‍♀️ E11.0 woman supervillain: medium-light skin tone -#1F9B9 1F3FC 200D 2640 ; minimally-qualified # 🦹🏼‍♀ E11.0 woman supervillain: medium-light skin tone -#1F9B9 1F3FD 200D 2640 FE0F ; fully-qualified # 🦹🏽‍♀️ E11.0 woman supervillain: medium skin tone -#1F9B9 1F3FD 200D 2640 ; minimally-qualified # 🦹🏽‍♀ E11.0 woman supervillain: medium skin tone -#1F9B9 1F3FE 200D 2640 FE0F ; fully-qualified # 🦹🏾‍♀️ E11.0 woman supervillain: medium-dark skin tone -#1F9B9 1F3FE 200D 2640 ; minimally-qualified # 🦹🏾‍♀ E11.0 woman supervillain: medium-dark skin tone -#1F9B9 1F3FF 200D 2640 FE0F ; fully-qualified # 🦹🏿‍♀️ E11.0 woman supervillain: dark skin tone -#1F9B9 1F3FF 200D 2640 ; minimally-qualified # 🦹🏿‍♀ E11.0 woman supervillain: dark skin tone -#1F9D9 ; fully-qualified # 🧙 E5.0 mage -#1F9D9 1F3FB ; fully-qualified # 🧙🏻 E5.0 mage: light skin tone -#1F9D9 1F3FC ; fully-qualified # 🧙🏼 E5.0 mage: medium-light skin tone -#1F9D9 1F3FD ; fully-qualified # 🧙🏽 E5.0 mage: medium skin tone -#1F9D9 1F3FE ; fully-qualified # 🧙🏾 E5.0 mage: medium-dark skin tone -#1F9D9 1F3FF ; fully-qualified # 🧙🏿 E5.0 mage: dark skin tone -#1F9D9 200D 2642 FE0F ; fully-qualified # 🧙‍♂️ E5.0 man mage -#1F9D9 200D 2642 ; minimally-qualified # 🧙‍♂ E5.0 man mage -#1F9D9 1F3FB 200D 2642 FE0F ; fully-qualified # 🧙🏻‍♂️ E5.0 man mage: light skin tone -#1F9D9 1F3FB 200D 2642 ; minimally-qualified # 🧙🏻‍♂ E5.0 man mage: light skin tone -#1F9D9 1F3FC 200D 2642 FE0F ; fully-qualified # 🧙🏼‍♂️ E5.0 man mage: medium-light skin tone -#1F9D9 1F3FC 200D 2642 ; minimally-qualified # 🧙🏼‍♂ E5.0 man mage: medium-light skin tone -#1F9D9 1F3FD 200D 2642 FE0F ; fully-qualified # 🧙🏽‍♂️ E5.0 man mage: medium skin tone -#1F9D9 1F3FD 200D 2642 ; minimally-qualified # 🧙🏽‍♂ E5.0 man mage: medium skin tone -#1F9D9 1F3FE 200D 2642 FE0F ; fully-qualified # 🧙🏾‍♂️ E5.0 man mage: medium-dark skin tone -#1F9D9 1F3FE 200D 2642 ; minimally-qualified # 🧙🏾‍♂ E5.0 man mage: medium-dark skin tone -#1F9D9 1F3FF 200D 2642 FE0F ; fully-qualified # 🧙🏿‍♂️ E5.0 man mage: dark skin tone -#1F9D9 1F3FF 200D 2642 ; minimally-qualified # 🧙🏿‍♂ E5.0 man mage: dark skin tone -#1F9D9 200D 2640 FE0F ; fully-qualified # 🧙‍♀️ E5.0 woman mage -#1F9D9 200D 2640 ; minimally-qualified # 🧙‍♀ E5.0 woman mage -#1F9D9 1F3FB 200D 2640 FE0F ; fully-qualified # 🧙🏻‍♀️ E5.0 woman mage: light skin tone -#1F9D9 1F3FB 200D 2640 ; minimally-qualified # 🧙🏻‍♀ E5.0 woman mage: light skin tone -#1F9D9 1F3FC 200D 2640 FE0F ; fully-qualified # 🧙🏼‍♀️ E5.0 woman mage: medium-light skin tone -#1F9D9 1F3FC 200D 2640 ; minimally-qualified # 🧙🏼‍♀ E5.0 woman mage: medium-light skin tone -#1F9D9 1F3FD 200D 2640 FE0F ; fully-qualified # 🧙🏽‍♀️ E5.0 woman mage: medium skin tone -#1F9D9 1F3FD 200D 2640 ; minimally-qualified # 🧙🏽‍♀ E5.0 woman mage: medium skin tone -#1F9D9 1F3FE 200D 2640 FE0F ; fully-qualified # 🧙🏾‍♀️ E5.0 woman mage: medium-dark skin tone -#1F9D9 1F3FE 200D 2640 ; minimally-qualified # 🧙🏾‍♀ E5.0 woman mage: medium-dark skin tone -#1F9D9 1F3FF 200D 2640 FE0F ; fully-qualified # 🧙🏿‍♀️ E5.0 woman mage: dark skin tone -#1F9D9 1F3FF 200D 2640 ; minimally-qualified # 🧙🏿‍♀ E5.0 woman mage: dark skin tone -#1F9DA ; fully-qualified # 🧚 E5.0 fairy -#1F9DA 1F3FB ; fully-qualified # 🧚🏻 E5.0 fairy: light skin tone -#1F9DA 1F3FC ; fully-qualified # 🧚🏼 E5.0 fairy: medium-light skin tone -#1F9DA 1F3FD ; fully-qualified # 🧚🏽 E5.0 fairy: medium skin tone -#1F9DA 1F3FE ; fully-qualified # 🧚🏾 E5.0 fairy: medium-dark skin tone -#1F9DA 1F3FF ; fully-qualified # 🧚🏿 E5.0 fairy: dark skin tone -#1F9DA 200D 2642 FE0F ; fully-qualified # 🧚‍♂️ E5.0 man fairy -#1F9DA 200D 2642 ; minimally-qualified # 🧚‍♂ E5.0 man fairy -#1F9DA 1F3FB 200D 2642 FE0F ; fully-qualified # 🧚🏻‍♂️ E5.0 man fairy: light skin tone -#1F9DA 1F3FB 200D 2642 ; minimally-qualified # 🧚🏻‍♂ E5.0 man fairy: light skin tone -#1F9DA 1F3FC 200D 2642 FE0F ; fully-qualified # 🧚🏼‍♂️ E5.0 man fairy: medium-light skin tone -#1F9DA 1F3FC 200D 2642 ; minimally-qualified # 🧚🏼‍♂ E5.0 man fairy: medium-light skin tone -#1F9DA 1F3FD 200D 2642 FE0F ; fully-qualified # 🧚🏽‍♂️ E5.0 man fairy: medium skin tone -#1F9DA 1F3FD 200D 2642 ; minimally-qualified # 🧚🏽‍♂ E5.0 man fairy: medium skin tone -#1F9DA 1F3FE 200D 2642 FE0F ; fully-qualified # 🧚🏾‍♂️ E5.0 man fairy: medium-dark skin tone -#1F9DA 1F3FE 200D 2642 ; minimally-qualified # 🧚🏾‍♂ E5.0 man fairy: medium-dark skin tone -#1F9DA 1F3FF 200D 2642 FE0F ; fully-qualified # 🧚🏿‍♂️ E5.0 man fairy: dark skin tone -#1F9DA 1F3FF 200D 2642 ; minimally-qualified # 🧚🏿‍♂ E5.0 man fairy: dark skin tone -#1F9DA 200D 2640 FE0F ; fully-qualified # 🧚‍♀️ E5.0 woman fairy -#1F9DA 200D 2640 ; minimally-qualified # 🧚‍♀ E5.0 woman fairy -#1F9DA 1F3FB 200D 2640 FE0F ; fully-qualified # 🧚🏻‍♀️ E5.0 woman fairy: light skin tone -#1F9DA 1F3FB 200D 2640 ; minimally-qualified # 🧚🏻‍♀ E5.0 woman fairy: light skin tone -#1F9DA 1F3FC 200D 2640 FE0F ; fully-qualified # 🧚🏼‍♀️ E5.0 woman fairy: medium-light skin tone -#1F9DA 1F3FC 200D 2640 ; minimally-qualified # 🧚🏼‍♀ E5.0 woman fairy: medium-light skin tone -#1F9DA 1F3FD 200D 2640 FE0F ; fully-qualified # 🧚🏽‍♀️ E5.0 woman fairy: medium skin tone -#1F9DA 1F3FD 200D 2640 ; minimally-qualified # 🧚🏽‍♀ E5.0 woman fairy: medium skin tone -#1F9DA 1F3FE 200D 2640 FE0F ; fully-qualified # 🧚🏾‍♀️ E5.0 woman fairy: medium-dark skin tone -#1F9DA 1F3FE 200D 2640 ; minimally-qualified # 🧚🏾‍♀ E5.0 woman fairy: medium-dark skin tone -#1F9DA 1F3FF 200D 2640 FE0F ; fully-qualified # 🧚🏿‍♀️ E5.0 woman fairy: dark skin tone -#1F9DA 1F3FF 200D 2640 ; minimally-qualified # 🧚🏿‍♀ E5.0 woman fairy: dark skin tone -#1F9DB ; fully-qualified # 🧛 E5.0 vampire -#1F9DB 1F3FB ; fully-qualified # 🧛🏻 E5.0 vampire: light skin tone -#1F9DB 1F3FC ; fully-qualified # 🧛🏼 E5.0 vampire: medium-light skin tone -#1F9DB 1F3FD ; fully-qualified # 🧛🏽 E5.0 vampire: medium skin tone -#1F9DB 1F3FE ; fully-qualified # 🧛🏾 E5.0 vampire: medium-dark skin tone -#1F9DB 1F3FF ; fully-qualified # 🧛🏿 E5.0 vampire: dark skin tone -#1F9DB 200D 2642 FE0F ; fully-qualified # 🧛‍♂️ E5.0 man vampire -#1F9DB 200D 2642 ; minimally-qualified # 🧛‍♂ E5.0 man vampire -#1F9DB 1F3FB 200D 2642 FE0F ; fully-qualified # 🧛🏻‍♂️ E5.0 man vampire: light skin tone -#1F9DB 1F3FB 200D 2642 ; minimally-qualified # 🧛🏻‍♂ E5.0 man vampire: light skin tone -#1F9DB 1F3FC 200D 2642 FE0F ; fully-qualified # 🧛🏼‍♂️ E5.0 man vampire: medium-light skin tone -#1F9DB 1F3FC 200D 2642 ; minimally-qualified # 🧛🏼‍♂ E5.0 man vampire: medium-light skin tone -#1F9DB 1F3FD 200D 2642 FE0F ; fully-qualified # 🧛🏽‍♂️ E5.0 man vampire: medium skin tone -#1F9DB 1F3FD 200D 2642 ; minimally-qualified # 🧛🏽‍♂ E5.0 man vampire: medium skin tone -#1F9DB 1F3FE 200D 2642 FE0F ; fully-qualified # 🧛🏾‍♂️ E5.0 man vampire: medium-dark skin tone -#1F9DB 1F3FE 200D 2642 ; minimally-qualified # 🧛🏾‍♂ E5.0 man vampire: medium-dark skin tone -#1F9DB 1F3FF 200D 2642 FE0F ; fully-qualified # 🧛🏿‍♂️ E5.0 man vampire: dark skin tone -#1F9DB 1F3FF 200D 2642 ; minimally-qualified # 🧛🏿‍♂ E5.0 man vampire: dark skin tone -#1F9DB 200D 2640 FE0F ; fully-qualified # 🧛‍♀️ E5.0 woman vampire -#1F9DB 200D 2640 ; minimally-qualified # 🧛‍♀ E5.0 woman vampire -#1F9DB 1F3FB 200D 2640 FE0F ; fully-qualified # 🧛🏻‍♀️ E5.0 woman vampire: light skin tone -#1F9DB 1F3FB 200D 2640 ; minimally-qualified # 🧛🏻‍♀ E5.0 woman vampire: light skin tone -#1F9DB 1F3FC 200D 2640 FE0F ; fully-qualified # 🧛🏼‍♀️ E5.0 woman vampire: medium-light skin tone -#1F9DB 1F3FC 200D 2640 ; minimally-qualified # 🧛🏼‍♀ E5.0 woman vampire: medium-light skin tone -#1F9DB 1F3FD 200D 2640 FE0F ; fully-qualified # 🧛🏽‍♀️ E5.0 woman vampire: medium skin tone -#1F9DB 1F3FD 200D 2640 ; minimally-qualified # 🧛🏽‍♀ E5.0 woman vampire: medium skin tone -#1F9DB 1F3FE 200D 2640 FE0F ; fully-qualified # 🧛🏾‍♀️ E5.0 woman vampire: medium-dark skin tone -#1F9DB 1F3FE 200D 2640 ; minimally-qualified # 🧛🏾‍♀ E5.0 woman vampire: medium-dark skin tone -#1F9DB 1F3FF 200D 2640 FE0F ; fully-qualified # 🧛🏿‍♀️ E5.0 woman vampire: dark skin tone -#1F9DB 1F3FF 200D 2640 ; minimally-qualified # 🧛🏿‍♀ E5.0 woman vampire: dark skin tone -#1F9DC ; fully-qualified # 🧜 E5.0 merperson -#1F9DC 1F3FB ; fully-qualified # 🧜🏻 E5.0 merperson: light skin tone -#1F9DC 1F3FC ; fully-qualified # 🧜🏼 E5.0 merperson: medium-light skin tone -#1F9DC 1F3FD ; fully-qualified # 🧜🏽 E5.0 merperson: medium skin tone -#1F9DC 1F3FE ; fully-qualified # 🧜🏾 E5.0 merperson: medium-dark skin tone -#1F9DC 1F3FF ; fully-qualified # 🧜🏿 E5.0 merperson: dark skin tone -#1F9DC 200D 2642 FE0F ; fully-qualified # 🧜‍♂️ E5.0 merman -#1F9DC 200D 2642 ; minimally-qualified # 🧜‍♂ E5.0 merman -#1F9DC 1F3FB 200D 2642 FE0F ; fully-qualified # 🧜🏻‍♂️ E5.0 merman: light skin tone -#1F9DC 1F3FB 200D 2642 ; minimally-qualified # 🧜🏻‍♂ E5.0 merman: light skin tone -#1F9DC 1F3FC 200D 2642 FE0F ; fully-qualified # 🧜🏼‍♂️ E5.0 merman: medium-light skin tone -#1F9DC 1F3FC 200D 2642 ; minimally-qualified # 🧜🏼‍♂ E5.0 merman: medium-light skin tone -#1F9DC 1F3FD 200D 2642 FE0F ; fully-qualified # 🧜🏽‍♂️ E5.0 merman: medium skin tone -#1F9DC 1F3FD 200D 2642 ; minimally-qualified # 🧜🏽‍♂ E5.0 merman: medium skin tone -#1F9DC 1F3FE 200D 2642 FE0F ; fully-qualified # 🧜🏾‍♂️ E5.0 merman: medium-dark skin tone -#1F9DC 1F3FE 200D 2642 ; minimally-qualified # 🧜🏾‍♂ E5.0 merman: medium-dark skin tone -#1F9DC 1F3FF 200D 2642 FE0F ; fully-qualified # 🧜🏿‍♂️ E5.0 merman: dark skin tone -#1F9DC 1F3FF 200D 2642 ; minimally-qualified # 🧜🏿‍♂ E5.0 merman: dark skin tone -#1F9DC 200D 2640 FE0F ; fully-qualified # 🧜‍♀️ E5.0 mermaid -#1F9DC 200D 2640 ; minimally-qualified # 🧜‍♀ E5.0 mermaid -#1F9DC 1F3FB 200D 2640 FE0F ; fully-qualified # 🧜🏻‍♀️ E5.0 mermaid: light skin tone -#1F9DC 1F3FB 200D 2640 ; minimally-qualified # 🧜🏻‍♀ E5.0 mermaid: light skin tone -#1F9DC 1F3FC 200D 2640 FE0F ; fully-qualified # 🧜🏼‍♀️ E5.0 mermaid: medium-light skin tone -#1F9DC 1F3FC 200D 2640 ; minimally-qualified # 🧜🏼‍♀ E5.0 mermaid: medium-light skin tone -#1F9DC 1F3FD 200D 2640 FE0F ; fully-qualified # 🧜🏽‍♀️ E5.0 mermaid: medium skin tone -#1F9DC 1F3FD 200D 2640 ; minimally-qualified # 🧜🏽‍♀ E5.0 mermaid: medium skin tone -#1F9DC 1F3FE 200D 2640 FE0F ; fully-qualified # 🧜🏾‍♀️ E5.0 mermaid: medium-dark skin tone -#1F9DC 1F3FE 200D 2640 ; minimally-qualified # 🧜🏾‍♀ E5.0 mermaid: medium-dark skin tone -#1F9DC 1F3FF 200D 2640 FE0F ; fully-qualified # 🧜🏿‍♀️ E5.0 mermaid: dark skin tone -#1F9DC 1F3FF 200D 2640 ; minimally-qualified # 🧜🏿‍♀ E5.0 mermaid: dark skin tone -#1F9DD ; fully-qualified # 🧝 E5.0 elf -#1F9DD 1F3FB ; fully-qualified # 🧝🏻 E5.0 elf: light skin tone -#1F9DD 1F3FC ; fully-qualified # 🧝🏼 E5.0 elf: medium-light skin tone -#1F9DD 1F3FD ; fully-qualified # 🧝🏽 E5.0 elf: medium skin tone -#1F9DD 1F3FE ; fully-qualified # 🧝🏾 E5.0 elf: medium-dark skin tone -#1F9DD 1F3FF ; fully-qualified # 🧝🏿 E5.0 elf: dark skin tone -#1F9DD 200D 2642 FE0F ; fully-qualified # 🧝‍♂️ E5.0 man elf -#1F9DD 200D 2642 ; minimally-qualified # 🧝‍♂ E5.0 man elf -#1F9DD 1F3FB 200D 2642 FE0F ; fully-qualified # 🧝🏻‍♂️ E5.0 man elf: light skin tone -#1F9DD 1F3FB 200D 2642 ; minimally-qualified # 🧝🏻‍♂ E5.0 man elf: light skin tone -#1F9DD 1F3FC 200D 2642 FE0F ; fully-qualified # 🧝🏼‍♂️ E5.0 man elf: medium-light skin tone -#1F9DD 1F3FC 200D 2642 ; minimally-qualified # 🧝🏼‍♂ E5.0 man elf: medium-light skin tone -#1F9DD 1F3FD 200D 2642 FE0F ; fully-qualified # 🧝🏽‍♂️ E5.0 man elf: medium skin tone -#1F9DD 1F3FD 200D 2642 ; minimally-qualified # 🧝🏽‍♂ E5.0 man elf: medium skin tone -#1F9DD 1F3FE 200D 2642 FE0F ; fully-qualified # 🧝🏾‍♂️ E5.0 man elf: medium-dark skin tone -#1F9DD 1F3FE 200D 2642 ; minimally-qualified # 🧝🏾‍♂ E5.0 man elf: medium-dark skin tone -#1F9DD 1F3FF 200D 2642 FE0F ; fully-qualified # 🧝🏿‍♂️ E5.0 man elf: dark skin tone -#1F9DD 1F3FF 200D 2642 ; minimally-qualified # 🧝🏿‍♂ E5.0 man elf: dark skin tone -#1F9DD 200D 2640 FE0F ; fully-qualified # 🧝‍♀️ E5.0 woman elf -#1F9DD 200D 2640 ; minimally-qualified # 🧝‍♀ E5.0 woman elf -#1F9DD 1F3FB 200D 2640 FE0F ; fully-qualified # 🧝🏻‍♀️ E5.0 woman elf: light skin tone -#1F9DD 1F3FB 200D 2640 ; minimally-qualified # 🧝🏻‍♀ E5.0 woman elf: light skin tone -#1F9DD 1F3FC 200D 2640 FE0F ; fully-qualified # 🧝🏼‍♀️ E5.0 woman elf: medium-light skin tone -#1F9DD 1F3FC 200D 2640 ; minimally-qualified # 🧝🏼‍♀ E5.0 woman elf: medium-light skin tone -#1F9DD 1F3FD 200D 2640 FE0F ; fully-qualified # 🧝🏽‍♀️ E5.0 woman elf: medium skin tone -#1F9DD 1F3FD 200D 2640 ; minimally-qualified # 🧝🏽‍♀ E5.0 woman elf: medium skin tone -#1F9DD 1F3FE 200D 2640 FE0F ; fully-qualified # 🧝🏾‍♀️ E5.0 woman elf: medium-dark skin tone -#1F9DD 1F3FE 200D 2640 ; minimally-qualified # 🧝🏾‍♀ E5.0 woman elf: medium-dark skin tone -#1F9DD 1F3FF 200D 2640 FE0F ; fully-qualified # 🧝🏿‍♀️ E5.0 woman elf: dark skin tone -#1F9DD 1F3FF 200D 2640 ; minimally-qualified # 🧝🏿‍♀ E5.0 woman elf: dark skin tone -#1F9DE ; fully-qualified # 🧞 E5.0 genie -#1F9DE 200D 2642 FE0F ; fully-qualified # 🧞‍♂️ E5.0 man genie -#1F9DE 200D 2642 ; minimally-qualified # 🧞‍♂ E5.0 man genie -#1F9DE 200D 2640 FE0F ; fully-qualified # 🧞‍♀️ E5.0 woman genie -#1F9DE 200D 2640 ; minimally-qualified # 🧞‍♀ E5.0 woman genie -#1F9DF ; fully-qualified # 🧟 E5.0 zombie -#1F9DF 200D 2642 FE0F ; fully-qualified # 🧟‍♂️ E5.0 man zombie -#1F9DF 200D 2642 ; minimally-qualified # 🧟‍♂ E5.0 man zombie -#1F9DF 200D 2640 FE0F ; fully-qualified # 🧟‍♀️ E5.0 woman zombie -#1F9DF 200D 2640 ; minimally-qualified # 🧟‍♀ E5.0 woman zombie -#1F9CC ; fully-qualified # 🧌 E14.0 troll -# -## subgroup: person-activity -#1F486 ; fully-qualified # 💆 E0.6 person getting massage -#1F486 1F3FB ; fully-qualified # 💆🏻 E1.0 person getting massage: light skin tone -#1F486 1F3FC ; fully-qualified # 💆🏼 E1.0 person getting massage: medium-light skin tone -#1F486 1F3FD ; fully-qualified # 💆🏽 E1.0 person getting massage: medium skin tone -#1F486 1F3FE ; fully-qualified # 💆🏾 E1.0 person getting massage: medium-dark skin tone -#1F486 1F3FF ; fully-qualified # 💆🏿 E1.0 person getting massage: dark skin tone -#1F486 200D 2642 FE0F ; fully-qualified # 💆‍♂️ E4.0 man getting massage -#1F486 200D 2642 ; minimally-qualified # 💆‍♂ E4.0 man getting massage -#1F486 1F3FB 200D 2642 FE0F ; fully-qualified # 💆🏻‍♂️ E4.0 man getting massage: light skin tone -#1F486 1F3FB 200D 2642 ; minimally-qualified # 💆🏻‍♂ E4.0 man getting massage: light skin tone -#1F486 1F3FC 200D 2642 FE0F ; fully-qualified # 💆🏼‍♂️ E4.0 man getting massage: medium-light skin tone -#1F486 1F3FC 200D 2642 ; minimally-qualified # 💆🏼‍♂ E4.0 man getting massage: medium-light skin tone -#1F486 1F3FD 200D 2642 FE0F ; fully-qualified # 💆🏽‍♂️ E4.0 man getting massage: medium skin tone -#1F486 1F3FD 200D 2642 ; minimally-qualified # 💆🏽‍♂ E4.0 man getting massage: medium skin tone -#1F486 1F3FE 200D 2642 FE0F ; fully-qualified # 💆🏾‍♂️ E4.0 man getting massage: medium-dark skin tone -#1F486 1F3FE 200D 2642 ; minimally-qualified # 💆🏾‍♂ E4.0 man getting massage: medium-dark skin tone -#1F486 1F3FF 200D 2642 FE0F ; fully-qualified # 💆🏿‍♂️ E4.0 man getting massage: dark skin tone -#1F486 1F3FF 200D 2642 ; minimally-qualified # 💆🏿‍♂ E4.0 man getting massage: dark skin tone -#1F486 200D 2640 FE0F ; fully-qualified # 💆‍♀️ E4.0 woman getting massage -#1F486 200D 2640 ; minimally-qualified # 💆‍♀ E4.0 woman getting massage -#1F486 1F3FB 200D 2640 FE0F ; fully-qualified # 💆🏻‍♀️ E4.0 woman getting massage: light skin tone -#1F486 1F3FB 200D 2640 ; minimally-qualified # 💆🏻‍♀ E4.0 woman getting massage: light skin tone -#1F486 1F3FC 200D 2640 FE0F ; fully-qualified # 💆🏼‍♀️ E4.0 woman getting massage: medium-light skin tone -#1F486 1F3FC 200D 2640 ; minimally-qualified # 💆🏼‍♀ E4.0 woman getting massage: medium-light skin tone -#1F486 1F3FD 200D 2640 FE0F ; fully-qualified # 💆🏽‍♀️ E4.0 woman getting massage: medium skin tone -#1F486 1F3FD 200D 2640 ; minimally-qualified # 💆🏽‍♀ E4.0 woman getting massage: medium skin tone -#1F486 1F3FE 200D 2640 FE0F ; fully-qualified # 💆🏾‍♀️ E4.0 woman getting massage: medium-dark skin tone -#1F486 1F3FE 200D 2640 ; minimally-qualified # 💆🏾‍♀ E4.0 woman getting massage: medium-dark skin tone -#1F486 1F3FF 200D 2640 FE0F ; fully-qualified # 💆🏿‍♀️ E4.0 woman getting massage: dark skin tone -#1F486 1F3FF 200D 2640 ; minimally-qualified # 💆🏿‍♀ E4.0 woman getting massage: dark skin tone -#1F487 ; fully-qualified # 💇 E0.6 person getting haircut -#1F487 1F3FB ; fully-qualified # 💇🏻 E1.0 person getting haircut: light skin tone -#1F487 1F3FC ; fully-qualified # 💇🏼 E1.0 person getting haircut: medium-light skin tone -#1F487 1F3FD ; fully-qualified # 💇🏽 E1.0 person getting haircut: medium skin tone -#1F487 1F3FE ; fully-qualified # 💇🏾 E1.0 person getting haircut: medium-dark skin tone -#1F487 1F3FF ; fully-qualified # 💇🏿 E1.0 person getting haircut: dark skin tone -#1F487 200D 2642 FE0F ; fully-qualified # 💇‍♂️ E4.0 man getting haircut -#1F487 200D 2642 ; minimally-qualified # 💇‍♂ E4.0 man getting haircut -#1F487 1F3FB 200D 2642 FE0F ; fully-qualified # 💇🏻‍♂️ E4.0 man getting haircut: light skin tone -#1F487 1F3FB 200D 2642 ; minimally-qualified # 💇🏻‍♂ E4.0 man getting haircut: light skin tone -#1F487 1F3FC 200D 2642 FE0F ; fully-qualified # 💇🏼‍♂️ E4.0 man getting haircut: medium-light skin tone -#1F487 1F3FC 200D 2642 ; minimally-qualified # 💇🏼‍♂ E4.0 man getting haircut: medium-light skin tone -#1F487 1F3FD 200D 2642 FE0F ; fully-qualified # 💇🏽‍♂️ E4.0 man getting haircut: medium skin tone -#1F487 1F3FD 200D 2642 ; minimally-qualified # 💇🏽‍♂ E4.0 man getting haircut: medium skin tone -#1F487 1F3FE 200D 2642 FE0F ; fully-qualified # 💇🏾‍♂️ E4.0 man getting haircut: medium-dark skin tone -#1F487 1F3FE 200D 2642 ; minimally-qualified # 💇🏾‍♂ E4.0 man getting haircut: medium-dark skin tone -#1F487 1F3FF 200D 2642 FE0F ; fully-qualified # 💇🏿‍♂️ E4.0 man getting haircut: dark skin tone -#1F487 1F3FF 200D 2642 ; minimally-qualified # 💇🏿‍♂ E4.0 man getting haircut: dark skin tone -#1F487 200D 2640 FE0F ; fully-qualified # 💇‍♀️ E4.0 woman getting haircut -#1F487 200D 2640 ; minimally-qualified # 💇‍♀ E4.0 woman getting haircut -#1F487 1F3FB 200D 2640 FE0F ; fully-qualified # 💇🏻‍♀️ E4.0 woman getting haircut: light skin tone -#1F487 1F3FB 200D 2640 ; minimally-qualified # 💇🏻‍♀ E4.0 woman getting haircut: light skin tone -#1F487 1F3FC 200D 2640 FE0F ; fully-qualified # 💇🏼‍♀️ E4.0 woman getting haircut: medium-light skin tone -#1F487 1F3FC 200D 2640 ; minimally-qualified # 💇🏼‍♀ E4.0 woman getting haircut: medium-light skin tone -#1F487 1F3FD 200D 2640 FE0F ; fully-qualified # 💇🏽‍♀️ E4.0 woman getting haircut: medium skin tone -#1F487 1F3FD 200D 2640 ; minimally-qualified # 💇🏽‍♀ E4.0 woman getting haircut: medium skin tone -#1F487 1F3FE 200D 2640 FE0F ; fully-qualified # 💇🏾‍♀️ E4.0 woman getting haircut: medium-dark skin tone -#1F487 1F3FE 200D 2640 ; minimally-qualified # 💇🏾‍♀ E4.0 woman getting haircut: medium-dark skin tone -#1F487 1F3FF 200D 2640 FE0F ; fully-qualified # 💇🏿‍♀️ E4.0 woman getting haircut: dark skin tone -#1F487 1F3FF 200D 2640 ; minimally-qualified # 💇🏿‍♀ E4.0 woman getting haircut: dark skin tone -#1F6B6 ; fully-qualified # 🚶 E0.6 person walking -#1F6B6 1F3FB ; fully-qualified # 🚶🏻 E1.0 person walking: light skin tone -#1F6B6 1F3FC ; fully-qualified # 🚶🏼 E1.0 person walking: medium-light skin tone -#1F6B6 1F3FD ; fully-qualified # 🚶🏽 E1.0 person walking: medium skin tone -#1F6B6 1F3FE ; fully-qualified # 🚶🏾 E1.0 person walking: medium-dark skin tone -#1F6B6 1F3FF ; fully-qualified # 🚶🏿 E1.0 person walking: dark skin tone -#1F6B6 200D 2642 FE0F ; fully-qualified # 🚶‍♂️ E4.0 man walking -#1F6B6 200D 2642 ; minimally-qualified # 🚶‍♂ E4.0 man walking -#1F6B6 1F3FB 200D 2642 FE0F ; fully-qualified # 🚶🏻‍♂️ E4.0 man walking: light skin tone -#1F6B6 1F3FB 200D 2642 ; minimally-qualified # 🚶🏻‍♂ E4.0 man walking: light skin tone -#1F6B6 1F3FC 200D 2642 FE0F ; fully-qualified # 🚶🏼‍♂️ E4.0 man walking: medium-light skin tone -#1F6B6 1F3FC 200D 2642 ; minimally-qualified # 🚶🏼‍♂ E4.0 man walking: medium-light skin tone -#1F6B6 1F3FD 200D 2642 FE0F ; fully-qualified # 🚶🏽‍♂️ E4.0 man walking: medium skin tone -#1F6B6 1F3FD 200D 2642 ; minimally-qualified # 🚶🏽‍♂ E4.0 man walking: medium skin tone -#1F6B6 1F3FE 200D 2642 FE0F ; fully-qualified # 🚶🏾‍♂️ E4.0 man walking: medium-dark skin tone -#1F6B6 1F3FE 200D 2642 ; minimally-qualified # 🚶🏾‍♂ E4.0 man walking: medium-dark skin tone -#1F6B6 1F3FF 200D 2642 FE0F ; fully-qualified # 🚶🏿‍♂️ E4.0 man walking: dark skin tone -#1F6B6 1F3FF 200D 2642 ; minimally-qualified # 🚶🏿‍♂ E4.0 man walking: dark skin tone -#1F6B6 200D 2640 FE0F ; fully-qualified # 🚶‍♀️ E4.0 woman walking -#1F6B6 200D 2640 ; minimally-qualified # 🚶‍♀ E4.0 woman walking -#1F6B6 1F3FB 200D 2640 FE0F ; fully-qualified # 🚶🏻‍♀️ E4.0 woman walking: light skin tone -#1F6B6 1F3FB 200D 2640 ; minimally-qualified # 🚶🏻‍♀ E4.0 woman walking: light skin tone -#1F6B6 1F3FC 200D 2640 FE0F ; fully-qualified # 🚶🏼‍♀️ E4.0 woman walking: medium-light skin tone -#1F6B6 1F3FC 200D 2640 ; minimally-qualified # 🚶🏼‍♀ E4.0 woman walking: medium-light skin tone -#1F6B6 1F3FD 200D 2640 FE0F ; fully-qualified # 🚶🏽‍♀️ E4.0 woman walking: medium skin tone -#1F6B6 1F3FD 200D 2640 ; minimally-qualified # 🚶🏽‍♀ E4.0 woman walking: medium skin tone -#1F6B6 1F3FE 200D 2640 FE0F ; fully-qualified # 🚶🏾‍♀️ E4.0 woman walking: medium-dark skin tone -#1F6B6 1F3FE 200D 2640 ; minimally-qualified # 🚶🏾‍♀ E4.0 woman walking: medium-dark skin tone -#1F6B6 1F3FF 200D 2640 FE0F ; fully-qualified # 🚶🏿‍♀️ E4.0 woman walking: dark skin tone -#1F6B6 1F3FF 200D 2640 ; minimally-qualified # 🚶🏿‍♀ E4.0 woman walking: dark skin tone -#1F9CD ; fully-qualified # 🧍 E12.0 person standing -#1F9CD 1F3FB ; fully-qualified # 🧍🏻 E12.0 person standing: light skin tone -#1F9CD 1F3FC ; fully-qualified # 🧍🏼 E12.0 person standing: medium-light skin tone -#1F9CD 1F3FD ; fully-qualified # 🧍🏽 E12.0 person standing: medium skin tone -#1F9CD 1F3FE ; fully-qualified # 🧍🏾 E12.0 person standing: medium-dark skin tone -#1F9CD 1F3FF ; fully-qualified # 🧍🏿 E12.0 person standing: dark skin tone -#1F9CD 200D 2642 FE0F ; fully-qualified # 🧍‍♂️ E12.0 man standing -#1F9CD 200D 2642 ; minimally-qualified # 🧍‍♂ E12.0 man standing -#1F9CD 1F3FB 200D 2642 FE0F ; fully-qualified # 🧍🏻‍♂️ E12.0 man standing: light skin tone -#1F9CD 1F3FB 200D 2642 ; minimally-qualified # 🧍🏻‍♂ E12.0 man standing: light skin tone -#1F9CD 1F3FC 200D 2642 FE0F ; fully-qualified # 🧍🏼‍♂️ E12.0 man standing: medium-light skin tone -#1F9CD 1F3FC 200D 2642 ; minimally-qualified # 🧍🏼‍♂ E12.0 man standing: medium-light skin tone -#1F9CD 1F3FD 200D 2642 FE0F ; fully-qualified # 🧍🏽‍♂️ E12.0 man standing: medium skin tone -#1F9CD 1F3FD 200D 2642 ; minimally-qualified # 🧍🏽‍♂ E12.0 man standing: medium skin tone -#1F9CD 1F3FE 200D 2642 FE0F ; fully-qualified # 🧍🏾‍♂️ E12.0 man standing: medium-dark skin tone -#1F9CD 1F3FE 200D 2642 ; minimally-qualified # 🧍🏾‍♂ E12.0 man standing: medium-dark skin tone -#1F9CD 1F3FF 200D 2642 FE0F ; fully-qualified # 🧍🏿‍♂️ E12.0 man standing: dark skin tone -#1F9CD 1F3FF 200D 2642 ; minimally-qualified # 🧍🏿‍♂ E12.0 man standing: dark skin tone -#1F9CD 200D 2640 FE0F ; fully-qualified # 🧍‍♀️ E12.0 woman standing -#1F9CD 200D 2640 ; minimally-qualified # 🧍‍♀ E12.0 woman standing -#1F9CD 1F3FB 200D 2640 FE0F ; fully-qualified # 🧍🏻‍♀️ E12.0 woman standing: light skin tone -#1F9CD 1F3FB 200D 2640 ; minimally-qualified # 🧍🏻‍♀ E12.0 woman standing: light skin tone -#1F9CD 1F3FC 200D 2640 FE0F ; fully-qualified # 🧍🏼‍♀️ E12.0 woman standing: medium-light skin tone -#1F9CD 1F3FC 200D 2640 ; minimally-qualified # 🧍🏼‍♀ E12.0 woman standing: medium-light skin tone -#1F9CD 1F3FD 200D 2640 FE0F ; fully-qualified # 🧍🏽‍♀️ E12.0 woman standing: medium skin tone -#1F9CD 1F3FD 200D 2640 ; minimally-qualified # 🧍🏽‍♀ E12.0 woman standing: medium skin tone -#1F9CD 1F3FE 200D 2640 FE0F ; fully-qualified # 🧍🏾‍♀️ E12.0 woman standing: medium-dark skin tone -#1F9CD 1F3FE 200D 2640 ; minimally-qualified # 🧍🏾‍♀ E12.0 woman standing: medium-dark skin tone -#1F9CD 1F3FF 200D 2640 FE0F ; fully-qualified # 🧍🏿‍♀️ E12.0 woman standing: dark skin tone -#1F9CD 1F3FF 200D 2640 ; minimally-qualified # 🧍🏿‍♀ E12.0 woman standing: dark skin tone -#1F9CE ; fully-qualified # 🧎 E12.0 person kneeling -#1F9CE 1F3FB ; fully-qualified # 🧎🏻 E12.0 person kneeling: light skin tone -#1F9CE 1F3FC ; fully-qualified # 🧎🏼 E12.0 person kneeling: medium-light skin tone -#1F9CE 1F3FD ; fully-qualified # 🧎🏽 E12.0 person kneeling: medium skin tone -#1F9CE 1F3FE ; fully-qualified # 🧎🏾 E12.0 person kneeling: medium-dark skin tone -#1F9CE 1F3FF ; fully-qualified # 🧎🏿 E12.0 person kneeling: dark skin tone -#1F9CE 200D 2642 FE0F ; fully-qualified # 🧎‍♂️ E12.0 man kneeling -#1F9CE 200D 2642 ; minimally-qualified # 🧎‍♂ E12.0 man kneeling -#1F9CE 1F3FB 200D 2642 FE0F ; fully-qualified # 🧎🏻‍♂️ E12.0 man kneeling: light skin tone -#1F9CE 1F3FB 200D 2642 ; minimally-qualified # 🧎🏻‍♂ E12.0 man kneeling: light skin tone -#1F9CE 1F3FC 200D 2642 FE0F ; fully-qualified # 🧎🏼‍♂️ E12.0 man kneeling: medium-light skin tone -#1F9CE 1F3FC 200D 2642 ; minimally-qualified # 🧎🏼‍♂ E12.0 man kneeling: medium-light skin tone -#1F9CE 1F3FD 200D 2642 FE0F ; fully-qualified # 🧎🏽‍♂️ E12.0 man kneeling: medium skin tone -#1F9CE 1F3FD 200D 2642 ; minimally-qualified # 🧎🏽‍♂ E12.0 man kneeling: medium skin tone -#1F9CE 1F3FE 200D 2642 FE0F ; fully-qualified # 🧎🏾‍♂️ E12.0 man kneeling: medium-dark skin tone -#1F9CE 1F3FE 200D 2642 ; minimally-qualified # 🧎🏾‍♂ E12.0 man kneeling: medium-dark skin tone -#1F9CE 1F3FF 200D 2642 FE0F ; fully-qualified # 🧎🏿‍♂️ E12.0 man kneeling: dark skin tone -#1F9CE 1F3FF 200D 2642 ; minimally-qualified # 🧎🏿‍♂ E12.0 man kneeling: dark skin tone -#1F9CE 200D 2640 FE0F ; fully-qualified # 🧎‍♀️ E12.0 woman kneeling -#1F9CE 200D 2640 ; minimally-qualified # 🧎‍♀ E12.0 woman kneeling -#1F9CE 1F3FB 200D 2640 FE0F ; fully-qualified # 🧎🏻‍♀️ E12.0 woman kneeling: light skin tone -#1F9CE 1F3FB 200D 2640 ; minimally-qualified # 🧎🏻‍♀ E12.0 woman kneeling: light skin tone -#1F9CE 1F3FC 200D 2640 FE0F ; fully-qualified # 🧎🏼‍♀️ E12.0 woman kneeling: medium-light skin tone -#1F9CE 1F3FC 200D 2640 ; minimally-qualified # 🧎🏼‍♀ E12.0 woman kneeling: medium-light skin tone -#1F9CE 1F3FD 200D 2640 FE0F ; fully-qualified # 🧎🏽‍♀️ E12.0 woman kneeling: medium skin tone -#1F9CE 1F3FD 200D 2640 ; minimally-qualified # 🧎🏽‍♀ E12.0 woman kneeling: medium skin tone -#1F9CE 1F3FE 200D 2640 FE0F ; fully-qualified # 🧎🏾‍♀️ E12.0 woman kneeling: medium-dark skin tone -#1F9CE 1F3FE 200D 2640 ; minimally-qualified # 🧎🏾‍♀ E12.0 woman kneeling: medium-dark skin tone -#1F9CE 1F3FF 200D 2640 FE0F ; fully-qualified # 🧎🏿‍♀️ E12.0 woman kneeling: dark skin tone -#1F9CE 1F3FF 200D 2640 ; minimally-qualified # 🧎🏿‍♀ E12.0 woman kneeling: dark skin tone -#1F9D1 200D 1F9AF ; fully-qualified # 🧑‍🦯 E12.1 person with white cane -#1F9D1 1F3FB 200D 1F9AF ; fully-qualified # 🧑🏻‍🦯 E12.1 person with white cane: light skin tone -#1F9D1 1F3FC 200D 1F9AF ; fully-qualified # 🧑🏼‍🦯 E12.1 person with white cane: medium-light skin tone -#1F9D1 1F3FD 200D 1F9AF ; fully-qualified # 🧑🏽‍🦯 E12.1 person with white cane: medium skin tone -#1F9D1 1F3FE 200D 1F9AF ; fully-qualified # 🧑🏾‍🦯 E12.1 person with white cane: medium-dark skin tone -#1F9D1 1F3FF 200D 1F9AF ; fully-qualified # 🧑🏿‍🦯 E12.1 person with white cane: dark skin tone -#1F468 200D 1F9AF ; fully-qualified # 👨‍🦯 E12.0 man with white cane -#1F468 1F3FB 200D 1F9AF ; fully-qualified # 👨🏻‍🦯 E12.0 man with white cane: light skin tone -#1F468 1F3FC 200D 1F9AF ; fully-qualified # 👨🏼‍🦯 E12.0 man with white cane: medium-light skin tone -#1F468 1F3FD 200D 1F9AF ; fully-qualified # 👨🏽‍🦯 E12.0 man with white cane: medium skin tone -#1F468 1F3FE 200D 1F9AF ; fully-qualified # 👨🏾‍🦯 E12.0 man with white cane: medium-dark skin tone -#1F468 1F3FF 200D 1F9AF ; fully-qualified # 👨🏿‍🦯 E12.0 man with white cane: dark skin tone -#1F469 200D 1F9AF ; fully-qualified # 👩‍🦯 E12.0 woman with white cane -#1F469 1F3FB 200D 1F9AF ; fully-qualified # 👩🏻‍🦯 E12.0 woman with white cane: light skin tone -#1F469 1F3FC 200D 1F9AF ; fully-qualified # 👩🏼‍🦯 E12.0 woman with white cane: medium-light skin tone -#1F469 1F3FD 200D 1F9AF ; fully-qualified # 👩🏽‍🦯 E12.0 woman with white cane: medium skin tone -#1F469 1F3FE 200D 1F9AF ; fully-qualified # 👩🏾‍🦯 E12.0 woman with white cane: medium-dark skin tone -#1F469 1F3FF 200D 1F9AF ; fully-qualified # 👩🏿‍🦯 E12.0 woman with white cane: dark skin tone -#1F9D1 200D 1F9BC ; fully-qualified # 🧑‍🦼 E12.1 person in motorized wheelchair -#1F9D1 1F3FB 200D 1F9BC ; fully-qualified # 🧑🏻‍🦼 E12.1 person in motorized wheelchair: light skin tone -#1F9D1 1F3FC 200D 1F9BC ; fully-qualified # 🧑🏼‍🦼 E12.1 person in motorized wheelchair: medium-light skin tone -#1F9D1 1F3FD 200D 1F9BC ; fully-qualified # 🧑🏽‍🦼 E12.1 person in motorized wheelchair: medium skin tone -#1F9D1 1F3FE 200D 1F9BC ; fully-qualified # 🧑🏾‍🦼 E12.1 person in motorized wheelchair: medium-dark skin tone -#1F9D1 1F3FF 200D 1F9BC ; fully-qualified # 🧑🏿‍🦼 E12.1 person in motorized wheelchair: dark skin tone -#1F468 200D 1F9BC ; fully-qualified # 👨‍🦼 E12.0 man in motorized wheelchair -#1F468 1F3FB 200D 1F9BC ; fully-qualified # 👨🏻‍🦼 E12.0 man in motorized wheelchair: light skin tone -#1F468 1F3FC 200D 1F9BC ; fully-qualified # 👨🏼‍🦼 E12.0 man in motorized wheelchair: medium-light skin tone -#1F468 1F3FD 200D 1F9BC ; fully-qualified # 👨🏽‍🦼 E12.0 man in motorized wheelchair: medium skin tone -#1F468 1F3FE 200D 1F9BC ; fully-qualified # 👨🏾‍🦼 E12.0 man in motorized wheelchair: medium-dark skin tone -#1F468 1F3FF 200D 1F9BC ; fully-qualified # 👨🏿‍🦼 E12.0 man in motorized wheelchair: dark skin tone -#1F469 200D 1F9BC ; fully-qualified # 👩‍🦼 E12.0 woman in motorized wheelchair -#1F469 1F3FB 200D 1F9BC ; fully-qualified # 👩🏻‍🦼 E12.0 woman in motorized wheelchair: light skin tone -#1F469 1F3FC 200D 1F9BC ; fully-qualified # 👩🏼‍🦼 E12.0 woman in motorized wheelchair: medium-light skin tone -#1F469 1F3FD 200D 1F9BC ; fully-qualified # 👩🏽‍🦼 E12.0 woman in motorized wheelchair: medium skin tone -#1F469 1F3FE 200D 1F9BC ; fully-qualified # 👩🏾‍🦼 E12.0 woman in motorized wheelchair: medium-dark skin tone -#1F469 1F3FF 200D 1F9BC ; fully-qualified # 👩🏿‍🦼 E12.0 woman in motorized wheelchair: dark skin tone -#1F9D1 200D 1F9BD ; fully-qualified # 🧑‍🦽 E12.1 person in manual wheelchair -#1F9D1 1F3FB 200D 1F9BD ; fully-qualified # 🧑🏻‍🦽 E12.1 person in manual wheelchair: light skin tone -#1F9D1 1F3FC 200D 1F9BD ; fully-qualified # 🧑🏼‍🦽 E12.1 person in manual wheelchair: medium-light skin tone -#1F9D1 1F3FD 200D 1F9BD ; fully-qualified # 🧑🏽‍🦽 E12.1 person in manual wheelchair: medium skin tone -#1F9D1 1F3FE 200D 1F9BD ; fully-qualified # 🧑🏾‍🦽 E12.1 person in manual wheelchair: medium-dark skin tone -#1F9D1 1F3FF 200D 1F9BD ; fully-qualified # 🧑🏿‍🦽 E12.1 person in manual wheelchair: dark skin tone -#1F468 200D 1F9BD ; fully-qualified # 👨‍🦽 E12.0 man in manual wheelchair -#1F468 1F3FB 200D 1F9BD ; fully-qualified # 👨🏻‍🦽 E12.0 man in manual wheelchair: light skin tone -#1F468 1F3FC 200D 1F9BD ; fully-qualified # 👨🏼‍🦽 E12.0 man in manual wheelchair: medium-light skin tone -#1F468 1F3FD 200D 1F9BD ; fully-qualified # 👨🏽‍🦽 E12.0 man in manual wheelchair: medium skin tone -#1F468 1F3FE 200D 1F9BD ; fully-qualified # 👨🏾‍🦽 E12.0 man in manual wheelchair: medium-dark skin tone -#1F468 1F3FF 200D 1F9BD ; fully-qualified # 👨🏿‍🦽 E12.0 man in manual wheelchair: dark skin tone -#1F469 200D 1F9BD ; fully-qualified # 👩‍🦽 E12.0 woman in manual wheelchair -#1F469 1F3FB 200D 1F9BD ; fully-qualified # 👩🏻‍🦽 E12.0 woman in manual wheelchair: light skin tone -#1F469 1F3FC 200D 1F9BD ; fully-qualified # 👩🏼‍🦽 E12.0 woman in manual wheelchair: medium-light skin tone -#1F469 1F3FD 200D 1F9BD ; fully-qualified # 👩🏽‍🦽 E12.0 woman in manual wheelchair: medium skin tone -#1F469 1F3FE 200D 1F9BD ; fully-qualified # 👩🏾‍🦽 E12.0 woman in manual wheelchair: medium-dark skin tone -#1F469 1F3FF 200D 1F9BD ; fully-qualified # 👩🏿‍🦽 E12.0 woman in manual wheelchair: dark skin tone -#1F3C3 ; fully-qualified # 🏃 E0.6 person running -#1F3C3 1F3FB ; fully-qualified # 🏃🏻 E1.0 person running: light skin tone -#1F3C3 1F3FC ; fully-qualified # 🏃🏼 E1.0 person running: medium-light skin tone -#1F3C3 1F3FD ; fully-qualified # 🏃🏽 E1.0 person running: medium skin tone -#1F3C3 1F3FE ; fully-qualified # 🏃🏾 E1.0 person running: medium-dark skin tone -#1F3C3 1F3FF ; fully-qualified # 🏃🏿 E1.0 person running: dark skin tone -#1F3C3 200D 2642 FE0F ; fully-qualified # 🏃‍♂️ E4.0 man running -#1F3C3 200D 2642 ; minimally-qualified # 🏃‍♂ E4.0 man running -#1F3C3 1F3FB 200D 2642 FE0F ; fully-qualified # 🏃🏻‍♂️ E4.0 man running: light skin tone -#1F3C3 1F3FB 200D 2642 ; minimally-qualified # 🏃🏻‍♂ E4.0 man running: light skin tone -#1F3C3 1F3FC 200D 2642 FE0F ; fully-qualified # 🏃🏼‍♂️ E4.0 man running: medium-light skin tone -#1F3C3 1F3FC 200D 2642 ; minimally-qualified # 🏃🏼‍♂ E4.0 man running: medium-light skin tone -#1F3C3 1F3FD 200D 2642 FE0F ; fully-qualified # 🏃🏽‍♂️ E4.0 man running: medium skin tone -#1F3C3 1F3FD 200D 2642 ; minimally-qualified # 🏃🏽‍♂ E4.0 man running: medium skin tone -#1F3C3 1F3FE 200D 2642 FE0F ; fully-qualified # 🏃🏾‍♂️ E4.0 man running: medium-dark skin tone -#1F3C3 1F3FE 200D 2642 ; minimally-qualified # 🏃🏾‍♂ E4.0 man running: medium-dark skin tone -#1F3C3 1F3FF 200D 2642 FE0F ; fully-qualified # 🏃🏿‍♂️ E4.0 man running: dark skin tone -#1F3C3 1F3FF 200D 2642 ; minimally-qualified # 🏃🏿‍♂ E4.0 man running: dark skin tone -#1F3C3 200D 2640 FE0F ; fully-qualified # 🏃‍♀️ E4.0 woman running -#1F3C3 200D 2640 ; minimally-qualified # 🏃‍♀ E4.0 woman running -#1F3C3 1F3FB 200D 2640 FE0F ; fully-qualified # 🏃🏻‍♀️ E4.0 woman running: light skin tone -#1F3C3 1F3FB 200D 2640 ; minimally-qualified # 🏃🏻‍♀ E4.0 woman running: light skin tone -#1F3C3 1F3FC 200D 2640 FE0F ; fully-qualified # 🏃🏼‍♀️ E4.0 woman running: medium-light skin tone -#1F3C3 1F3FC 200D 2640 ; minimally-qualified # 🏃🏼‍♀ E4.0 woman running: medium-light skin tone -#1F3C3 1F3FD 200D 2640 FE0F ; fully-qualified # 🏃🏽‍♀️ E4.0 woman running: medium skin tone -#1F3C3 1F3FD 200D 2640 ; minimally-qualified # 🏃🏽‍♀ E4.0 woman running: medium skin tone -#1F3C3 1F3FE 200D 2640 FE0F ; fully-qualified # 🏃🏾‍♀️ E4.0 woman running: medium-dark skin tone -#1F3C3 1F3FE 200D 2640 ; minimally-qualified # 🏃🏾‍♀ E4.0 woman running: medium-dark skin tone -#1F3C3 1F3FF 200D 2640 FE0F ; fully-qualified # 🏃🏿‍♀️ E4.0 woman running: dark skin tone -#1F3C3 1F3FF 200D 2640 ; minimally-qualified # 🏃🏿‍♀ E4.0 woman running: dark skin tone -#1F483 ; fully-qualified # 💃 E0.6 woman dancing -#1F483 1F3FB ; fully-qualified # 💃🏻 E1.0 woman dancing: light skin tone -#1F483 1F3FC ; fully-qualified # 💃🏼 E1.0 woman dancing: medium-light skin tone -#1F483 1F3FD ; fully-qualified # 💃🏽 E1.0 woman dancing: medium skin tone -#1F483 1F3FE ; fully-qualified # 💃🏾 E1.0 woman dancing: medium-dark skin tone -#1F483 1F3FF ; fully-qualified # 💃🏿 E1.0 woman dancing: dark skin tone -#1F57A ; fully-qualified # 🕺 E3.0 man dancing -#1F57A 1F3FB ; fully-qualified # 🕺🏻 E3.0 man dancing: light skin tone -#1F57A 1F3FC ; fully-qualified # 🕺🏼 E3.0 man dancing: medium-light skin tone -#1F57A 1F3FD ; fully-qualified # 🕺🏽 E3.0 man dancing: medium skin tone -#1F57A 1F3FE ; fully-qualified # 🕺🏾 E3.0 man dancing: medium-dark skin tone -#1F57A 1F3FF ; fully-qualified # 🕺🏿 E3.0 man dancing: dark skin tone -#1F574 FE0F ; fully-qualified # 🕴️ E0.7 person in suit levitating -#1F574 ; unqualified # 🕴 E0.7 person in suit levitating -#1F574 1F3FB ; fully-qualified # 🕴🏻 E4.0 person in suit levitating: light skin tone -#1F574 1F3FC ; fully-qualified # 🕴🏼 E4.0 person in suit levitating: medium-light skin tone -#1F574 1F3FD ; fully-qualified # 🕴🏽 E4.0 person in suit levitating: medium skin tone -#1F574 1F3FE ; fully-qualified # 🕴🏾 E4.0 person in suit levitating: medium-dark skin tone -#1F574 1F3FF ; fully-qualified # 🕴🏿 E4.0 person in suit levitating: dark skin tone -#1F46F ; fully-qualified # 👯 E0.6 people with bunny ears -#1F46F 200D 2642 FE0F ; fully-qualified # 👯‍♂️ E4.0 men with bunny ears -#1F46F 200D 2642 ; minimally-qualified # 👯‍♂ E4.0 men with bunny ears -#1F46F 200D 2640 FE0F ; fully-qualified # 👯‍♀️ E4.0 women with bunny ears -#1F46F 200D 2640 ; minimally-qualified # 👯‍♀ E4.0 women with bunny ears -#1F9D6 ; fully-qualified # 🧖 E5.0 person in steamy room -#1F9D6 1F3FB ; fully-qualified # 🧖🏻 E5.0 person in steamy room: light skin tone -#1F9D6 1F3FC ; fully-qualified # 🧖🏼 E5.0 person in steamy room: medium-light skin tone -#1F9D6 1F3FD ; fully-qualified # 🧖🏽 E5.0 person in steamy room: medium skin tone -#1F9D6 1F3FE ; fully-qualified # 🧖🏾 E5.0 person in steamy room: medium-dark skin tone -#1F9D6 1F3FF ; fully-qualified # 🧖🏿 E5.0 person in steamy room: dark skin tone -#1F9D6 200D 2642 FE0F ; fully-qualified # 🧖‍♂️ E5.0 man in steamy room -#1F9D6 200D 2642 ; minimally-qualified # 🧖‍♂ E5.0 man in steamy room -#1F9D6 1F3FB 200D 2642 FE0F ; fully-qualified # 🧖🏻‍♂️ E5.0 man in steamy room: light skin tone -#1F9D6 1F3FB 200D 2642 ; minimally-qualified # 🧖🏻‍♂ E5.0 man in steamy room: light skin tone -#1F9D6 1F3FC 200D 2642 FE0F ; fully-qualified # 🧖🏼‍♂️ E5.0 man in steamy room: medium-light skin tone -#1F9D6 1F3FC 200D 2642 ; minimally-qualified # 🧖🏼‍♂ E5.0 man in steamy room: medium-light skin tone -#1F9D6 1F3FD 200D 2642 FE0F ; fully-qualified # 🧖🏽‍♂️ E5.0 man in steamy room: medium skin tone -#1F9D6 1F3FD 200D 2642 ; minimally-qualified # 🧖🏽‍♂ E5.0 man in steamy room: medium skin tone -#1F9D6 1F3FE 200D 2642 FE0F ; fully-qualified # 🧖🏾‍♂️ E5.0 man in steamy room: medium-dark skin tone -#1F9D6 1F3FE 200D 2642 ; minimally-qualified # 🧖🏾‍♂ E5.0 man in steamy room: medium-dark skin tone -#1F9D6 1F3FF 200D 2642 FE0F ; fully-qualified # 🧖🏿‍♂️ E5.0 man in steamy room: dark skin tone -#1F9D6 1F3FF 200D 2642 ; minimally-qualified # 🧖🏿‍♂ E5.0 man in steamy room: dark skin tone -#1F9D6 200D 2640 FE0F ; fully-qualified # 🧖‍♀️ E5.0 woman in steamy room -#1F9D6 200D 2640 ; minimally-qualified # 🧖‍♀ E5.0 woman in steamy room -#1F9D6 1F3FB 200D 2640 FE0F ; fully-qualified # 🧖🏻‍♀️ E5.0 woman in steamy room: light skin tone -#1F9D6 1F3FB 200D 2640 ; minimally-qualified # 🧖🏻‍♀ E5.0 woman in steamy room: light skin tone -#1F9D6 1F3FC 200D 2640 FE0F ; fully-qualified # 🧖🏼‍♀️ E5.0 woman in steamy room: medium-light skin tone -#1F9D6 1F3FC 200D 2640 ; minimally-qualified # 🧖🏼‍♀ E5.0 woman in steamy room: medium-light skin tone -#1F9D6 1F3FD 200D 2640 FE0F ; fully-qualified # 🧖🏽‍♀️ E5.0 woman in steamy room: medium skin tone -#1F9D6 1F3FD 200D 2640 ; minimally-qualified # 🧖🏽‍♀ E5.0 woman in steamy room: medium skin tone -#1F9D6 1F3FE 200D 2640 FE0F ; fully-qualified # 🧖🏾‍♀️ E5.0 woman in steamy room: medium-dark skin tone -#1F9D6 1F3FE 200D 2640 ; minimally-qualified # 🧖🏾‍♀ E5.0 woman in steamy room: medium-dark skin tone -#1F9D6 1F3FF 200D 2640 FE0F ; fully-qualified # 🧖🏿‍♀️ E5.0 woman in steamy room: dark skin tone -#1F9D6 1F3FF 200D 2640 ; minimally-qualified # 🧖🏿‍♀ E5.0 woman in steamy room: dark skin tone -#1F9D7 ; fully-qualified # 🧗 E5.0 person climbing -#1F9D7 1F3FB ; fully-qualified # 🧗🏻 E5.0 person climbing: light skin tone -#1F9D7 1F3FC ; fully-qualified # 🧗🏼 E5.0 person climbing: medium-light skin tone -#1F9D7 1F3FD ; fully-qualified # 🧗🏽 E5.0 person climbing: medium skin tone -#1F9D7 1F3FE ; fully-qualified # 🧗🏾 E5.0 person climbing: medium-dark skin tone -#1F9D7 1F3FF ; fully-qualified # 🧗🏿 E5.0 person climbing: dark skin tone -#1F9D7 200D 2642 FE0F ; fully-qualified # 🧗‍♂️ E5.0 man climbing -#1F9D7 200D 2642 ; minimally-qualified # 🧗‍♂ E5.0 man climbing -#1F9D7 1F3FB 200D 2642 FE0F ; fully-qualified # 🧗🏻‍♂️ E5.0 man climbing: light skin tone -#1F9D7 1F3FB 200D 2642 ; minimally-qualified # 🧗🏻‍♂ E5.0 man climbing: light skin tone -#1F9D7 1F3FC 200D 2642 FE0F ; fully-qualified # 🧗🏼‍♂️ E5.0 man climbing: medium-light skin tone -#1F9D7 1F3FC 200D 2642 ; minimally-qualified # 🧗🏼‍♂ E5.0 man climbing: medium-light skin tone -#1F9D7 1F3FD 200D 2642 FE0F ; fully-qualified # 🧗🏽‍♂️ E5.0 man climbing: medium skin tone -#1F9D7 1F3FD 200D 2642 ; minimally-qualified # 🧗🏽‍♂ E5.0 man climbing: medium skin tone -#1F9D7 1F3FE 200D 2642 FE0F ; fully-qualified # 🧗🏾‍♂️ E5.0 man climbing: medium-dark skin tone -#1F9D7 1F3FE 200D 2642 ; minimally-qualified # 🧗🏾‍♂ E5.0 man climbing: medium-dark skin tone -#1F9D7 1F3FF 200D 2642 FE0F ; fully-qualified # 🧗🏿‍♂️ E5.0 man climbing: dark skin tone -#1F9D7 1F3FF 200D 2642 ; minimally-qualified # 🧗🏿‍♂ E5.0 man climbing: dark skin tone -#1F9D7 200D 2640 FE0F ; fully-qualified # 🧗‍♀️ E5.0 woman climbing -#1F9D7 200D 2640 ; minimally-qualified # 🧗‍♀ E5.0 woman climbing -#1F9D7 1F3FB 200D 2640 FE0F ; fully-qualified # 🧗🏻‍♀️ E5.0 woman climbing: light skin tone -#1F9D7 1F3FB 200D 2640 ; minimally-qualified # 🧗🏻‍♀ E5.0 woman climbing: light skin tone -#1F9D7 1F3FC 200D 2640 FE0F ; fully-qualified # 🧗🏼‍♀️ E5.0 woman climbing: medium-light skin tone -#1F9D7 1F3FC 200D 2640 ; minimally-qualified # 🧗🏼‍♀ E5.0 woman climbing: medium-light skin tone -#1F9D7 1F3FD 200D 2640 FE0F ; fully-qualified # 🧗🏽‍♀️ E5.0 woman climbing: medium skin tone -#1F9D7 1F3FD 200D 2640 ; minimally-qualified # 🧗🏽‍♀ E5.0 woman climbing: medium skin tone -#1F9D7 1F3FE 200D 2640 FE0F ; fully-qualified # 🧗🏾‍♀️ E5.0 woman climbing: medium-dark skin tone -#1F9D7 1F3FE 200D 2640 ; minimally-qualified # 🧗🏾‍♀ E5.0 woman climbing: medium-dark skin tone -#1F9D7 1F3FF 200D 2640 FE0F ; fully-qualified # 🧗🏿‍♀️ E5.0 woman climbing: dark skin tone -#1F9D7 1F3FF 200D 2640 ; minimally-qualified # 🧗🏿‍♀ E5.0 woman climbing: dark skin tone -# -## subgroup: person-sport -#1F93A ; fully-qualified # 🤺 E3.0 person fencing -#1F3C7 ; fully-qualified # 🏇 E1.0 horse racing -#1F3C7 1F3FB ; fully-qualified # 🏇🏻 E1.0 horse racing: light skin tone -#1F3C7 1F3FC ; fully-qualified # 🏇🏼 E1.0 horse racing: medium-light skin tone -#1F3C7 1F3FD ; fully-qualified # 🏇🏽 E1.0 horse racing: medium skin tone -#1F3C7 1F3FE ; fully-qualified # 🏇🏾 E1.0 horse racing: medium-dark skin tone -#1F3C7 1F3FF ; fully-qualified # 🏇🏿 E1.0 horse racing: dark skin tone -#26F7 FE0F ; fully-qualified # ⛷️ E0.7 skier -#26F7 ; unqualified # ⛷ E0.7 skier -#1F3C2 ; fully-qualified # 🏂 E0.6 snowboarder -#1F3C2 1F3FB ; fully-qualified # 🏂🏻 E1.0 snowboarder: light skin tone -#1F3C2 1F3FC ; fully-qualified # 🏂🏼 E1.0 snowboarder: medium-light skin tone -#1F3C2 1F3FD ; fully-qualified # 🏂🏽 E1.0 snowboarder: medium skin tone -#1F3C2 1F3FE ; fully-qualified # 🏂🏾 E1.0 snowboarder: medium-dark skin tone -#1F3C2 1F3FF ; fully-qualified # 🏂🏿 E1.0 snowboarder: dark skin tone -#1F3CC FE0F ; fully-qualified # 🏌️ E0.7 person golfing -#1F3CC ; unqualified # 🏌 E0.7 person golfing -#1F3CC 1F3FB ; fully-qualified # 🏌🏻 E4.0 person golfing: light skin tone -#1F3CC 1F3FC ; fully-qualified # 🏌🏼 E4.0 person golfing: medium-light skin tone -#1F3CC 1F3FD ; fully-qualified # 🏌🏽 E4.0 person golfing: medium skin tone -#1F3CC 1F3FE ; fully-qualified # 🏌🏾 E4.0 person golfing: medium-dark skin tone -#1F3CC 1F3FF ; fully-qualified # 🏌🏿 E4.0 person golfing: dark skin tone -#1F3CC FE0F 200D 2642 FE0F ; fully-qualified # 🏌️‍♂️ E4.0 man golfing -#1F3CC 200D 2642 FE0F ; unqualified # 🏌‍♂️ E4.0 man golfing -#1F3CC FE0F 200D 2642 ; unqualified # 🏌️‍♂ E4.0 man golfing -#1F3CC 200D 2642 ; unqualified # 🏌‍♂ E4.0 man golfing -#1F3CC 1F3FB 200D 2642 FE0F ; fully-qualified # 🏌🏻‍♂️ E4.0 man golfing: light skin tone -#1F3CC 1F3FB 200D 2642 ; minimally-qualified # 🏌🏻‍♂ E4.0 man golfing: light skin tone -#1F3CC 1F3FC 200D 2642 FE0F ; fully-qualified # 🏌🏼‍♂️ E4.0 man golfing: medium-light skin tone -#1F3CC 1F3FC 200D 2642 ; minimally-qualified # 🏌🏼‍♂ E4.0 man golfing: medium-light skin tone -#1F3CC 1F3FD 200D 2642 FE0F ; fully-qualified # 🏌🏽‍♂️ E4.0 man golfing: medium skin tone -#1F3CC 1F3FD 200D 2642 ; minimally-qualified # 🏌🏽‍♂ E4.0 man golfing: medium skin tone -#1F3CC 1F3FE 200D 2642 FE0F ; fully-qualified # 🏌🏾‍♂️ E4.0 man golfing: medium-dark skin tone -#1F3CC 1F3FE 200D 2642 ; minimally-qualified # 🏌🏾‍♂ E4.0 man golfing: medium-dark skin tone -#1F3CC 1F3FF 200D 2642 FE0F ; fully-qualified # 🏌🏿‍♂️ E4.0 man golfing: dark skin tone -#1F3CC 1F3FF 200D 2642 ; minimally-qualified # 🏌🏿‍♂ E4.0 man golfing: dark skin tone -#1F3CC FE0F 200D 2640 FE0F ; fully-qualified # 🏌️‍♀️ E4.0 woman golfing -#1F3CC 200D 2640 FE0F ; unqualified # 🏌‍♀️ E4.0 woman golfing -#1F3CC FE0F 200D 2640 ; unqualified # 🏌️‍♀ E4.0 woman golfing -#1F3CC 200D 2640 ; unqualified # 🏌‍♀ E4.0 woman golfing -#1F3CC 1F3FB 200D 2640 FE0F ; fully-qualified # 🏌🏻‍♀️ E4.0 woman golfing: light skin tone -#1F3CC 1F3FB 200D 2640 ; minimally-qualified # 🏌🏻‍♀ E4.0 woman golfing: light skin tone -#1F3CC 1F3FC 200D 2640 FE0F ; fully-qualified # 🏌🏼‍♀️ E4.0 woman golfing: medium-light skin tone -#1F3CC 1F3FC 200D 2640 ; minimally-qualified # 🏌🏼‍♀ E4.0 woman golfing: medium-light skin tone -#1F3CC 1F3FD 200D 2640 FE0F ; fully-qualified # 🏌🏽‍♀️ E4.0 woman golfing: medium skin tone -#1F3CC 1F3FD 200D 2640 ; minimally-qualified # 🏌🏽‍♀ E4.0 woman golfing: medium skin tone -#1F3CC 1F3FE 200D 2640 FE0F ; fully-qualified # 🏌🏾‍♀️ E4.0 woman golfing: medium-dark skin tone -#1F3CC 1F3FE 200D 2640 ; minimally-qualified # 🏌🏾‍♀ E4.0 woman golfing: medium-dark skin tone -#1F3CC 1F3FF 200D 2640 FE0F ; fully-qualified # 🏌🏿‍♀️ E4.0 woman golfing: dark skin tone -#1F3CC 1F3FF 200D 2640 ; minimally-qualified # 🏌🏿‍♀ E4.0 woman golfing: dark skin tone -#1F3C4 ; fully-qualified # 🏄 E0.6 person surfing -#1F3C4 1F3FB ; fully-qualified # 🏄🏻 E1.0 person surfing: light skin tone -#1F3C4 1F3FC ; fully-qualified # 🏄🏼 E1.0 person surfing: medium-light skin tone -#1F3C4 1F3FD ; fully-qualified # 🏄🏽 E1.0 person surfing: medium skin tone -#1F3C4 1F3FE ; fully-qualified # 🏄🏾 E1.0 person surfing: medium-dark skin tone -#1F3C4 1F3FF ; fully-qualified # 🏄🏿 E1.0 person surfing: dark skin tone -#1F3C4 200D 2642 FE0F ; fully-qualified # 🏄‍♂️ E4.0 man surfing -#1F3C4 200D 2642 ; minimally-qualified # 🏄‍♂ E4.0 man surfing -#1F3C4 1F3FB 200D 2642 FE0F ; fully-qualified # 🏄🏻‍♂️ E4.0 man surfing: light skin tone -#1F3C4 1F3FB 200D 2642 ; minimally-qualified # 🏄🏻‍♂ E4.0 man surfing: light skin tone -#1F3C4 1F3FC 200D 2642 FE0F ; fully-qualified # 🏄🏼‍♂️ E4.0 man surfing: medium-light skin tone -#1F3C4 1F3FC 200D 2642 ; minimally-qualified # 🏄🏼‍♂ E4.0 man surfing: medium-light skin tone -#1F3C4 1F3FD 200D 2642 FE0F ; fully-qualified # 🏄🏽‍♂️ E4.0 man surfing: medium skin tone -#1F3C4 1F3FD 200D 2642 ; minimally-qualified # 🏄🏽‍♂ E4.0 man surfing: medium skin tone -#1F3C4 1F3FE 200D 2642 FE0F ; fully-qualified # 🏄🏾‍♂️ E4.0 man surfing: medium-dark skin tone -#1F3C4 1F3FE 200D 2642 ; minimally-qualified # 🏄🏾‍♂ E4.0 man surfing: medium-dark skin tone -#1F3C4 1F3FF 200D 2642 FE0F ; fully-qualified # 🏄🏿‍♂️ E4.0 man surfing: dark skin tone -#1F3C4 1F3FF 200D 2642 ; minimally-qualified # 🏄🏿‍♂ E4.0 man surfing: dark skin tone -#1F3C4 200D 2640 FE0F ; fully-qualified # 🏄‍♀️ E4.0 woman surfing -#1F3C4 200D 2640 ; minimally-qualified # 🏄‍♀ E4.0 woman surfing -#1F3C4 1F3FB 200D 2640 FE0F ; fully-qualified # 🏄🏻‍♀️ E4.0 woman surfing: light skin tone -#1F3C4 1F3FB 200D 2640 ; minimally-qualified # 🏄🏻‍♀ E4.0 woman surfing: light skin tone -#1F3C4 1F3FC 200D 2640 FE0F ; fully-qualified # 🏄🏼‍♀️ E4.0 woman surfing: medium-light skin tone -#1F3C4 1F3FC 200D 2640 ; minimally-qualified # 🏄🏼‍♀ E4.0 woman surfing: medium-light skin tone -#1F3C4 1F3FD 200D 2640 FE0F ; fully-qualified # 🏄🏽‍♀️ E4.0 woman surfing: medium skin tone -#1F3C4 1F3FD 200D 2640 ; minimally-qualified # 🏄🏽‍♀ E4.0 woman surfing: medium skin tone -#1F3C4 1F3FE 200D 2640 FE0F ; fully-qualified # 🏄🏾‍♀️ E4.0 woman surfing: medium-dark skin tone -#1F3C4 1F3FE 200D 2640 ; minimally-qualified # 🏄🏾‍♀ E4.0 woman surfing: medium-dark skin tone -#1F3C4 1F3FF 200D 2640 FE0F ; fully-qualified # 🏄🏿‍♀️ E4.0 woman surfing: dark skin tone -#1F3C4 1F3FF 200D 2640 ; minimally-qualified # 🏄🏿‍♀ E4.0 woman surfing: dark skin tone -#1F6A3 ; fully-qualified # 🚣 E1.0 person rowing boat -#1F6A3 1F3FB ; fully-qualified # 🚣🏻 E1.0 person rowing boat: light skin tone -#1F6A3 1F3FC ; fully-qualified # 🚣🏼 E1.0 person rowing boat: medium-light skin tone -#1F6A3 1F3FD ; fully-qualified # 🚣🏽 E1.0 person rowing boat: medium skin tone -#1F6A3 1F3FE ; fully-qualified # 🚣🏾 E1.0 person rowing boat: medium-dark skin tone -#1F6A3 1F3FF ; fully-qualified # 🚣🏿 E1.0 person rowing boat: dark skin tone -#1F6A3 200D 2642 FE0F ; fully-qualified # 🚣‍♂️ E4.0 man rowing boat -#1F6A3 200D 2642 ; minimally-qualified # 🚣‍♂ E4.0 man rowing boat -#1F6A3 1F3FB 200D 2642 FE0F ; fully-qualified # 🚣🏻‍♂️ E4.0 man rowing boat: light skin tone -#1F6A3 1F3FB 200D 2642 ; minimally-qualified # 🚣🏻‍♂ E4.0 man rowing boat: light skin tone -#1F6A3 1F3FC 200D 2642 FE0F ; fully-qualified # 🚣🏼‍♂️ E4.0 man rowing boat: medium-light skin tone -#1F6A3 1F3FC 200D 2642 ; minimally-qualified # 🚣🏼‍♂ E4.0 man rowing boat: medium-light skin tone -#1F6A3 1F3FD 200D 2642 FE0F ; fully-qualified # 🚣🏽‍♂️ E4.0 man rowing boat: medium skin tone -#1F6A3 1F3FD 200D 2642 ; minimally-qualified # 🚣🏽‍♂ E4.0 man rowing boat: medium skin tone -#1F6A3 1F3FE 200D 2642 FE0F ; fully-qualified # 🚣🏾‍♂️ E4.0 man rowing boat: medium-dark skin tone -#1F6A3 1F3FE 200D 2642 ; minimally-qualified # 🚣🏾‍♂ E4.0 man rowing boat: medium-dark skin tone -#1F6A3 1F3FF 200D 2642 FE0F ; fully-qualified # 🚣🏿‍♂️ E4.0 man rowing boat: dark skin tone -#1F6A3 1F3FF 200D 2642 ; minimally-qualified # 🚣🏿‍♂ E4.0 man rowing boat: dark skin tone -#1F6A3 200D 2640 FE0F ; fully-qualified # 🚣‍♀️ E4.0 woman rowing boat -#1F6A3 200D 2640 ; minimally-qualified # 🚣‍♀ E4.0 woman rowing boat -#1F6A3 1F3FB 200D 2640 FE0F ; fully-qualified # 🚣🏻‍♀️ E4.0 woman rowing boat: light skin tone -#1F6A3 1F3FB 200D 2640 ; minimally-qualified # 🚣🏻‍♀ E4.0 woman rowing boat: light skin tone -#1F6A3 1F3FC 200D 2640 FE0F ; fully-qualified # 🚣🏼‍♀️ E4.0 woman rowing boat: medium-light skin tone -#1F6A3 1F3FC 200D 2640 ; minimally-qualified # 🚣🏼‍♀ E4.0 woman rowing boat: medium-light skin tone -#1F6A3 1F3FD 200D 2640 FE0F ; fully-qualified # 🚣🏽‍♀️ E4.0 woman rowing boat: medium skin tone -#1F6A3 1F3FD 200D 2640 ; minimally-qualified # 🚣🏽‍♀ E4.0 woman rowing boat: medium skin tone -#1F6A3 1F3FE 200D 2640 FE0F ; fully-qualified # 🚣🏾‍♀️ E4.0 woman rowing boat: medium-dark skin tone -#1F6A3 1F3FE 200D 2640 ; minimally-qualified # 🚣🏾‍♀ E4.0 woman rowing boat: medium-dark skin tone -#1F6A3 1F3FF 200D 2640 FE0F ; fully-qualified # 🚣🏿‍♀️ E4.0 woman rowing boat: dark skin tone -#1F6A3 1F3FF 200D 2640 ; minimally-qualified # 🚣🏿‍♀ E4.0 woman rowing boat: dark skin tone -#1F3CA ; fully-qualified # 🏊 E0.6 person swimming -#1F3CA 1F3FB ; fully-qualified # 🏊🏻 E1.0 person swimming: light skin tone -#1F3CA 1F3FC ; fully-qualified # 🏊🏼 E1.0 person swimming: medium-light skin tone -#1F3CA 1F3FD ; fully-qualified # 🏊🏽 E1.0 person swimming: medium skin tone -#1F3CA 1F3FE ; fully-qualified # 🏊🏾 E1.0 person swimming: medium-dark skin tone -#1F3CA 1F3FF ; fully-qualified # 🏊🏿 E1.0 person swimming: dark skin tone -#1F3CA 200D 2642 FE0F ; fully-qualified # 🏊‍♂️ E4.0 man swimming -#1F3CA 200D 2642 ; minimally-qualified # 🏊‍♂ E4.0 man swimming -#1F3CA 1F3FB 200D 2642 FE0F ; fully-qualified # 🏊🏻‍♂️ E4.0 man swimming: light skin tone -#1F3CA 1F3FB 200D 2642 ; minimally-qualified # 🏊🏻‍♂ E4.0 man swimming: light skin tone -#1F3CA 1F3FC 200D 2642 FE0F ; fully-qualified # 🏊🏼‍♂️ E4.0 man swimming: medium-light skin tone -#1F3CA 1F3FC 200D 2642 ; minimally-qualified # 🏊🏼‍♂ E4.0 man swimming: medium-light skin tone -#1F3CA 1F3FD 200D 2642 FE0F ; fully-qualified # 🏊🏽‍♂️ E4.0 man swimming: medium skin tone -#1F3CA 1F3FD 200D 2642 ; minimally-qualified # 🏊🏽‍♂ E4.0 man swimming: medium skin tone -#1F3CA 1F3FE 200D 2642 FE0F ; fully-qualified # 🏊🏾‍♂️ E4.0 man swimming: medium-dark skin tone -#1F3CA 1F3FE 200D 2642 ; minimally-qualified # 🏊🏾‍♂ E4.0 man swimming: medium-dark skin tone -#1F3CA 1F3FF 200D 2642 FE0F ; fully-qualified # 🏊🏿‍♂️ E4.0 man swimming: dark skin tone -#1F3CA 1F3FF 200D 2642 ; minimally-qualified # 🏊🏿‍♂ E4.0 man swimming: dark skin tone -#1F3CA 200D 2640 FE0F ; fully-qualified # 🏊‍♀️ E4.0 woman swimming -#1F3CA 200D 2640 ; minimally-qualified # 🏊‍♀ E4.0 woman swimming -#1F3CA 1F3FB 200D 2640 FE0F ; fully-qualified # 🏊🏻‍♀️ E4.0 woman swimming: light skin tone -#1F3CA 1F3FB 200D 2640 ; minimally-qualified # 🏊🏻‍♀ E4.0 woman swimming: light skin tone -#1F3CA 1F3FC 200D 2640 FE0F ; fully-qualified # 🏊🏼‍♀️ E4.0 woman swimming: medium-light skin tone -#1F3CA 1F3FC 200D 2640 ; minimally-qualified # 🏊🏼‍♀ E4.0 woman swimming: medium-light skin tone -#1F3CA 1F3FD 200D 2640 FE0F ; fully-qualified # 🏊🏽‍♀️ E4.0 woman swimming: medium skin tone -#1F3CA 1F3FD 200D 2640 ; minimally-qualified # 🏊🏽‍♀ E4.0 woman swimming: medium skin tone -#1F3CA 1F3FE 200D 2640 FE0F ; fully-qualified # 🏊🏾‍♀️ E4.0 woman swimming: medium-dark skin tone -#1F3CA 1F3FE 200D 2640 ; minimally-qualified # 🏊🏾‍♀ E4.0 woman swimming: medium-dark skin tone -#1F3CA 1F3FF 200D 2640 FE0F ; fully-qualified # 🏊🏿‍♀️ E4.0 woman swimming: dark skin tone -#1F3CA 1F3FF 200D 2640 ; minimally-qualified # 🏊🏿‍♀ E4.0 woman swimming: dark skin tone -#26F9 FE0F ; fully-qualified # ⛹️ E0.7 person bouncing ball -#26F9 ; unqualified # ⛹ E0.7 person bouncing ball -#26F9 1F3FB ; fully-qualified # ⛹🏻 E2.0 person bouncing ball: light skin tone -#26F9 1F3FC ; fully-qualified # ⛹🏼 E2.0 person bouncing ball: medium-light skin tone -#26F9 1F3FD ; fully-qualified # ⛹🏽 E2.0 person bouncing ball: medium skin tone -#26F9 1F3FE ; fully-qualified # ⛹🏾 E2.0 person bouncing ball: medium-dark skin tone -#26F9 1F3FF ; fully-qualified # ⛹🏿 E2.0 person bouncing ball: dark skin tone -#26F9 FE0F 200D 2642 FE0F ; fully-qualified # ⛹️‍♂️ E4.0 man bouncing ball -#26F9 200D 2642 FE0F ; unqualified # ⛹‍♂️ E4.0 man bouncing ball -#26F9 FE0F 200D 2642 ; unqualified # ⛹️‍♂ E4.0 man bouncing ball -#26F9 200D 2642 ; unqualified # ⛹‍♂ E4.0 man bouncing ball -#26F9 1F3FB 200D 2642 FE0F ; fully-qualified # ⛹🏻‍♂️ E4.0 man bouncing ball: light skin tone -#26F9 1F3FB 200D 2642 ; minimally-qualified # ⛹🏻‍♂ E4.0 man bouncing ball: light skin tone -#26F9 1F3FC 200D 2642 FE0F ; fully-qualified # ⛹🏼‍♂️ E4.0 man bouncing ball: medium-light skin tone -#26F9 1F3FC 200D 2642 ; minimally-qualified # ⛹🏼‍♂ E4.0 man bouncing ball: medium-light skin tone -#26F9 1F3FD 200D 2642 FE0F ; fully-qualified # ⛹🏽‍♂️ E4.0 man bouncing ball: medium skin tone -#26F9 1F3FD 200D 2642 ; minimally-qualified # ⛹🏽‍♂ E4.0 man bouncing ball: medium skin tone -#26F9 1F3FE 200D 2642 FE0F ; fully-qualified # ⛹🏾‍♂️ E4.0 man bouncing ball: medium-dark skin tone -#26F9 1F3FE 200D 2642 ; minimally-qualified # ⛹🏾‍♂ E4.0 man bouncing ball: medium-dark skin tone -#26F9 1F3FF 200D 2642 FE0F ; fully-qualified # ⛹🏿‍♂️ E4.0 man bouncing ball: dark skin tone -#26F9 1F3FF 200D 2642 ; minimally-qualified # ⛹🏿‍♂ E4.0 man bouncing ball: dark skin tone -#26F9 FE0F 200D 2640 FE0F ; fully-qualified # ⛹️‍♀️ E4.0 woman bouncing ball -#26F9 200D 2640 FE0F ; unqualified # ⛹‍♀️ E4.0 woman bouncing ball -#26F9 FE0F 200D 2640 ; unqualified # ⛹️‍♀ E4.0 woman bouncing ball -#26F9 200D 2640 ; unqualified # ⛹‍♀ E4.0 woman bouncing ball -#26F9 1F3FB 200D 2640 FE0F ; fully-qualified # ⛹🏻‍♀️ E4.0 woman bouncing ball: light skin tone -#26F9 1F3FB 200D 2640 ; minimally-qualified # ⛹🏻‍♀ E4.0 woman bouncing ball: light skin tone -#26F9 1F3FC 200D 2640 FE0F ; fully-qualified # ⛹🏼‍♀️ E4.0 woman bouncing ball: medium-light skin tone -#26F9 1F3FC 200D 2640 ; minimally-qualified # ⛹🏼‍♀ E4.0 woman bouncing ball: medium-light skin tone -#26F9 1F3FD 200D 2640 FE0F ; fully-qualified # ⛹🏽‍♀️ E4.0 woman bouncing ball: medium skin tone -#26F9 1F3FD 200D 2640 ; minimally-qualified # ⛹🏽‍♀ E4.0 woman bouncing ball: medium skin tone -#26F9 1F3FE 200D 2640 FE0F ; fully-qualified # ⛹🏾‍♀️ E4.0 woman bouncing ball: medium-dark skin tone -#26F9 1F3FE 200D 2640 ; minimally-qualified # ⛹🏾‍♀ E4.0 woman bouncing ball: medium-dark skin tone -#26F9 1F3FF 200D 2640 FE0F ; fully-qualified # ⛹🏿‍♀️ E4.0 woman bouncing ball: dark skin tone -#26F9 1F3FF 200D 2640 ; minimally-qualified # ⛹🏿‍♀ E4.0 woman bouncing ball: dark skin tone -#1F3CB FE0F ; fully-qualified # 🏋️ E0.7 person lifting weights -#1F3CB ; unqualified # 🏋 E0.7 person lifting weights -#1F3CB 1F3FB ; fully-qualified # 🏋🏻 E2.0 person lifting weights: light skin tone -#1F3CB 1F3FC ; fully-qualified # 🏋🏼 E2.0 person lifting weights: medium-light skin tone -#1F3CB 1F3FD ; fully-qualified # 🏋🏽 E2.0 person lifting weights: medium skin tone -#1F3CB 1F3FE ; fully-qualified # 🏋🏾 E2.0 person lifting weights: medium-dark skin tone -#1F3CB 1F3FF ; fully-qualified # 🏋🏿 E2.0 person lifting weights: dark skin tone -#1F3CB FE0F 200D 2642 FE0F ; fully-qualified # 🏋️‍♂️ E4.0 man lifting weights -#1F3CB 200D 2642 FE0F ; unqualified # 🏋‍♂️ E4.0 man lifting weights -#1F3CB FE0F 200D 2642 ; unqualified # 🏋️‍♂ E4.0 man lifting weights -#1F3CB 200D 2642 ; unqualified # 🏋‍♂ E4.0 man lifting weights -#1F3CB 1F3FB 200D 2642 FE0F ; fully-qualified # 🏋🏻‍♂️ E4.0 man lifting weights: light skin tone -#1F3CB 1F3FB 200D 2642 ; minimally-qualified # 🏋🏻‍♂ E4.0 man lifting weights: light skin tone -#1F3CB 1F3FC 200D 2642 FE0F ; fully-qualified # 🏋🏼‍♂️ E4.0 man lifting weights: medium-light skin tone -#1F3CB 1F3FC 200D 2642 ; minimally-qualified # 🏋🏼‍♂ E4.0 man lifting weights: medium-light skin tone -#1F3CB 1F3FD 200D 2642 FE0F ; fully-qualified # 🏋🏽‍♂️ E4.0 man lifting weights: medium skin tone -#1F3CB 1F3FD 200D 2642 ; minimally-qualified # 🏋🏽‍♂ E4.0 man lifting weights: medium skin tone -#1F3CB 1F3FE 200D 2642 FE0F ; fully-qualified # 🏋🏾‍♂️ E4.0 man lifting weights: medium-dark skin tone -#1F3CB 1F3FE 200D 2642 ; minimally-qualified # 🏋🏾‍♂ E4.0 man lifting weights: medium-dark skin tone -#1F3CB 1F3FF 200D 2642 FE0F ; fully-qualified # 🏋🏿‍♂️ E4.0 man lifting weights: dark skin tone -#1F3CB 1F3FF 200D 2642 ; minimally-qualified # 🏋🏿‍♂ E4.0 man lifting weights: dark skin tone -#1F3CB FE0F 200D 2640 FE0F ; fully-qualified # 🏋️‍♀️ E4.0 woman lifting weights -#1F3CB 200D 2640 FE0F ; unqualified # 🏋‍♀️ E4.0 woman lifting weights -#1F3CB FE0F 200D 2640 ; unqualified # 🏋️‍♀ E4.0 woman lifting weights -#1F3CB 200D 2640 ; unqualified # 🏋‍♀ E4.0 woman lifting weights -#1F3CB 1F3FB 200D 2640 FE0F ; fully-qualified # 🏋🏻‍♀️ E4.0 woman lifting weights: light skin tone -#1F3CB 1F3FB 200D 2640 ; minimally-qualified # 🏋🏻‍♀ E4.0 woman lifting weights: light skin tone -#1F3CB 1F3FC 200D 2640 FE0F ; fully-qualified # 🏋🏼‍♀️ E4.0 woman lifting weights: medium-light skin tone -#1F3CB 1F3FC 200D 2640 ; minimally-qualified # 🏋🏼‍♀ E4.0 woman lifting weights: medium-light skin tone -#1F3CB 1F3FD 200D 2640 FE0F ; fully-qualified # 🏋🏽‍♀️ E4.0 woman lifting weights: medium skin tone -#1F3CB 1F3FD 200D 2640 ; minimally-qualified # 🏋🏽‍♀ E4.0 woman lifting weights: medium skin tone -#1F3CB 1F3FE 200D 2640 FE0F ; fully-qualified # 🏋🏾‍♀️ E4.0 woman lifting weights: medium-dark skin tone -#1F3CB 1F3FE 200D 2640 ; minimally-qualified # 🏋🏾‍♀ E4.0 woman lifting weights: medium-dark skin tone -#1F3CB 1F3FF 200D 2640 FE0F ; fully-qualified # 🏋🏿‍♀️ E4.0 woman lifting weights: dark skin tone -#1F3CB 1F3FF 200D 2640 ; minimally-qualified # 🏋🏿‍♀ E4.0 woman lifting weights: dark skin tone -#1F6B4 ; fully-qualified # 🚴 E1.0 person biking -#1F6B4 1F3FB ; fully-qualified # 🚴🏻 E1.0 person biking: light skin tone -#1F6B4 1F3FC ; fully-qualified # 🚴🏼 E1.0 person biking: medium-light skin tone -#1F6B4 1F3FD ; fully-qualified # 🚴🏽 E1.0 person biking: medium skin tone -#1F6B4 1F3FE ; fully-qualified # 🚴🏾 E1.0 person biking: medium-dark skin tone -#1F6B4 1F3FF ; fully-qualified # 🚴🏿 E1.0 person biking: dark skin tone -#1F6B4 200D 2642 FE0F ; fully-qualified # 🚴‍♂️ E4.0 man biking -#1F6B4 200D 2642 ; minimally-qualified # 🚴‍♂ E4.0 man biking -#1F6B4 1F3FB 200D 2642 FE0F ; fully-qualified # 🚴🏻‍♂️ E4.0 man biking: light skin tone -#1F6B4 1F3FB 200D 2642 ; minimally-qualified # 🚴🏻‍♂ E4.0 man biking: light skin tone -#1F6B4 1F3FC 200D 2642 FE0F ; fully-qualified # 🚴🏼‍♂️ E4.0 man biking: medium-light skin tone -#1F6B4 1F3FC 200D 2642 ; minimally-qualified # 🚴🏼‍♂ E4.0 man biking: medium-light skin tone -#1F6B4 1F3FD 200D 2642 FE0F ; fully-qualified # 🚴🏽‍♂️ E4.0 man biking: medium skin tone -#1F6B4 1F3FD 200D 2642 ; minimally-qualified # 🚴🏽‍♂ E4.0 man biking: medium skin tone -#1F6B4 1F3FE 200D 2642 FE0F ; fully-qualified # 🚴🏾‍♂️ E4.0 man biking: medium-dark skin tone -#1F6B4 1F3FE 200D 2642 ; minimally-qualified # 🚴🏾‍♂ E4.0 man biking: medium-dark skin tone -#1F6B4 1F3FF 200D 2642 FE0F ; fully-qualified # 🚴🏿‍♂️ E4.0 man biking: dark skin tone -#1F6B4 1F3FF 200D 2642 ; minimally-qualified # 🚴🏿‍♂ E4.0 man biking: dark skin tone -#1F6B4 200D 2640 FE0F ; fully-qualified # 🚴‍♀️ E4.0 woman biking -#1F6B4 200D 2640 ; minimally-qualified # 🚴‍♀ E4.0 woman biking -#1F6B4 1F3FB 200D 2640 FE0F ; fully-qualified # 🚴🏻‍♀️ E4.0 woman biking: light skin tone -#1F6B4 1F3FB 200D 2640 ; minimally-qualified # 🚴🏻‍♀ E4.0 woman biking: light skin tone -#1F6B4 1F3FC 200D 2640 FE0F ; fully-qualified # 🚴🏼‍♀️ E4.0 woman biking: medium-light skin tone -#1F6B4 1F3FC 200D 2640 ; minimally-qualified # 🚴🏼‍♀ E4.0 woman biking: medium-light skin tone -#1F6B4 1F3FD 200D 2640 FE0F ; fully-qualified # 🚴🏽‍♀️ E4.0 woman biking: medium skin tone -#1F6B4 1F3FD 200D 2640 ; minimally-qualified # 🚴🏽‍♀ E4.0 woman biking: medium skin tone -#1F6B4 1F3FE 200D 2640 FE0F ; fully-qualified # 🚴🏾‍♀️ E4.0 woman biking: medium-dark skin tone -#1F6B4 1F3FE 200D 2640 ; minimally-qualified # 🚴🏾‍♀ E4.0 woman biking: medium-dark skin tone -#1F6B4 1F3FF 200D 2640 FE0F ; fully-qualified # 🚴🏿‍♀️ E4.0 woman biking: dark skin tone -#1F6B4 1F3FF 200D 2640 ; minimally-qualified # 🚴🏿‍♀ E4.0 woman biking: dark skin tone -#1F6B5 ; fully-qualified # 🚵 E1.0 person mountain biking -#1F6B5 1F3FB ; fully-qualified # 🚵🏻 E1.0 person mountain biking: light skin tone -#1F6B5 1F3FC ; fully-qualified # 🚵🏼 E1.0 person mountain biking: medium-light skin tone -#1F6B5 1F3FD ; fully-qualified # 🚵🏽 E1.0 person mountain biking: medium skin tone -#1F6B5 1F3FE ; fully-qualified # 🚵🏾 E1.0 person mountain biking: medium-dark skin tone -#1F6B5 1F3FF ; fully-qualified # 🚵🏿 E1.0 person mountain biking: dark skin tone -#1F6B5 200D 2642 FE0F ; fully-qualified # 🚵‍♂️ E4.0 man mountain biking -#1F6B5 200D 2642 ; minimally-qualified # 🚵‍♂ E4.0 man mountain biking -#1F6B5 1F3FB 200D 2642 FE0F ; fully-qualified # 🚵🏻‍♂️ E4.0 man mountain biking: light skin tone -#1F6B5 1F3FB 200D 2642 ; minimally-qualified # 🚵🏻‍♂ E4.0 man mountain biking: light skin tone -#1F6B5 1F3FC 200D 2642 FE0F ; fully-qualified # 🚵🏼‍♂️ E4.0 man mountain biking: medium-light skin tone -#1F6B5 1F3FC 200D 2642 ; minimally-qualified # 🚵🏼‍♂ E4.0 man mountain biking: medium-light skin tone -#1F6B5 1F3FD 200D 2642 FE0F ; fully-qualified # 🚵🏽‍♂️ E4.0 man mountain biking: medium skin tone -#1F6B5 1F3FD 200D 2642 ; minimally-qualified # 🚵🏽‍♂ E4.0 man mountain biking: medium skin tone -#1F6B5 1F3FE 200D 2642 FE0F ; fully-qualified # 🚵🏾‍♂️ E4.0 man mountain biking: medium-dark skin tone -#1F6B5 1F3FE 200D 2642 ; minimally-qualified # 🚵🏾‍♂ E4.0 man mountain biking: medium-dark skin tone -#1F6B5 1F3FF 200D 2642 FE0F ; fully-qualified # 🚵🏿‍♂️ E4.0 man mountain biking: dark skin tone -#1F6B5 1F3FF 200D 2642 ; minimally-qualified # 🚵🏿‍♂ E4.0 man mountain biking: dark skin tone -#1F6B5 200D 2640 FE0F ; fully-qualified # 🚵‍♀️ E4.0 woman mountain biking -#1F6B5 200D 2640 ; minimally-qualified # 🚵‍♀ E4.0 woman mountain biking -#1F6B5 1F3FB 200D 2640 FE0F ; fully-qualified # 🚵🏻‍♀️ E4.0 woman mountain biking: light skin tone -#1F6B5 1F3FB 200D 2640 ; minimally-qualified # 🚵🏻‍♀ E4.0 woman mountain biking: light skin tone -#1F6B5 1F3FC 200D 2640 FE0F ; fully-qualified # 🚵🏼‍♀️ E4.0 woman mountain biking: medium-light skin tone -#1F6B5 1F3FC 200D 2640 ; minimally-qualified # 🚵🏼‍♀ E4.0 woman mountain biking: medium-light skin tone -#1F6B5 1F3FD 200D 2640 FE0F ; fully-qualified # 🚵🏽‍♀️ E4.0 woman mountain biking: medium skin tone -#1F6B5 1F3FD 200D 2640 ; minimally-qualified # 🚵🏽‍♀ E4.0 woman mountain biking: medium skin tone -#1F6B5 1F3FE 200D 2640 FE0F ; fully-qualified # 🚵🏾‍♀️ E4.0 woman mountain biking: medium-dark skin tone -#1F6B5 1F3FE 200D 2640 ; minimally-qualified # 🚵🏾‍♀ E4.0 woman mountain biking: medium-dark skin tone -#1F6B5 1F3FF 200D 2640 FE0F ; fully-qualified # 🚵🏿‍♀️ E4.0 woman mountain biking: dark skin tone -#1F6B5 1F3FF 200D 2640 ; minimally-qualified # 🚵🏿‍♀ E4.0 woman mountain biking: dark skin tone -#1F938 ; fully-qualified # 🤸 E3.0 person cartwheeling -#1F938 1F3FB ; fully-qualified # 🤸🏻 E3.0 person cartwheeling: light skin tone -#1F938 1F3FC ; fully-qualified # 🤸🏼 E3.0 person cartwheeling: medium-light skin tone -#1F938 1F3FD ; fully-qualified # 🤸🏽 E3.0 person cartwheeling: medium skin tone -#1F938 1F3FE ; fully-qualified # 🤸🏾 E3.0 person cartwheeling: medium-dark skin tone -#1F938 1F3FF ; fully-qualified # 🤸🏿 E3.0 person cartwheeling: dark skin tone -#1F938 200D 2642 FE0F ; fully-qualified # 🤸‍♂️ E4.0 man cartwheeling -#1F938 200D 2642 ; minimally-qualified # 🤸‍♂ E4.0 man cartwheeling -#1F938 1F3FB 200D 2642 FE0F ; fully-qualified # 🤸🏻‍♂️ E4.0 man cartwheeling: light skin tone -#1F938 1F3FB 200D 2642 ; minimally-qualified # 🤸🏻‍♂ E4.0 man cartwheeling: light skin tone -#1F938 1F3FC 200D 2642 FE0F ; fully-qualified # 🤸🏼‍♂️ E4.0 man cartwheeling: medium-light skin tone -#1F938 1F3FC 200D 2642 ; minimally-qualified # 🤸🏼‍♂ E4.0 man cartwheeling: medium-light skin tone -#1F938 1F3FD 200D 2642 FE0F ; fully-qualified # 🤸🏽‍♂️ E4.0 man cartwheeling: medium skin tone -#1F938 1F3FD 200D 2642 ; minimally-qualified # 🤸🏽‍♂ E4.0 man cartwheeling: medium skin tone -#1F938 1F3FE 200D 2642 FE0F ; fully-qualified # 🤸🏾‍♂️ E4.0 man cartwheeling: medium-dark skin tone -#1F938 1F3FE 200D 2642 ; minimally-qualified # 🤸🏾‍♂ E4.0 man cartwheeling: medium-dark skin tone -#1F938 1F3FF 200D 2642 FE0F ; fully-qualified # 🤸🏿‍♂️ E4.0 man cartwheeling: dark skin tone -#1F938 1F3FF 200D 2642 ; minimally-qualified # 🤸🏿‍♂ E4.0 man cartwheeling: dark skin tone -#1F938 200D 2640 FE0F ; fully-qualified # 🤸‍♀️ E4.0 woman cartwheeling -#1F938 200D 2640 ; minimally-qualified # 🤸‍♀ E4.0 woman cartwheeling -#1F938 1F3FB 200D 2640 FE0F ; fully-qualified # 🤸🏻‍♀️ E4.0 woman cartwheeling: light skin tone -#1F938 1F3FB 200D 2640 ; minimally-qualified # 🤸🏻‍♀ E4.0 woman cartwheeling: light skin tone -#1F938 1F3FC 200D 2640 FE0F ; fully-qualified # 🤸🏼‍♀️ E4.0 woman cartwheeling: medium-light skin tone -#1F938 1F3FC 200D 2640 ; minimally-qualified # 🤸🏼‍♀ E4.0 woman cartwheeling: medium-light skin tone -#1F938 1F3FD 200D 2640 FE0F ; fully-qualified # 🤸🏽‍♀️ E4.0 woman cartwheeling: medium skin tone -#1F938 1F3FD 200D 2640 ; minimally-qualified # 🤸🏽‍♀ E4.0 woman cartwheeling: medium skin tone -#1F938 1F3FE 200D 2640 FE0F ; fully-qualified # 🤸🏾‍♀️ E4.0 woman cartwheeling: medium-dark skin tone -#1F938 1F3FE 200D 2640 ; minimally-qualified # 🤸🏾‍♀ E4.0 woman cartwheeling: medium-dark skin tone -#1F938 1F3FF 200D 2640 FE0F ; fully-qualified # 🤸🏿‍♀️ E4.0 woman cartwheeling: dark skin tone -#1F938 1F3FF 200D 2640 ; minimally-qualified # 🤸🏿‍♀ E4.0 woman cartwheeling: dark skin tone -#1F93C ; fully-qualified # 🤼 E3.0 people wrestling -#1F93C 200D 2642 FE0F ; fully-qualified # 🤼‍♂️ E4.0 men wrestling -#1F93C 200D 2642 ; minimally-qualified # 🤼‍♂ E4.0 men wrestling -#1F93C 200D 2640 FE0F ; fully-qualified # 🤼‍♀️ E4.0 women wrestling -#1F93C 200D 2640 ; minimally-qualified # 🤼‍♀ E4.0 women wrestling -#1F93D ; fully-qualified # 🤽 E3.0 person playing water polo -#1F93D 1F3FB ; fully-qualified # 🤽🏻 E3.0 person playing water polo: light skin tone -#1F93D 1F3FC ; fully-qualified # 🤽🏼 E3.0 person playing water polo: medium-light skin tone -#1F93D 1F3FD ; fully-qualified # 🤽🏽 E3.0 person playing water polo: medium skin tone -#1F93D 1F3FE ; fully-qualified # 🤽🏾 E3.0 person playing water polo: medium-dark skin tone -#1F93D 1F3FF ; fully-qualified # 🤽🏿 E3.0 person playing water polo: dark skin tone -#1F93D 200D 2642 FE0F ; fully-qualified # 🤽‍♂️ E4.0 man playing water polo -#1F93D 200D 2642 ; minimally-qualified # 🤽‍♂ E4.0 man playing water polo -#1F93D 1F3FB 200D 2642 FE0F ; fully-qualified # 🤽🏻‍♂️ E4.0 man playing water polo: light skin tone -#1F93D 1F3FB 200D 2642 ; minimally-qualified # 🤽🏻‍♂ E4.0 man playing water polo: light skin tone -#1F93D 1F3FC 200D 2642 FE0F ; fully-qualified # 🤽🏼‍♂️ E4.0 man playing water polo: medium-light skin tone -#1F93D 1F3FC 200D 2642 ; minimally-qualified # 🤽🏼‍♂ E4.0 man playing water polo: medium-light skin tone -#1F93D 1F3FD 200D 2642 FE0F ; fully-qualified # 🤽🏽‍♂️ E4.0 man playing water polo: medium skin tone -#1F93D 1F3FD 200D 2642 ; minimally-qualified # 🤽🏽‍♂ E4.0 man playing water polo: medium skin tone -#1F93D 1F3FE 200D 2642 FE0F ; fully-qualified # 🤽🏾‍♂️ E4.0 man playing water polo: medium-dark skin tone -#1F93D 1F3FE 200D 2642 ; minimally-qualified # 🤽🏾‍♂ E4.0 man playing water polo: medium-dark skin tone -#1F93D 1F3FF 200D 2642 FE0F ; fully-qualified # 🤽🏿‍♂️ E4.0 man playing water polo: dark skin tone -#1F93D 1F3FF 200D 2642 ; minimally-qualified # 🤽🏿‍♂ E4.0 man playing water polo: dark skin tone -#1F93D 200D 2640 FE0F ; fully-qualified # 🤽‍♀️ E4.0 woman playing water polo -#1F93D 200D 2640 ; minimally-qualified # 🤽‍♀ E4.0 woman playing water polo -#1F93D 1F3FB 200D 2640 FE0F ; fully-qualified # 🤽🏻‍♀️ E4.0 woman playing water polo: light skin tone -#1F93D 1F3FB 200D 2640 ; minimally-qualified # 🤽🏻‍♀ E4.0 woman playing water polo: light skin tone -#1F93D 1F3FC 200D 2640 FE0F ; fully-qualified # 🤽🏼‍♀️ E4.0 woman playing water polo: medium-light skin tone -#1F93D 1F3FC 200D 2640 ; minimally-qualified # 🤽🏼‍♀ E4.0 woman playing water polo: medium-light skin tone -#1F93D 1F3FD 200D 2640 FE0F ; fully-qualified # 🤽🏽‍♀️ E4.0 woman playing water polo: medium skin tone -#1F93D 1F3FD 200D 2640 ; minimally-qualified # 🤽🏽‍♀ E4.0 woman playing water polo: medium skin tone -#1F93D 1F3FE 200D 2640 FE0F ; fully-qualified # 🤽🏾‍♀️ E4.0 woman playing water polo: medium-dark skin tone -#1F93D 1F3FE 200D 2640 ; minimally-qualified # 🤽🏾‍♀ E4.0 woman playing water polo: medium-dark skin tone -#1F93D 1F3FF 200D 2640 FE0F ; fully-qualified # 🤽🏿‍♀️ E4.0 woman playing water polo: dark skin tone -#1F93D 1F3FF 200D 2640 ; minimally-qualified # 🤽🏿‍♀ E4.0 woman playing water polo: dark skin tone -#1F93E ; fully-qualified # 🤾 E3.0 person playing handball -#1F93E 1F3FB ; fully-qualified # 🤾🏻 E3.0 person playing handball: light skin tone -#1F93E 1F3FC ; fully-qualified # 🤾🏼 E3.0 person playing handball: medium-light skin tone -#1F93E 1F3FD ; fully-qualified # 🤾🏽 E3.0 person playing handball: medium skin tone -#1F93E 1F3FE ; fully-qualified # 🤾🏾 E3.0 person playing handball: medium-dark skin tone -#1F93E 1F3FF ; fully-qualified # 🤾🏿 E3.0 person playing handball: dark skin tone -#1F93E 200D 2642 FE0F ; fully-qualified # 🤾‍♂️ E4.0 man playing handball -#1F93E 200D 2642 ; minimally-qualified # 🤾‍♂ E4.0 man playing handball -#1F93E 1F3FB 200D 2642 FE0F ; fully-qualified # 🤾🏻‍♂️ E4.0 man playing handball: light skin tone -#1F93E 1F3FB 200D 2642 ; minimally-qualified # 🤾🏻‍♂ E4.0 man playing handball: light skin tone -#1F93E 1F3FC 200D 2642 FE0F ; fully-qualified # 🤾🏼‍♂️ E4.0 man playing handball: medium-light skin tone -#1F93E 1F3FC 200D 2642 ; minimally-qualified # 🤾🏼‍♂ E4.0 man playing handball: medium-light skin tone -#1F93E 1F3FD 200D 2642 FE0F ; fully-qualified # 🤾🏽‍♂️ E4.0 man playing handball: medium skin tone -#1F93E 1F3FD 200D 2642 ; minimally-qualified # 🤾🏽‍♂ E4.0 man playing handball: medium skin tone -#1F93E 1F3FE 200D 2642 FE0F ; fully-qualified # 🤾🏾‍♂️ E4.0 man playing handball: medium-dark skin tone -#1F93E 1F3FE 200D 2642 ; minimally-qualified # 🤾🏾‍♂ E4.0 man playing handball: medium-dark skin tone -#1F93E 1F3FF 200D 2642 FE0F ; fully-qualified # 🤾🏿‍♂️ E4.0 man playing handball: dark skin tone -#1F93E 1F3FF 200D 2642 ; minimally-qualified # 🤾🏿‍♂ E4.0 man playing handball: dark skin tone -#1F93E 200D 2640 FE0F ; fully-qualified # 🤾‍♀️ E4.0 woman playing handball -#1F93E 200D 2640 ; minimally-qualified # 🤾‍♀ E4.0 woman playing handball -#1F93E 1F3FB 200D 2640 FE0F ; fully-qualified # 🤾🏻‍♀️ E4.0 woman playing handball: light skin tone -#1F93E 1F3FB 200D 2640 ; minimally-qualified # 🤾🏻‍♀ E4.0 woman playing handball: light skin tone -#1F93E 1F3FC 200D 2640 FE0F ; fully-qualified # 🤾🏼‍♀️ E4.0 woman playing handball: medium-light skin tone -#1F93E 1F3FC 200D 2640 ; minimally-qualified # 🤾🏼‍♀ E4.0 woman playing handball: medium-light skin tone -#1F93E 1F3FD 200D 2640 FE0F ; fully-qualified # 🤾🏽‍♀️ E4.0 woman playing handball: medium skin tone -#1F93E 1F3FD 200D 2640 ; minimally-qualified # 🤾🏽‍♀ E4.0 woman playing handball: medium skin tone -#1F93E 1F3FE 200D 2640 FE0F ; fully-qualified # 🤾🏾‍♀️ E4.0 woman playing handball: medium-dark skin tone -#1F93E 1F3FE 200D 2640 ; minimally-qualified # 🤾🏾‍♀ E4.0 woman playing handball: medium-dark skin tone -#1F93E 1F3FF 200D 2640 FE0F ; fully-qualified # 🤾🏿‍♀️ E4.0 woman playing handball: dark skin tone -#1F93E 1F3FF 200D 2640 ; minimally-qualified # 🤾🏿‍♀ E4.0 woman playing handball: dark skin tone -#1F939 ; fully-qualified # 🤹 E3.0 person juggling -#1F939 1F3FB ; fully-qualified # 🤹🏻 E3.0 person juggling: light skin tone -#1F939 1F3FC ; fully-qualified # 🤹🏼 E3.0 person juggling: medium-light skin tone -#1F939 1F3FD ; fully-qualified # 🤹🏽 E3.0 person juggling: medium skin tone -#1F939 1F3FE ; fully-qualified # 🤹🏾 E3.0 person juggling: medium-dark skin tone -#1F939 1F3FF ; fully-qualified # 🤹🏿 E3.0 person juggling: dark skin tone -#1F939 200D 2642 FE0F ; fully-qualified # 🤹‍♂️ E4.0 man juggling -#1F939 200D 2642 ; minimally-qualified # 🤹‍♂ E4.0 man juggling -#1F939 1F3FB 200D 2642 FE0F ; fully-qualified # 🤹🏻‍♂️ E4.0 man juggling: light skin tone -#1F939 1F3FB 200D 2642 ; minimally-qualified # 🤹🏻‍♂ E4.0 man juggling: light skin tone -#1F939 1F3FC 200D 2642 FE0F ; fully-qualified # 🤹🏼‍♂️ E4.0 man juggling: medium-light skin tone -#1F939 1F3FC 200D 2642 ; minimally-qualified # 🤹🏼‍♂ E4.0 man juggling: medium-light skin tone -#1F939 1F3FD 200D 2642 FE0F ; fully-qualified # 🤹🏽‍♂️ E4.0 man juggling: medium skin tone -#1F939 1F3FD 200D 2642 ; minimally-qualified # 🤹🏽‍♂ E4.0 man juggling: medium skin tone -#1F939 1F3FE 200D 2642 FE0F ; fully-qualified # 🤹🏾‍♂️ E4.0 man juggling: medium-dark skin tone -#1F939 1F3FE 200D 2642 ; minimally-qualified # 🤹🏾‍♂ E4.0 man juggling: medium-dark skin tone -#1F939 1F3FF 200D 2642 FE0F ; fully-qualified # 🤹🏿‍♂️ E4.0 man juggling: dark skin tone -#1F939 1F3FF 200D 2642 ; minimally-qualified # 🤹🏿‍♂ E4.0 man juggling: dark skin tone -#1F939 200D 2640 FE0F ; fully-qualified # 🤹‍♀️ E4.0 woman juggling -#1F939 200D 2640 ; minimally-qualified # 🤹‍♀ E4.0 woman juggling -#1F939 1F3FB 200D 2640 FE0F ; fully-qualified # 🤹🏻‍♀️ E4.0 woman juggling: light skin tone -#1F939 1F3FB 200D 2640 ; minimally-qualified # 🤹🏻‍♀ E4.0 woman juggling: light skin tone -#1F939 1F3FC 200D 2640 FE0F ; fully-qualified # 🤹🏼‍♀️ E4.0 woman juggling: medium-light skin tone -#1F939 1F3FC 200D 2640 ; minimally-qualified # 🤹🏼‍♀ E4.0 woman juggling: medium-light skin tone -#1F939 1F3FD 200D 2640 FE0F ; fully-qualified # 🤹🏽‍♀️ E4.0 woman juggling: medium skin tone -#1F939 1F3FD 200D 2640 ; minimally-qualified # 🤹🏽‍♀ E4.0 woman juggling: medium skin tone -#1F939 1F3FE 200D 2640 FE0F ; fully-qualified # 🤹🏾‍♀️ E4.0 woman juggling: medium-dark skin tone -#1F939 1F3FE 200D 2640 ; minimally-qualified # 🤹🏾‍♀ E4.0 woman juggling: medium-dark skin tone -#1F939 1F3FF 200D 2640 FE0F ; fully-qualified # 🤹🏿‍♀️ E4.0 woman juggling: dark skin tone -#1F939 1F3FF 200D 2640 ; minimally-qualified # 🤹🏿‍♀ E4.0 woman juggling: dark skin tone -# -## subgroup: person-resting -#1F9D8 ; fully-qualified # 🧘 E5.0 person in lotus position -#1F9D8 1F3FB ; fully-qualified # 🧘🏻 E5.0 person in lotus position: light skin tone -#1F9D8 1F3FC ; fully-qualified # 🧘🏼 E5.0 person in lotus position: medium-light skin tone -#1F9D8 1F3FD ; fully-qualified # 🧘🏽 E5.0 person in lotus position: medium skin tone -#1F9D8 1F3FE ; fully-qualified # 🧘🏾 E5.0 person in lotus position: medium-dark skin tone -#1F9D8 1F3FF ; fully-qualified # 🧘🏿 E5.0 person in lotus position: dark skin tone -#1F9D8 200D 2642 FE0F ; fully-qualified # 🧘‍♂️ E5.0 man in lotus position -#1F9D8 200D 2642 ; minimally-qualified # 🧘‍♂ E5.0 man in lotus position -#1F9D8 1F3FB 200D 2642 FE0F ; fully-qualified # 🧘🏻‍♂️ E5.0 man in lotus position: light skin tone -#1F9D8 1F3FB 200D 2642 ; minimally-qualified # 🧘🏻‍♂ E5.0 man in lotus position: light skin tone -#1F9D8 1F3FC 200D 2642 FE0F ; fully-qualified # 🧘🏼‍♂️ E5.0 man in lotus position: medium-light skin tone -#1F9D8 1F3FC 200D 2642 ; minimally-qualified # 🧘🏼‍♂ E5.0 man in lotus position: medium-light skin tone -#1F9D8 1F3FD 200D 2642 FE0F ; fully-qualified # 🧘🏽‍♂️ E5.0 man in lotus position: medium skin tone -#1F9D8 1F3FD 200D 2642 ; minimally-qualified # 🧘🏽‍♂ E5.0 man in lotus position: medium skin tone -#1F9D8 1F3FE 200D 2642 FE0F ; fully-qualified # 🧘🏾‍♂️ E5.0 man in lotus position: medium-dark skin tone -#1F9D8 1F3FE 200D 2642 ; minimally-qualified # 🧘🏾‍♂ E5.0 man in lotus position: medium-dark skin tone -#1F9D8 1F3FF 200D 2642 FE0F ; fully-qualified # 🧘🏿‍♂️ E5.0 man in lotus position: dark skin tone -#1F9D8 1F3FF 200D 2642 ; minimally-qualified # 🧘🏿‍♂ E5.0 man in lotus position: dark skin tone -#1F9D8 200D 2640 FE0F ; fully-qualified # 🧘‍♀️ E5.0 woman in lotus position -#1F9D8 200D 2640 ; minimally-qualified # 🧘‍♀ E5.0 woman in lotus position -#1F9D8 1F3FB 200D 2640 FE0F ; fully-qualified # 🧘🏻‍♀️ E5.0 woman in lotus position: light skin tone -#1F9D8 1F3FB 200D 2640 ; minimally-qualified # 🧘🏻‍♀ E5.0 woman in lotus position: light skin tone -#1F9D8 1F3FC 200D 2640 FE0F ; fully-qualified # 🧘🏼‍♀️ E5.0 woman in lotus position: medium-light skin tone -#1F9D8 1F3FC 200D 2640 ; minimally-qualified # 🧘🏼‍♀ E5.0 woman in lotus position: medium-light skin tone -#1F9D8 1F3FD 200D 2640 FE0F ; fully-qualified # 🧘🏽‍♀️ E5.0 woman in lotus position: medium skin tone -#1F9D8 1F3FD 200D 2640 ; minimally-qualified # 🧘🏽‍♀ E5.0 woman in lotus position: medium skin tone -#1F9D8 1F3FE 200D 2640 FE0F ; fully-qualified # 🧘🏾‍♀️ E5.0 woman in lotus position: medium-dark skin tone -#1F9D8 1F3FE 200D 2640 ; minimally-qualified # 🧘🏾‍♀ E5.0 woman in lotus position: medium-dark skin tone -#1F9D8 1F3FF 200D 2640 FE0F ; fully-qualified # 🧘🏿‍♀️ E5.0 woman in lotus position: dark skin tone -#1F9D8 1F3FF 200D 2640 ; minimally-qualified # 🧘🏿‍♀ E5.0 woman in lotus position: dark skin tone -#1F6C0 ; fully-qualified # 🛀 E0.6 person taking bath -#1F6C0 1F3FB ; fully-qualified # 🛀🏻 E1.0 person taking bath: light skin tone -#1F6C0 1F3FC ; fully-qualified # 🛀🏼 E1.0 person taking bath: medium-light skin tone -#1F6C0 1F3FD ; fully-qualified # 🛀🏽 E1.0 person taking bath: medium skin tone -#1F6C0 1F3FE ; fully-qualified # 🛀🏾 E1.0 person taking bath: medium-dark skin tone -#1F6C0 1F3FF ; fully-qualified # 🛀🏿 E1.0 person taking bath: dark skin tone -#1F6CC ; fully-qualified # 🛌 E1.0 person in bed -#1F6CC 1F3FB ; fully-qualified # 🛌🏻 E4.0 person in bed: light skin tone -#1F6CC 1F3FC ; fully-qualified # 🛌🏼 E4.0 person in bed: medium-light skin tone -#1F6CC 1F3FD ; fully-qualified # 🛌🏽 E4.0 person in bed: medium skin tone -#1F6CC 1F3FE ; fully-qualified # 🛌🏾 E4.0 person in bed: medium-dark skin tone -#1F6CC 1F3FF ; fully-qualified # 🛌🏿 E4.0 person in bed: dark skin tone -# -## subgroup: family -#1F9D1 200D 1F91D 200D 1F9D1 ; fully-qualified # 🧑‍🤝‍🧑 E12.0 people holding hands -#1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏻‍🤝‍🧑🏻 E12.0 people holding hands: light skin tone -#1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏻‍🤝‍🧑🏼 E12.1 people holding hands: light skin tone, medium-light skin tone -#1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏻‍🤝‍🧑🏽 E12.1 people holding hands: light skin tone, medium skin tone -#1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏻‍🤝‍🧑🏾 E12.1 people holding hands: light skin tone, medium-dark skin tone -#1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏻‍🤝‍🧑🏿 E12.1 people holding hands: light skin tone, dark skin tone -#1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏼‍🤝‍🧑🏻 E12.0 people holding hands: medium-light skin tone, light skin tone -#1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏼‍🤝‍🧑🏼 E12.0 people holding hands: medium-light skin tone -#1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏼‍🤝‍🧑🏽 E12.1 people holding hands: medium-light skin tone, medium skin tone -#1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏼‍🤝‍🧑🏾 E12.1 people holding hands: medium-light skin tone, medium-dark skin tone -#1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏼‍🤝‍🧑🏿 E12.1 people holding hands: medium-light skin tone, dark skin tone -#1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏽‍🤝‍🧑🏻 E12.0 people holding hands: medium skin tone, light skin tone -#1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏽‍🤝‍🧑🏼 E12.0 people holding hands: medium skin tone, medium-light skin tone -#1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏽‍🤝‍🧑🏽 E12.0 people holding hands: medium skin tone -#1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏽‍🤝‍🧑🏾 E12.1 people holding hands: medium skin tone, medium-dark skin tone -#1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏽‍🤝‍🧑🏿 E12.1 people holding hands: medium skin tone, dark skin tone -#1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏾‍🤝‍🧑🏻 E12.0 people holding hands: medium-dark skin tone, light skin tone -#1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏾‍🤝‍🧑🏼 E12.0 people holding hands: medium-dark skin tone, medium-light skin tone -#1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏾‍🤝‍🧑🏽 E12.0 people holding hands: medium-dark skin tone, medium skin tone -#1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏾‍🤝‍🧑🏾 E12.0 people holding hands: medium-dark skin tone -#1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏾‍🤝‍🧑🏿 E12.1 people holding hands: medium-dark skin tone, dark skin tone -#1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏿‍🤝‍🧑🏻 E12.0 people holding hands: dark skin tone, light skin tone -#1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏿‍🤝‍🧑🏼 E12.0 people holding hands: dark skin tone, medium-light skin tone -#1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏿‍🤝‍🧑🏽 E12.0 people holding hands: dark skin tone, medium skin tone -#1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏿‍🤝‍🧑🏾 E12.0 people holding hands: dark skin tone, medium-dark skin tone -#1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏿‍🤝‍🧑🏿 E12.0 people holding hands: dark skin tone -#1F46D ; fully-qualified # 👭 E1.0 women holding hands -#1F46D 1F3FB ; fully-qualified # 👭🏻 E12.0 women holding hands: light skin tone -#1F469 1F3FB 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏻‍🤝‍👩🏼 E12.1 women holding hands: light skin tone, medium-light skin tone -#1F469 1F3FB 200D 1F91D 200D 1F469 1F3FD ; fully-qualified # 👩🏻‍🤝‍👩🏽 E12.1 women holding hands: light skin tone, medium skin tone -#1F469 1F3FB 200D 1F91D 200D 1F469 1F3FE ; fully-qualified # 👩🏻‍🤝‍👩🏾 E12.1 women holding hands: light skin tone, medium-dark skin tone -#1F469 1F3FB 200D 1F91D 200D 1F469 1F3FF ; fully-qualified # 👩🏻‍🤝‍👩🏿 E12.1 women holding hands: light skin tone, dark skin tone -#1F469 1F3FC 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏼‍🤝‍👩🏻 E12.0 women holding hands: medium-light skin tone, light skin tone -#1F46D 1F3FC ; fully-qualified # 👭🏼 E12.0 women holding hands: medium-light skin tone -#1F469 1F3FC 200D 1F91D 200D 1F469 1F3FD ; fully-qualified # 👩🏼‍🤝‍👩🏽 E12.1 women holding hands: medium-light skin tone, medium skin tone -#1F469 1F3FC 200D 1F91D 200D 1F469 1F3FE ; fully-qualified # 👩🏼‍🤝‍👩🏾 E12.1 women holding hands: medium-light skin tone, medium-dark skin tone -#1F469 1F3FC 200D 1F91D 200D 1F469 1F3FF ; fully-qualified # 👩🏼‍🤝‍👩🏿 E12.1 women holding hands: medium-light skin tone, dark skin tone -#1F469 1F3FD 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏽‍🤝‍👩🏻 E12.0 women holding hands: medium skin tone, light skin tone -#1F469 1F3FD 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏽‍🤝‍👩🏼 E12.0 women holding hands: medium skin tone, medium-light skin tone -#1F46D 1F3FD ; fully-qualified # 👭🏽 E12.0 women holding hands: medium skin tone -#1F469 1F3FD 200D 1F91D 200D 1F469 1F3FE ; fully-qualified # 👩🏽‍🤝‍👩🏾 E12.1 women holding hands: medium skin tone, medium-dark skin tone -#1F469 1F3FD 200D 1F91D 200D 1F469 1F3FF ; fully-qualified # 👩🏽‍🤝‍👩🏿 E12.1 women holding hands: medium skin tone, dark skin tone -#1F469 1F3FE 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏾‍🤝‍👩🏻 E12.0 women holding hands: medium-dark skin tone, light skin tone -#1F469 1F3FE 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏾‍🤝‍👩🏼 E12.0 women holding hands: medium-dark skin tone, medium-light skin tone -#1F469 1F3FE 200D 1F91D 200D 1F469 1F3FD ; fully-qualified # 👩🏾‍🤝‍👩🏽 E12.0 women holding hands: medium-dark skin tone, medium skin tone -#1F46D 1F3FE ; fully-qualified # 👭🏾 E12.0 women holding hands: medium-dark skin tone -#1F469 1F3FE 200D 1F91D 200D 1F469 1F3FF ; fully-qualified # 👩🏾‍🤝‍👩🏿 E12.1 women holding hands: medium-dark skin tone, dark skin tone -#1F469 1F3FF 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏿‍🤝‍👩🏻 E12.0 women holding hands: dark skin tone, light skin tone -#1F469 1F3FF 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏿‍🤝‍👩🏼 E12.0 women holding hands: dark skin tone, medium-light skin tone -#1F469 1F3FF 200D 1F91D 200D 1F469 1F3FD ; fully-qualified # 👩🏿‍🤝‍👩🏽 E12.0 women holding hands: dark skin tone, medium skin tone -#1F469 1F3FF 200D 1F91D 200D 1F469 1F3FE ; fully-qualified # 👩🏿‍🤝‍👩🏾 E12.0 women holding hands: dark skin tone, medium-dark skin tone -#1F46D 1F3FF ; fully-qualified # 👭🏿 E12.0 women holding hands: dark skin tone -#1F46B ; fully-qualified # 👫 E0.6 woman and man holding hands -#1F46B 1F3FB ; fully-qualified # 👫🏻 E12.0 woman and man holding hands: light skin tone -#1F469 1F3FB 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏻‍🤝‍👨🏼 E12.0 woman and man holding hands: light skin tone, medium-light skin tone -#1F469 1F3FB 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏻‍🤝‍👨🏽 E12.0 woman and man holding hands: light skin tone, medium skin tone -#1F469 1F3FB 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏻‍🤝‍👨🏾 E12.0 woman and man holding hands: light skin tone, medium-dark skin tone -#1F469 1F3FB 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏻‍🤝‍👨🏿 E12.0 woman and man holding hands: light skin tone, dark skin tone -#1F469 1F3FC 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏼‍🤝‍👨🏻 E12.0 woman and man holding hands: medium-light skin tone, light skin tone -#1F46B 1F3FC ; fully-qualified # 👫🏼 E12.0 woman and man holding hands: medium-light skin tone -#1F469 1F3FC 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏼‍🤝‍👨🏽 E12.0 woman and man holding hands: medium-light skin tone, medium skin tone -#1F469 1F3FC 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏼‍🤝‍👨🏾 E12.0 woman and man holding hands: medium-light skin tone, medium-dark skin tone -#1F469 1F3FC 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏼‍🤝‍👨🏿 E12.0 woman and man holding hands: medium-light skin tone, dark skin tone -#1F469 1F3FD 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏽‍🤝‍👨🏻 E12.0 woman and man holding hands: medium skin tone, light skin tone -#1F469 1F3FD 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏽‍🤝‍👨🏼 E12.0 woman and man holding hands: medium skin tone, medium-light skin tone -#1F46B 1F3FD ; fully-qualified # 👫🏽 E12.0 woman and man holding hands: medium skin tone -#1F469 1F3FD 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏽‍🤝‍👨🏾 E12.0 woman and man holding hands: medium skin tone, medium-dark skin tone -#1F469 1F3FD 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏽‍🤝‍👨🏿 E12.0 woman and man holding hands: medium skin tone, dark skin tone -#1F469 1F3FE 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏾‍🤝‍👨🏻 E12.0 woman and man holding hands: medium-dark skin tone, light skin tone -#1F469 1F3FE 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏾‍🤝‍👨🏼 E12.0 woman and man holding hands: medium-dark skin tone, medium-light skin tone -#1F469 1F3FE 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏾‍🤝‍👨🏽 E12.0 woman and man holding hands: medium-dark skin tone, medium skin tone -#1F46B 1F3FE ; fully-qualified # 👫🏾 E12.0 woman and man holding hands: medium-dark skin tone -#1F469 1F3FE 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏾‍🤝‍👨🏿 E12.0 woman and man holding hands: medium-dark skin tone, dark skin tone -#1F469 1F3FF 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏿‍🤝‍👨🏻 E12.0 woman and man holding hands: dark skin tone, light skin tone -#1F469 1F3FF 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏿‍🤝‍👨🏼 E12.0 woman and man holding hands: dark skin tone, medium-light skin tone -#1F469 1F3FF 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏿‍🤝‍👨🏽 E12.0 woman and man holding hands: dark skin tone, medium skin tone -#1F469 1F3FF 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏿‍🤝‍👨🏾 E12.0 woman and man holding hands: dark skin tone, medium-dark skin tone -#1F46B 1F3FF ; fully-qualified # 👫🏿 E12.0 woman and man holding hands: dark skin tone -#1F46C ; fully-qualified # 👬 E1.0 men holding hands -#1F46C 1F3FB ; fully-qualified # 👬🏻 E12.0 men holding hands: light skin tone -#1F468 1F3FB 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏻‍🤝‍👨🏼 E12.1 men holding hands: light skin tone, medium-light skin tone -#1F468 1F3FB 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👨🏻‍🤝‍👨🏽 E12.1 men holding hands: light skin tone, medium skin tone -#1F468 1F3FB 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👨🏻‍🤝‍👨🏾 E12.1 men holding hands: light skin tone, medium-dark skin tone -#1F468 1F3FB 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👨🏻‍🤝‍👨🏿 E12.1 men holding hands: light skin tone, dark skin tone -#1F468 1F3FC 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏼‍🤝‍👨🏻 E12.0 men holding hands: medium-light skin tone, light skin tone -#1F46C 1F3FC ; fully-qualified # 👬🏼 E12.0 men holding hands: medium-light skin tone -#1F468 1F3FC 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👨🏼‍🤝‍👨🏽 E12.1 men holding hands: medium-light skin tone, medium skin tone -#1F468 1F3FC 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👨🏼‍🤝‍👨🏾 E12.1 men holding hands: medium-light skin tone, medium-dark skin tone -#1F468 1F3FC 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👨🏼‍🤝‍👨🏿 E12.1 men holding hands: medium-light skin tone, dark skin tone -#1F468 1F3FD 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏽‍🤝‍👨🏻 E12.0 men holding hands: medium skin tone, light skin tone -#1F468 1F3FD 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏽‍🤝‍👨🏼 E12.0 men holding hands: medium skin tone, medium-light skin tone -#1F46C 1F3FD ; fully-qualified # 👬🏽 E12.0 men holding hands: medium skin tone -#1F468 1F3FD 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👨🏽‍🤝‍👨🏾 E12.1 men holding hands: medium skin tone, medium-dark skin tone -#1F468 1F3FD 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👨🏽‍🤝‍👨🏿 E12.1 men holding hands: medium skin tone, dark skin tone -#1F468 1F3FE 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏾‍🤝‍👨🏻 E12.0 men holding hands: medium-dark skin tone, light skin tone -#1F468 1F3FE 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏾‍🤝‍👨🏼 E12.0 men holding hands: medium-dark skin tone, medium-light skin tone -#1F468 1F3FE 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👨🏾‍🤝‍👨🏽 E12.0 men holding hands: medium-dark skin tone, medium skin tone -#1F46C 1F3FE ; fully-qualified # 👬🏾 E12.0 men holding hands: medium-dark skin tone -#1F468 1F3FE 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👨🏾‍🤝‍👨🏿 E12.1 men holding hands: medium-dark skin tone, dark skin tone -#1F468 1F3FF 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏿‍🤝‍👨🏻 E12.0 men holding hands: dark skin tone, light skin tone -#1F468 1F3FF 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏿‍🤝‍👨🏼 E12.0 men holding hands: dark skin tone, medium-light skin tone -#1F468 1F3FF 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👨🏿‍🤝‍👨🏽 E12.0 men holding hands: dark skin tone, medium skin tone -#1F468 1F3FF 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👨🏿‍🤝‍👨🏾 E12.0 men holding hands: dark skin tone, medium-dark skin tone -#1F46C 1F3FF ; fully-qualified # 👬🏿 E12.0 men holding hands: dark skin tone -#1F48F ; fully-qualified # 💏 E0.6 kiss -#1F48F 1F3FB ; fully-qualified # 💏🏻 E13.1 kiss: light skin tone -#1F48F 1F3FC ; fully-qualified # 💏🏼 E13.1 kiss: medium-light skin tone -#1F48F 1F3FD ; fully-qualified # 💏🏽 E13.1 kiss: medium skin tone -#1F48F 1F3FE ; fully-qualified # 💏🏾 E13.1 kiss: medium-dark skin tone -#1F48F 1F3FF ; fully-qualified # 💏🏿 E13.1 kiss: dark skin tone -#1F9D1 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏻‍❤️‍💋‍🧑🏼 E13.1 kiss: person, person, light skin tone, medium-light skin tone -#1F9D1 1F3FB 200D 2764 200D 1F48B 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏻‍❤‍💋‍🧑🏼 E13.1 kiss: person, person, light skin tone, medium-light skin tone -#1F9D1 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏻‍❤️‍💋‍🧑🏽 E13.1 kiss: person, person, light skin tone, medium skin tone -#1F9D1 1F3FB 200D 2764 200D 1F48B 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏻‍❤‍💋‍🧑🏽 E13.1 kiss: person, person, light skin tone, medium skin tone -#1F9D1 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏻‍❤️‍💋‍🧑🏾 E13.1 kiss: person, person, light skin tone, medium-dark skin tone -#1F9D1 1F3FB 200D 2764 200D 1F48B 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏻‍❤‍💋‍🧑🏾 E13.1 kiss: person, person, light skin tone, medium-dark skin tone -#1F9D1 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏻‍❤️‍💋‍🧑🏿 E13.1 kiss: person, person, light skin tone, dark skin tone -#1F9D1 1F3FB 200D 2764 200D 1F48B 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏻‍❤‍💋‍🧑🏿 E13.1 kiss: person, person, light skin tone, dark skin tone -#1F9D1 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏼‍❤️‍💋‍🧑🏻 E13.1 kiss: person, person, medium-light skin tone, light skin tone -#1F9D1 1F3FC 200D 2764 200D 1F48B 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏼‍❤‍💋‍🧑🏻 E13.1 kiss: person, person, medium-light skin tone, light skin tone -#1F9D1 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏼‍❤️‍💋‍🧑🏽 E13.1 kiss: person, person, medium-light skin tone, medium skin tone -#1F9D1 1F3FC 200D 2764 200D 1F48B 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏼‍❤‍💋‍🧑🏽 E13.1 kiss: person, person, medium-light skin tone, medium skin tone -#1F9D1 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏼‍❤️‍💋‍🧑🏾 E13.1 kiss: person, person, medium-light skin tone, medium-dark skin tone -#1F9D1 1F3FC 200D 2764 200D 1F48B 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏼‍❤‍💋‍🧑🏾 E13.1 kiss: person, person, medium-light skin tone, medium-dark skin tone -#1F9D1 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏼‍❤️‍💋‍🧑🏿 E13.1 kiss: person, person, medium-light skin tone, dark skin tone -#1F9D1 1F3FC 200D 2764 200D 1F48B 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏼‍❤‍💋‍🧑🏿 E13.1 kiss: person, person, medium-light skin tone, dark skin tone -#1F9D1 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏽‍❤️‍💋‍🧑🏻 E13.1 kiss: person, person, medium skin tone, light skin tone -#1F9D1 1F3FD 200D 2764 200D 1F48B 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏽‍❤‍💋‍🧑🏻 E13.1 kiss: person, person, medium skin tone, light skin tone -#1F9D1 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏽‍❤️‍💋‍🧑🏼 E13.1 kiss: person, person, medium skin tone, medium-light skin tone -#1F9D1 1F3FD 200D 2764 200D 1F48B 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏽‍❤‍💋‍🧑🏼 E13.1 kiss: person, person, medium skin tone, medium-light skin tone -#1F9D1 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏽‍❤️‍💋‍🧑🏾 E13.1 kiss: person, person, medium skin tone, medium-dark skin tone -#1F9D1 1F3FD 200D 2764 200D 1F48B 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏽‍❤‍💋‍🧑🏾 E13.1 kiss: person, person, medium skin tone, medium-dark skin tone -#1F9D1 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏽‍❤️‍💋‍🧑🏿 E13.1 kiss: person, person, medium skin tone, dark skin tone -#1F9D1 1F3FD 200D 2764 200D 1F48B 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏽‍❤‍💋‍🧑🏿 E13.1 kiss: person, person, medium skin tone, dark skin tone -#1F9D1 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏾‍❤️‍💋‍🧑🏻 E13.1 kiss: person, person, medium-dark skin tone, light skin tone -#1F9D1 1F3FE 200D 2764 200D 1F48B 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏾‍❤‍💋‍🧑🏻 E13.1 kiss: person, person, medium-dark skin tone, light skin tone -#1F9D1 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏾‍❤️‍💋‍🧑🏼 E13.1 kiss: person, person, medium-dark skin tone, medium-light skin tone -#1F9D1 1F3FE 200D 2764 200D 1F48B 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏾‍❤‍💋‍🧑🏼 E13.1 kiss: person, person, medium-dark skin tone, medium-light skin tone -#1F9D1 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏾‍❤️‍💋‍🧑🏽 E13.1 kiss: person, person, medium-dark skin tone, medium skin tone -#1F9D1 1F3FE 200D 2764 200D 1F48B 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏾‍❤‍💋‍🧑🏽 E13.1 kiss: person, person, medium-dark skin tone, medium skin tone -#1F9D1 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏾‍❤️‍💋‍🧑🏿 E13.1 kiss: person, person, medium-dark skin tone, dark skin tone -#1F9D1 1F3FE 200D 2764 200D 1F48B 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏾‍❤‍💋‍🧑🏿 E13.1 kiss: person, person, medium-dark skin tone, dark skin tone -#1F9D1 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏿‍❤️‍💋‍🧑🏻 E13.1 kiss: person, person, dark skin tone, light skin tone -#1F9D1 1F3FF 200D 2764 200D 1F48B 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏿‍❤‍💋‍🧑🏻 E13.1 kiss: person, person, dark skin tone, light skin tone -#1F9D1 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏿‍❤️‍💋‍🧑🏼 E13.1 kiss: person, person, dark skin tone, medium-light skin tone -#1F9D1 1F3FF 200D 2764 200D 1F48B 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏿‍❤‍💋‍🧑🏼 E13.1 kiss: person, person, dark skin tone, medium-light skin tone -#1F9D1 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏿‍❤️‍💋‍🧑🏽 E13.1 kiss: person, person, dark skin tone, medium skin tone -#1F9D1 1F3FF 200D 2764 200D 1F48B 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏿‍❤‍💋‍🧑🏽 E13.1 kiss: person, person, dark skin tone, medium skin tone -#1F9D1 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏿‍❤️‍💋‍🧑🏾 E13.1 kiss: person, person, dark skin tone, medium-dark skin tone -#1F9D1 1F3FF 200D 2764 200D 1F48B 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏿‍❤‍💋‍🧑🏾 E13.1 kiss: person, person, dark skin tone, medium-dark skin tone -#1F469 200D 2764 FE0F 200D 1F48B 200D 1F468 ; fully-qualified # 👩‍❤️‍💋‍👨 E2.0 kiss: woman, man -#1F469 200D 2764 200D 1F48B 200D 1F468 ; minimally-qualified # 👩‍❤‍💋‍👨 E2.0 kiss: woman, man -#1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👩🏻‍❤️‍💋‍👨🏻 E13.1 kiss: woman, man, light skin tone -#1F469 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👩🏻‍❤‍💋‍👨🏻 E13.1 kiss: woman, man, light skin tone -#1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👩🏻‍❤️‍💋‍👨🏼 E13.1 kiss: woman, man, light skin tone, medium-light skin tone -#1F469 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👩🏻‍❤‍💋‍👨🏼 E13.1 kiss: woman, man, light skin tone, medium-light skin tone -#1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👩🏻‍❤️‍💋‍👨🏽 E13.1 kiss: woman, man, light skin tone, medium skin tone -#1F469 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👩🏻‍❤‍💋‍👨🏽 E13.1 kiss: woman, man, light skin tone, medium skin tone -#1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👩🏻‍❤️‍💋‍👨🏾 E13.1 kiss: woman, man, light skin tone, medium-dark skin tone -#1F469 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👩🏻‍❤‍💋‍👨🏾 E13.1 kiss: woman, man, light skin tone, medium-dark skin tone -#1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👩🏻‍❤️‍💋‍👨🏿 E13.1 kiss: woman, man, light skin tone, dark skin tone -#1F469 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👩🏻‍❤‍💋‍👨🏿 E13.1 kiss: woman, man, light skin tone, dark skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👩🏼‍❤️‍💋‍👨🏻 E13.1 kiss: woman, man, medium-light skin tone, light skin tone -#1F469 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👩🏼‍❤‍💋‍👨🏻 E13.1 kiss: woman, man, medium-light skin tone, light skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👩🏼‍❤️‍💋‍👨🏼 E13.1 kiss: woman, man, medium-light skin tone -#1F469 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👩🏼‍❤‍💋‍👨🏼 E13.1 kiss: woman, man, medium-light skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👩🏼‍❤️‍💋‍👨🏽 E13.1 kiss: woman, man, medium-light skin tone, medium skin tone -#1F469 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👩🏼‍❤‍💋‍👨🏽 E13.1 kiss: woman, man, medium-light skin tone, medium skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👩🏼‍❤️‍💋‍👨🏾 E13.1 kiss: woman, man, medium-light skin tone, medium-dark skin tone -#1F469 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👩🏼‍❤‍💋‍👨🏾 E13.1 kiss: woman, man, medium-light skin tone, medium-dark skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👩🏼‍❤️‍💋‍👨🏿 E13.1 kiss: woman, man, medium-light skin tone, dark skin tone -#1F469 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👩🏼‍❤‍💋‍👨🏿 E13.1 kiss: woman, man, medium-light skin tone, dark skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👩🏽‍❤️‍💋‍👨🏻 E13.1 kiss: woman, man, medium skin tone, light skin tone -#1F469 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👩🏽‍❤‍💋‍👨🏻 E13.1 kiss: woman, man, medium skin tone, light skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👩🏽‍❤️‍💋‍👨🏼 E13.1 kiss: woman, man, medium skin tone, medium-light skin tone -#1F469 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👩🏽‍❤‍💋‍👨🏼 E13.1 kiss: woman, man, medium skin tone, medium-light skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👩🏽‍❤️‍💋‍👨🏽 E13.1 kiss: woman, man, medium skin tone -#1F469 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👩🏽‍❤‍💋‍👨🏽 E13.1 kiss: woman, man, medium skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👩🏽‍❤️‍💋‍👨🏾 E13.1 kiss: woman, man, medium skin tone, medium-dark skin tone -#1F469 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👩🏽‍❤‍💋‍👨🏾 E13.1 kiss: woman, man, medium skin tone, medium-dark skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👩🏽‍❤️‍💋‍👨🏿 E13.1 kiss: woman, man, medium skin tone, dark skin tone -#1F469 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👩🏽‍❤‍💋‍👨🏿 E13.1 kiss: woman, man, medium skin tone, dark skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👩🏾‍❤️‍💋‍👨🏻 E13.1 kiss: woman, man, medium-dark skin tone, light skin tone -#1F469 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👩🏾‍❤‍💋‍👨🏻 E13.1 kiss: woman, man, medium-dark skin tone, light skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👩🏾‍❤️‍💋‍👨🏼 E13.1 kiss: woman, man, medium-dark skin tone, medium-light skin tone -#1F469 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👩🏾‍❤‍💋‍👨🏼 E13.1 kiss: woman, man, medium-dark skin tone, medium-light skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👩🏾‍❤️‍💋‍👨🏽 E13.1 kiss: woman, man, medium-dark skin tone, medium skin tone -#1F469 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👩🏾‍❤‍💋‍👨🏽 E13.1 kiss: woman, man, medium-dark skin tone, medium skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👩🏾‍❤️‍💋‍👨🏾 E13.1 kiss: woman, man, medium-dark skin tone -#1F469 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👩🏾‍❤‍💋‍👨🏾 E13.1 kiss: woman, man, medium-dark skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👩🏾‍❤️‍💋‍👨🏿 E13.1 kiss: woman, man, medium-dark skin tone, dark skin tone -#1F469 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👩🏾‍❤‍💋‍👨🏿 E13.1 kiss: woman, man, medium-dark skin tone, dark skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👩🏿‍❤️‍💋‍👨🏻 E13.1 kiss: woman, man, dark skin tone, light skin tone -#1F469 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👩🏿‍❤‍💋‍👨🏻 E13.1 kiss: woman, man, dark skin tone, light skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👩🏿‍❤️‍💋‍👨🏼 E13.1 kiss: woman, man, dark skin tone, medium-light skin tone -#1F469 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👩🏿‍❤‍💋‍👨🏼 E13.1 kiss: woman, man, dark skin tone, medium-light skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👩🏿‍❤️‍💋‍👨🏽 E13.1 kiss: woman, man, dark skin tone, medium skin tone -#1F469 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👩🏿‍❤‍💋‍👨🏽 E13.1 kiss: woman, man, dark skin tone, medium skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👩🏿‍❤️‍💋‍👨🏾 E13.1 kiss: woman, man, dark skin tone, medium-dark skin tone -#1F469 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👩🏿‍❤‍💋‍👨🏾 E13.1 kiss: woman, man, dark skin tone, medium-dark skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👩🏿‍❤️‍💋‍👨🏿 E13.1 kiss: woman, man, dark skin tone -#1F469 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👩🏿‍❤‍💋‍👨🏿 E13.1 kiss: woman, man, dark skin tone -#1F468 200D 2764 FE0F 200D 1F48B 200D 1F468 ; fully-qualified # 👨‍❤️‍💋‍👨 E2.0 kiss: man, man -#1F468 200D 2764 200D 1F48B 200D 1F468 ; minimally-qualified # 👨‍❤‍💋‍👨 E2.0 kiss: man, man -#1F468 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👨🏻‍❤️‍💋‍👨🏻 E13.1 kiss: man, man, light skin tone -#1F468 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👨🏻‍❤‍💋‍👨🏻 E13.1 kiss: man, man, light skin tone -#1F468 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👨🏻‍❤️‍💋‍👨🏼 E13.1 kiss: man, man, light skin tone, medium-light skin tone -#1F468 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👨🏻‍❤‍💋‍👨🏼 E13.1 kiss: man, man, light skin tone, medium-light skin tone -#1F468 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👨🏻‍❤️‍💋‍👨🏽 E13.1 kiss: man, man, light skin tone, medium skin tone -#1F468 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👨🏻‍❤‍💋‍👨🏽 E13.1 kiss: man, man, light skin tone, medium skin tone -#1F468 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👨🏻‍❤️‍💋‍👨🏾 E13.1 kiss: man, man, light skin tone, medium-dark skin tone -#1F468 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👨🏻‍❤‍💋‍👨🏾 E13.1 kiss: man, man, light skin tone, medium-dark skin tone -#1F468 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👨🏻‍❤️‍💋‍👨🏿 E13.1 kiss: man, man, light skin tone, dark skin tone -#1F468 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👨🏻‍❤‍💋‍👨🏿 E13.1 kiss: man, man, light skin tone, dark skin tone -#1F468 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👨🏼‍❤️‍💋‍👨🏻 E13.1 kiss: man, man, medium-light skin tone, light skin tone -#1F468 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👨🏼‍❤‍💋‍👨🏻 E13.1 kiss: man, man, medium-light skin tone, light skin tone -#1F468 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👨🏼‍❤️‍💋‍👨🏼 E13.1 kiss: man, man, medium-light skin tone -#1F468 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👨🏼‍❤‍💋‍👨🏼 E13.1 kiss: man, man, medium-light skin tone -#1F468 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👨🏼‍❤️‍💋‍👨🏽 E13.1 kiss: man, man, medium-light skin tone, medium skin tone -#1F468 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👨🏼‍❤‍💋‍👨🏽 E13.1 kiss: man, man, medium-light skin tone, medium skin tone -#1F468 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👨🏼‍❤️‍💋‍👨🏾 E13.1 kiss: man, man, medium-light skin tone, medium-dark skin tone -#1F468 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👨🏼‍❤‍💋‍👨🏾 E13.1 kiss: man, man, medium-light skin tone, medium-dark skin tone -#1F468 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👨🏼‍❤️‍💋‍👨🏿 E13.1 kiss: man, man, medium-light skin tone, dark skin tone -#1F468 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👨🏼‍❤‍💋‍👨🏿 E13.1 kiss: man, man, medium-light skin tone, dark skin tone -#1F468 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👨🏽‍❤️‍💋‍👨🏻 E13.1 kiss: man, man, medium skin tone, light skin tone -#1F468 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👨🏽‍❤‍💋‍👨🏻 E13.1 kiss: man, man, medium skin tone, light skin tone -#1F468 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👨🏽‍❤️‍💋‍👨🏼 E13.1 kiss: man, man, medium skin tone, medium-light skin tone -#1F468 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👨🏽‍❤‍💋‍👨🏼 E13.1 kiss: man, man, medium skin tone, medium-light skin tone -#1F468 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👨🏽‍❤️‍💋‍👨🏽 E13.1 kiss: man, man, medium skin tone -#1F468 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👨🏽‍❤‍💋‍👨🏽 E13.1 kiss: man, man, medium skin tone -#1F468 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👨🏽‍❤️‍💋‍👨🏾 E13.1 kiss: man, man, medium skin tone, medium-dark skin tone -#1F468 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👨🏽‍❤‍💋‍👨🏾 E13.1 kiss: man, man, medium skin tone, medium-dark skin tone -#1F468 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👨🏽‍❤️‍💋‍👨🏿 E13.1 kiss: man, man, medium skin tone, dark skin tone -#1F468 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👨🏽‍❤‍💋‍👨🏿 E13.1 kiss: man, man, medium skin tone, dark skin tone -#1F468 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👨🏾‍❤️‍💋‍👨🏻 E13.1 kiss: man, man, medium-dark skin tone, light skin tone -#1F468 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👨🏾‍❤‍💋‍👨🏻 E13.1 kiss: man, man, medium-dark skin tone, light skin tone -#1F468 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👨🏾‍❤️‍💋‍👨🏼 E13.1 kiss: man, man, medium-dark skin tone, medium-light skin tone -#1F468 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👨🏾‍❤‍💋‍👨🏼 E13.1 kiss: man, man, medium-dark skin tone, medium-light skin tone -#1F468 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👨🏾‍❤️‍💋‍👨🏽 E13.1 kiss: man, man, medium-dark skin tone, medium skin tone -#1F468 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👨🏾‍❤‍💋‍👨🏽 E13.1 kiss: man, man, medium-dark skin tone, medium skin tone -#1F468 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👨🏾‍❤️‍💋‍👨🏾 E13.1 kiss: man, man, medium-dark skin tone -#1F468 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👨🏾‍❤‍💋‍👨🏾 E13.1 kiss: man, man, medium-dark skin tone -#1F468 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👨🏾‍❤️‍💋‍👨🏿 E13.1 kiss: man, man, medium-dark skin tone, dark skin tone -#1F468 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👨🏾‍❤‍💋‍👨🏿 E13.1 kiss: man, man, medium-dark skin tone, dark skin tone -#1F468 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👨🏿‍❤️‍💋‍👨🏻 E13.1 kiss: man, man, dark skin tone, light skin tone -#1F468 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👨🏿‍❤‍💋‍👨🏻 E13.1 kiss: man, man, dark skin tone, light skin tone -#1F468 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👨🏿‍❤️‍💋‍👨🏼 E13.1 kiss: man, man, dark skin tone, medium-light skin tone -#1F468 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👨🏿‍❤‍💋‍👨🏼 E13.1 kiss: man, man, dark skin tone, medium-light skin tone -#1F468 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👨🏿‍❤️‍💋‍👨🏽 E13.1 kiss: man, man, dark skin tone, medium skin tone -#1F468 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👨🏿‍❤‍💋‍👨🏽 E13.1 kiss: man, man, dark skin tone, medium skin tone -#1F468 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👨🏿‍❤️‍💋‍👨🏾 E13.1 kiss: man, man, dark skin tone, medium-dark skin tone -#1F468 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👨🏿‍❤‍💋‍👨🏾 E13.1 kiss: man, man, dark skin tone, medium-dark skin tone -#1F468 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👨🏿‍❤️‍💋‍👨🏿 E13.1 kiss: man, man, dark skin tone -#1F468 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👨🏿‍❤‍💋‍👨🏿 E13.1 kiss: man, man, dark skin tone -#1F469 200D 2764 FE0F 200D 1F48B 200D 1F469 ; fully-qualified # 👩‍❤️‍💋‍👩 E2.0 kiss: woman, woman -#1F469 200D 2764 200D 1F48B 200D 1F469 ; minimally-qualified # 👩‍❤‍💋‍👩 E2.0 kiss: woman, woman -#1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FB ; fully-qualified # 👩🏻‍❤️‍💋‍👩🏻 E13.1 kiss: woman, woman, light skin tone -#1F469 1F3FB 200D 2764 200D 1F48B 200D 1F469 1F3FB ; minimally-qualified # 👩🏻‍❤‍💋‍👩🏻 E13.1 kiss: woman, woman, light skin tone -#1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FC ; fully-qualified # 👩🏻‍❤️‍💋‍👩🏼 E13.1 kiss: woman, woman, light skin tone, medium-light skin tone -#1F469 1F3FB 200D 2764 200D 1F48B 200D 1F469 1F3FC ; minimally-qualified # 👩🏻‍❤‍💋‍👩🏼 E13.1 kiss: woman, woman, light skin tone, medium-light skin tone -#1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FD ; fully-qualified # 👩🏻‍❤️‍💋‍👩🏽 E13.1 kiss: woman, woman, light skin tone, medium skin tone -#1F469 1F3FB 200D 2764 200D 1F48B 200D 1F469 1F3FD ; minimally-qualified # 👩🏻‍❤‍💋‍👩🏽 E13.1 kiss: woman, woman, light skin tone, medium skin tone -#1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FE ; fully-qualified # 👩🏻‍❤️‍💋‍👩🏾 E13.1 kiss: woman, woman, light skin tone, medium-dark skin tone -#1F469 1F3FB 200D 2764 200D 1F48B 200D 1F469 1F3FE ; minimally-qualified # 👩🏻‍❤‍💋‍👩🏾 E13.1 kiss: woman, woman, light skin tone, medium-dark skin tone -#1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FF ; fully-qualified # 👩🏻‍❤️‍💋‍👩🏿 E13.1 kiss: woman, woman, light skin tone, dark skin tone -#1F469 1F3FB 200D 2764 200D 1F48B 200D 1F469 1F3FF ; minimally-qualified # 👩🏻‍❤‍💋‍👩🏿 E13.1 kiss: woman, woman, light skin tone, dark skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FB ; fully-qualified # 👩🏼‍❤️‍💋‍👩🏻 E13.1 kiss: woman, woman, medium-light skin tone, light skin tone -#1F469 1F3FC 200D 2764 200D 1F48B 200D 1F469 1F3FB ; minimally-qualified # 👩🏼‍❤‍💋‍👩🏻 E13.1 kiss: woman, woman, medium-light skin tone, light skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FC ; fully-qualified # 👩🏼‍❤️‍💋‍👩🏼 E13.1 kiss: woman, woman, medium-light skin tone -#1F469 1F3FC 200D 2764 200D 1F48B 200D 1F469 1F3FC ; minimally-qualified # 👩🏼‍❤‍💋‍👩🏼 E13.1 kiss: woman, woman, medium-light skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FD ; fully-qualified # 👩🏼‍❤️‍💋‍👩🏽 E13.1 kiss: woman, woman, medium-light skin tone, medium skin tone -#1F469 1F3FC 200D 2764 200D 1F48B 200D 1F469 1F3FD ; minimally-qualified # 👩🏼‍❤‍💋‍👩🏽 E13.1 kiss: woman, woman, medium-light skin tone, medium skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FE ; fully-qualified # 👩🏼‍❤️‍💋‍👩🏾 E13.1 kiss: woman, woman, medium-light skin tone, medium-dark skin tone -#1F469 1F3FC 200D 2764 200D 1F48B 200D 1F469 1F3FE ; minimally-qualified # 👩🏼‍❤‍💋‍👩🏾 E13.1 kiss: woman, woman, medium-light skin tone, medium-dark skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FF ; fully-qualified # 👩🏼‍❤️‍💋‍👩🏿 E13.1 kiss: woman, woman, medium-light skin tone, dark skin tone -#1F469 1F3FC 200D 2764 200D 1F48B 200D 1F469 1F3FF ; minimally-qualified # 👩🏼‍❤‍💋‍👩🏿 E13.1 kiss: woman, woman, medium-light skin tone, dark skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FB ; fully-qualified # 👩🏽‍❤️‍💋‍👩🏻 E13.1 kiss: woman, woman, medium skin tone, light skin tone -#1F469 1F3FD 200D 2764 200D 1F48B 200D 1F469 1F3FB ; minimally-qualified # 👩🏽‍❤‍💋‍👩🏻 E13.1 kiss: woman, woman, medium skin tone, light skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FC ; fully-qualified # 👩🏽‍❤️‍💋‍👩🏼 E13.1 kiss: woman, woman, medium skin tone, medium-light skin tone -#1F469 1F3FD 200D 2764 200D 1F48B 200D 1F469 1F3FC ; minimally-qualified # 👩🏽‍❤‍💋‍👩🏼 E13.1 kiss: woman, woman, medium skin tone, medium-light skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FD ; fully-qualified # 👩🏽‍❤️‍💋‍👩🏽 E13.1 kiss: woman, woman, medium skin tone -#1F469 1F3FD 200D 2764 200D 1F48B 200D 1F469 1F3FD ; minimally-qualified # 👩🏽‍❤‍💋‍👩🏽 E13.1 kiss: woman, woman, medium skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FE ; fully-qualified # 👩🏽‍❤️‍💋‍👩🏾 E13.1 kiss: woman, woman, medium skin tone, medium-dark skin tone -#1F469 1F3FD 200D 2764 200D 1F48B 200D 1F469 1F3FE ; minimally-qualified # 👩🏽‍❤‍💋‍👩🏾 E13.1 kiss: woman, woman, medium skin tone, medium-dark skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FF ; fully-qualified # 👩🏽‍❤️‍💋‍👩🏿 E13.1 kiss: woman, woman, medium skin tone, dark skin tone -#1F469 1F3FD 200D 2764 200D 1F48B 200D 1F469 1F3FF ; minimally-qualified # 👩🏽‍❤‍💋‍👩🏿 E13.1 kiss: woman, woman, medium skin tone, dark skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FB ; fully-qualified # 👩🏾‍❤️‍💋‍👩🏻 E13.1 kiss: woman, woman, medium-dark skin tone, light skin tone -#1F469 1F3FE 200D 2764 200D 1F48B 200D 1F469 1F3FB ; minimally-qualified # 👩🏾‍❤‍💋‍👩🏻 E13.1 kiss: woman, woman, medium-dark skin tone, light skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FC ; fully-qualified # 👩🏾‍❤️‍💋‍👩🏼 E13.1 kiss: woman, woman, medium-dark skin tone, medium-light skin tone -#1F469 1F3FE 200D 2764 200D 1F48B 200D 1F469 1F3FC ; minimally-qualified # 👩🏾‍❤‍💋‍👩🏼 E13.1 kiss: woman, woman, medium-dark skin tone, medium-light skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FD ; fully-qualified # 👩🏾‍❤️‍💋‍👩🏽 E13.1 kiss: woman, woman, medium-dark skin tone, medium skin tone -#1F469 1F3FE 200D 2764 200D 1F48B 200D 1F469 1F3FD ; minimally-qualified # 👩🏾‍❤‍💋‍👩🏽 E13.1 kiss: woman, woman, medium-dark skin tone, medium skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FE ; fully-qualified # 👩🏾‍❤️‍💋‍👩🏾 E13.1 kiss: woman, woman, medium-dark skin tone -#1F469 1F3FE 200D 2764 200D 1F48B 200D 1F469 1F3FE ; minimally-qualified # 👩🏾‍❤‍💋‍👩🏾 E13.1 kiss: woman, woman, medium-dark skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FF ; fully-qualified # 👩🏾‍❤️‍💋‍👩🏿 E13.1 kiss: woman, woman, medium-dark skin tone, dark skin tone -#1F469 1F3FE 200D 2764 200D 1F48B 200D 1F469 1F3FF ; minimally-qualified # 👩🏾‍❤‍💋‍👩🏿 E13.1 kiss: woman, woman, medium-dark skin tone, dark skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FB ; fully-qualified # 👩🏿‍❤️‍💋‍👩🏻 E13.1 kiss: woman, woman, dark skin tone, light skin tone -#1F469 1F3FF 200D 2764 200D 1F48B 200D 1F469 1F3FB ; minimally-qualified # 👩🏿‍❤‍💋‍👩🏻 E13.1 kiss: woman, woman, dark skin tone, light skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FC ; fully-qualified # 👩🏿‍❤️‍💋‍👩🏼 E13.1 kiss: woman, woman, dark skin tone, medium-light skin tone -#1F469 1F3FF 200D 2764 200D 1F48B 200D 1F469 1F3FC ; minimally-qualified # 👩🏿‍❤‍💋‍👩🏼 E13.1 kiss: woman, woman, dark skin tone, medium-light skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FD ; fully-qualified # 👩🏿‍❤️‍💋‍👩🏽 E13.1 kiss: woman, woman, dark skin tone, medium skin tone -#1F469 1F3FF 200D 2764 200D 1F48B 200D 1F469 1F3FD ; minimally-qualified # 👩🏿‍❤‍💋‍👩🏽 E13.1 kiss: woman, woman, dark skin tone, medium skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FE ; fully-qualified # 👩🏿‍❤️‍💋‍👩🏾 E13.1 kiss: woman, woman, dark skin tone, medium-dark skin tone -#1F469 1F3FF 200D 2764 200D 1F48B 200D 1F469 1F3FE ; minimally-qualified # 👩🏿‍❤‍💋‍👩🏾 E13.1 kiss: woman, woman, dark skin tone, medium-dark skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FF ; fully-qualified # 👩🏿‍❤️‍💋‍👩🏿 E13.1 kiss: woman, woman, dark skin tone -#1F469 1F3FF 200D 2764 200D 1F48B 200D 1F469 1F3FF ; minimally-qualified # 👩🏿‍❤‍💋‍👩🏿 E13.1 kiss: woman, woman, dark skin tone -#1F491 ; fully-qualified # 💑 E0.6 couple with heart -#1F491 1F3FB ; fully-qualified # 💑🏻 E13.1 couple with heart: light skin tone -#1F491 1F3FC ; fully-qualified # 💑🏼 E13.1 couple with heart: medium-light skin tone -#1F491 1F3FD ; fully-qualified # 💑🏽 E13.1 couple with heart: medium skin tone -#1F491 1F3FE ; fully-qualified # 💑🏾 E13.1 couple with heart: medium-dark skin tone -#1F491 1F3FF ; fully-qualified # 💑🏿 E13.1 couple with heart: dark skin tone -#1F9D1 1F3FB 200D 2764 FE0F 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏻‍❤️‍🧑🏼 E13.1 couple with heart: person, person, light skin tone, medium-light skin tone -#1F9D1 1F3FB 200D 2764 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏻‍❤‍🧑🏼 E13.1 couple with heart: person, person, light skin tone, medium-light skin tone -#1F9D1 1F3FB 200D 2764 FE0F 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏻‍❤️‍🧑🏽 E13.1 couple with heart: person, person, light skin tone, medium skin tone -#1F9D1 1F3FB 200D 2764 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏻‍❤‍🧑🏽 E13.1 couple with heart: person, person, light skin tone, medium skin tone -#1F9D1 1F3FB 200D 2764 FE0F 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏻‍❤️‍🧑🏾 E13.1 couple with heart: person, person, light skin tone, medium-dark skin tone -#1F9D1 1F3FB 200D 2764 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏻‍❤‍🧑🏾 E13.1 couple with heart: person, person, light skin tone, medium-dark skin tone -#1F9D1 1F3FB 200D 2764 FE0F 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏻‍❤️‍🧑🏿 E13.1 couple with heart: person, person, light skin tone, dark skin tone -#1F9D1 1F3FB 200D 2764 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏻‍❤‍🧑🏿 E13.1 couple with heart: person, person, light skin tone, dark skin tone -#1F9D1 1F3FC 200D 2764 FE0F 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏼‍❤️‍🧑🏻 E13.1 couple with heart: person, person, medium-light skin tone, light skin tone -#1F9D1 1F3FC 200D 2764 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏼‍❤‍🧑🏻 E13.1 couple with heart: person, person, medium-light skin tone, light skin tone -#1F9D1 1F3FC 200D 2764 FE0F 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏼‍❤️‍🧑🏽 E13.1 couple with heart: person, person, medium-light skin tone, medium skin tone -#1F9D1 1F3FC 200D 2764 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏼‍❤‍🧑🏽 E13.1 couple with heart: person, person, medium-light skin tone, medium skin tone -#1F9D1 1F3FC 200D 2764 FE0F 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏼‍❤️‍🧑🏾 E13.1 couple with heart: person, person, medium-light skin tone, medium-dark skin tone -#1F9D1 1F3FC 200D 2764 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏼‍❤‍🧑🏾 E13.1 couple with heart: person, person, medium-light skin tone, medium-dark skin tone -#1F9D1 1F3FC 200D 2764 FE0F 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏼‍❤️‍🧑🏿 E13.1 couple with heart: person, person, medium-light skin tone, dark skin tone -#1F9D1 1F3FC 200D 2764 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏼‍❤‍🧑🏿 E13.1 couple with heart: person, person, medium-light skin tone, dark skin tone -#1F9D1 1F3FD 200D 2764 FE0F 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏽‍❤️‍🧑🏻 E13.1 couple with heart: person, person, medium skin tone, light skin tone -#1F9D1 1F3FD 200D 2764 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏽‍❤‍🧑🏻 E13.1 couple with heart: person, person, medium skin tone, light skin tone -#1F9D1 1F3FD 200D 2764 FE0F 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏽‍❤️‍🧑🏼 E13.1 couple with heart: person, person, medium skin tone, medium-light skin tone -#1F9D1 1F3FD 200D 2764 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏽‍❤‍🧑🏼 E13.1 couple with heart: person, person, medium skin tone, medium-light skin tone -#1F9D1 1F3FD 200D 2764 FE0F 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏽‍❤️‍🧑🏾 E13.1 couple with heart: person, person, medium skin tone, medium-dark skin tone -#1F9D1 1F3FD 200D 2764 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏽‍❤‍🧑🏾 E13.1 couple with heart: person, person, medium skin tone, medium-dark skin tone -#1F9D1 1F3FD 200D 2764 FE0F 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏽‍❤️‍🧑🏿 E13.1 couple with heart: person, person, medium skin tone, dark skin tone -#1F9D1 1F3FD 200D 2764 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏽‍❤‍🧑🏿 E13.1 couple with heart: person, person, medium skin tone, dark skin tone -#1F9D1 1F3FE 200D 2764 FE0F 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏾‍❤️‍🧑🏻 E13.1 couple with heart: person, person, medium-dark skin tone, light skin tone -#1F9D1 1F3FE 200D 2764 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏾‍❤‍🧑🏻 E13.1 couple with heart: person, person, medium-dark skin tone, light skin tone -#1F9D1 1F3FE 200D 2764 FE0F 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏾‍❤️‍🧑🏼 E13.1 couple with heart: person, person, medium-dark skin tone, medium-light skin tone -#1F9D1 1F3FE 200D 2764 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏾‍❤‍🧑🏼 E13.1 couple with heart: person, person, medium-dark skin tone, medium-light skin tone -#1F9D1 1F3FE 200D 2764 FE0F 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏾‍❤️‍🧑🏽 E13.1 couple with heart: person, person, medium-dark skin tone, medium skin tone -#1F9D1 1F3FE 200D 2764 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏾‍❤‍🧑🏽 E13.1 couple with heart: person, person, medium-dark skin tone, medium skin tone -#1F9D1 1F3FE 200D 2764 FE0F 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏾‍❤️‍🧑🏿 E13.1 couple with heart: person, person, medium-dark skin tone, dark skin tone -#1F9D1 1F3FE 200D 2764 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏾‍❤‍🧑🏿 E13.1 couple with heart: person, person, medium-dark skin tone, dark skin tone -#1F9D1 1F3FF 200D 2764 FE0F 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏿‍❤️‍🧑🏻 E13.1 couple with heart: person, person, dark skin tone, light skin tone -#1F9D1 1F3FF 200D 2764 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏿‍❤‍🧑🏻 E13.1 couple with heart: person, person, dark skin tone, light skin tone -#1F9D1 1F3FF 200D 2764 FE0F 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏿‍❤️‍🧑🏼 E13.1 couple with heart: person, person, dark skin tone, medium-light skin tone -#1F9D1 1F3FF 200D 2764 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏿‍❤‍🧑🏼 E13.1 couple with heart: person, person, dark skin tone, medium-light skin tone -#1F9D1 1F3FF 200D 2764 FE0F 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏿‍❤️‍🧑🏽 E13.1 couple with heart: person, person, dark skin tone, medium skin tone -#1F9D1 1F3FF 200D 2764 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏿‍❤‍🧑🏽 E13.1 couple with heart: person, person, dark skin tone, medium skin tone -#1F9D1 1F3FF 200D 2764 FE0F 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏿‍❤️‍🧑🏾 E13.1 couple with heart: person, person, dark skin tone, medium-dark skin tone -#1F9D1 1F3FF 200D 2764 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏿‍❤‍🧑🏾 E13.1 couple with heart: person, person, dark skin tone, medium-dark skin tone -#1F469 200D 2764 FE0F 200D 1F468 ; fully-qualified # 👩‍❤️‍👨 E2.0 couple with heart: woman, man -#1F469 200D 2764 200D 1F468 ; minimally-qualified # 👩‍❤‍👨 E2.0 couple with heart: woman, man -#1F469 1F3FB 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👩🏻‍❤️‍👨🏻 E13.1 couple with heart: woman, man, light skin tone -#1F469 1F3FB 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👩🏻‍❤‍👨🏻 E13.1 couple with heart: woman, man, light skin tone -#1F469 1F3FB 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👩🏻‍❤️‍👨🏼 E13.1 couple with heart: woman, man, light skin tone, medium-light skin tone -#1F469 1F3FB 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👩🏻‍❤‍👨🏼 E13.1 couple with heart: woman, man, light skin tone, medium-light skin tone -#1F469 1F3FB 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👩🏻‍❤️‍👨🏽 E13.1 couple with heart: woman, man, light skin tone, medium skin tone -#1F469 1F3FB 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👩🏻‍❤‍👨🏽 E13.1 couple with heart: woman, man, light skin tone, medium skin tone -#1F469 1F3FB 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👩🏻‍❤️‍👨🏾 E13.1 couple with heart: woman, man, light skin tone, medium-dark skin tone -#1F469 1F3FB 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👩🏻‍❤‍👨🏾 E13.1 couple with heart: woman, man, light skin tone, medium-dark skin tone -#1F469 1F3FB 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👩🏻‍❤️‍👨🏿 E13.1 couple with heart: woman, man, light skin tone, dark skin tone -#1F469 1F3FB 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👩🏻‍❤‍👨🏿 E13.1 couple with heart: woman, man, light skin tone, dark skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👩🏼‍❤️‍👨🏻 E13.1 couple with heart: woman, man, medium-light skin tone, light skin tone -#1F469 1F3FC 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👩🏼‍❤‍👨🏻 E13.1 couple with heart: woman, man, medium-light skin tone, light skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👩🏼‍❤️‍👨🏼 E13.1 couple with heart: woman, man, medium-light skin tone -#1F469 1F3FC 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👩🏼‍❤‍👨🏼 E13.1 couple with heart: woman, man, medium-light skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👩🏼‍❤️‍👨🏽 E13.1 couple with heart: woman, man, medium-light skin tone, medium skin tone -#1F469 1F3FC 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👩🏼‍❤‍👨🏽 E13.1 couple with heart: woman, man, medium-light skin tone, medium skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👩🏼‍❤️‍👨🏾 E13.1 couple with heart: woman, man, medium-light skin tone, medium-dark skin tone -#1F469 1F3FC 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👩🏼‍❤‍👨🏾 E13.1 couple with heart: woman, man, medium-light skin tone, medium-dark skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👩🏼‍❤️‍👨🏿 E13.1 couple with heart: woman, man, medium-light skin tone, dark skin tone -#1F469 1F3FC 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👩🏼‍❤‍👨🏿 E13.1 couple with heart: woman, man, medium-light skin tone, dark skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👩🏽‍❤️‍👨🏻 E13.1 couple with heart: woman, man, medium skin tone, light skin tone -#1F469 1F3FD 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👩🏽‍❤‍👨🏻 E13.1 couple with heart: woman, man, medium skin tone, light skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👩🏽‍❤️‍👨🏼 E13.1 couple with heart: woman, man, medium skin tone, medium-light skin tone -#1F469 1F3FD 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👩🏽‍❤‍👨🏼 E13.1 couple with heart: woman, man, medium skin tone, medium-light skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👩🏽‍❤️‍👨🏽 E13.1 couple with heart: woman, man, medium skin tone -#1F469 1F3FD 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👩🏽‍❤‍👨🏽 E13.1 couple with heart: woman, man, medium skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👩🏽‍❤️‍👨🏾 E13.1 couple with heart: woman, man, medium skin tone, medium-dark skin tone -#1F469 1F3FD 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👩🏽‍❤‍👨🏾 E13.1 couple with heart: woman, man, medium skin tone, medium-dark skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👩🏽‍❤️‍👨🏿 E13.1 couple with heart: woman, man, medium skin tone, dark skin tone -#1F469 1F3FD 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👩🏽‍❤‍👨🏿 E13.1 couple with heart: woman, man, medium skin tone, dark skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👩🏾‍❤️‍👨🏻 E13.1 couple with heart: woman, man, medium-dark skin tone, light skin tone -#1F469 1F3FE 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👩🏾‍❤‍👨🏻 E13.1 couple with heart: woman, man, medium-dark skin tone, light skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👩🏾‍❤️‍👨🏼 E13.1 couple with heart: woman, man, medium-dark skin tone, medium-light skin tone -#1F469 1F3FE 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👩🏾‍❤‍👨🏼 E13.1 couple with heart: woman, man, medium-dark skin tone, medium-light skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👩🏾‍❤️‍👨🏽 E13.1 couple with heart: woman, man, medium-dark skin tone, medium skin tone -#1F469 1F3FE 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👩🏾‍❤‍👨🏽 E13.1 couple with heart: woman, man, medium-dark skin tone, medium skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👩🏾‍❤️‍👨🏾 E13.1 couple with heart: woman, man, medium-dark skin tone -#1F469 1F3FE 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👩🏾‍❤‍👨🏾 E13.1 couple with heart: woman, man, medium-dark skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👩🏾‍❤️‍👨🏿 E13.1 couple with heart: woman, man, medium-dark skin tone, dark skin tone -#1F469 1F3FE 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👩🏾‍❤‍👨🏿 E13.1 couple with heart: woman, man, medium-dark skin tone, dark skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👩🏿‍❤️‍👨🏻 E13.1 couple with heart: woman, man, dark skin tone, light skin tone -#1F469 1F3FF 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👩🏿‍❤‍👨🏻 E13.1 couple with heart: woman, man, dark skin tone, light skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👩🏿‍❤️‍👨🏼 E13.1 couple with heart: woman, man, dark skin tone, medium-light skin tone -#1F469 1F3FF 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👩🏿‍❤‍👨🏼 E13.1 couple with heart: woman, man, dark skin tone, medium-light skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👩🏿‍❤️‍👨🏽 E13.1 couple with heart: woman, man, dark skin tone, medium skin tone -#1F469 1F3FF 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👩🏿‍❤‍👨🏽 E13.1 couple with heart: woman, man, dark skin tone, medium skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👩🏿‍❤️‍👨🏾 E13.1 couple with heart: woman, man, dark skin tone, medium-dark skin tone -#1F469 1F3FF 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👩🏿‍❤‍👨🏾 E13.1 couple with heart: woman, man, dark skin tone, medium-dark skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👩🏿‍❤️‍👨🏿 E13.1 couple with heart: woman, man, dark skin tone -#1F469 1F3FF 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👩🏿‍❤‍👨🏿 E13.1 couple with heart: woman, man, dark skin tone -#1F468 200D 2764 FE0F 200D 1F468 ; fully-qualified # 👨‍❤️‍👨 E2.0 couple with heart: man, man -#1F468 200D 2764 200D 1F468 ; minimally-qualified # 👨‍❤‍👨 E2.0 couple with heart: man, man -#1F468 1F3FB 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👨🏻‍❤️‍👨🏻 E13.1 couple with heart: man, man, light skin tone -#1F468 1F3FB 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👨🏻‍❤‍👨🏻 E13.1 couple with heart: man, man, light skin tone -#1F468 1F3FB 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👨🏻‍❤️‍👨🏼 E13.1 couple with heart: man, man, light skin tone, medium-light skin tone -#1F468 1F3FB 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👨🏻‍❤‍👨🏼 E13.1 couple with heart: man, man, light skin tone, medium-light skin tone -#1F468 1F3FB 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👨🏻‍❤️‍👨🏽 E13.1 couple with heart: man, man, light skin tone, medium skin tone -#1F468 1F3FB 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👨🏻‍❤‍👨🏽 E13.1 couple with heart: man, man, light skin tone, medium skin tone -#1F468 1F3FB 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👨🏻‍❤️‍👨🏾 E13.1 couple with heart: man, man, light skin tone, medium-dark skin tone -#1F468 1F3FB 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👨🏻‍❤‍👨🏾 E13.1 couple with heart: man, man, light skin tone, medium-dark skin tone -#1F468 1F3FB 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👨🏻‍❤️‍👨🏿 E13.1 couple with heart: man, man, light skin tone, dark skin tone -#1F468 1F3FB 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👨🏻‍❤‍👨🏿 E13.1 couple with heart: man, man, light skin tone, dark skin tone -#1F468 1F3FC 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👨🏼‍❤️‍👨🏻 E13.1 couple with heart: man, man, medium-light skin tone, light skin tone -#1F468 1F3FC 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👨🏼‍❤‍👨🏻 E13.1 couple with heart: man, man, medium-light skin tone, light skin tone -#1F468 1F3FC 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👨🏼‍❤️‍👨🏼 E13.1 couple with heart: man, man, medium-light skin tone -#1F468 1F3FC 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👨🏼‍❤‍👨🏼 E13.1 couple with heart: man, man, medium-light skin tone -#1F468 1F3FC 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👨🏼‍❤️‍👨🏽 E13.1 couple with heart: man, man, medium-light skin tone, medium skin tone -#1F468 1F3FC 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👨🏼‍❤‍👨🏽 E13.1 couple with heart: man, man, medium-light skin tone, medium skin tone -#1F468 1F3FC 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👨🏼‍❤️‍👨🏾 E13.1 couple with heart: man, man, medium-light skin tone, medium-dark skin tone -#1F468 1F3FC 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👨🏼‍❤‍👨🏾 E13.1 couple with heart: man, man, medium-light skin tone, medium-dark skin tone -#1F468 1F3FC 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👨🏼‍❤️‍👨🏿 E13.1 couple with heart: man, man, medium-light skin tone, dark skin tone -#1F468 1F3FC 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👨🏼‍❤‍👨🏿 E13.1 couple with heart: man, man, medium-light skin tone, dark skin tone -#1F468 1F3FD 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👨🏽‍❤️‍👨🏻 E13.1 couple with heart: man, man, medium skin tone, light skin tone -#1F468 1F3FD 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👨🏽‍❤‍👨🏻 E13.1 couple with heart: man, man, medium skin tone, light skin tone -#1F468 1F3FD 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👨🏽‍❤️‍👨🏼 E13.1 couple with heart: man, man, medium skin tone, medium-light skin tone -#1F468 1F3FD 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👨🏽‍❤‍👨🏼 E13.1 couple with heart: man, man, medium skin tone, medium-light skin tone -#1F468 1F3FD 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👨🏽‍❤️‍👨🏽 E13.1 couple with heart: man, man, medium skin tone -#1F468 1F3FD 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👨🏽‍❤‍👨🏽 E13.1 couple with heart: man, man, medium skin tone -#1F468 1F3FD 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👨🏽‍❤️‍👨🏾 E13.1 couple with heart: man, man, medium skin tone, medium-dark skin tone -#1F468 1F3FD 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👨🏽‍❤‍👨🏾 E13.1 couple with heart: man, man, medium skin tone, medium-dark skin tone -#1F468 1F3FD 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👨🏽‍❤️‍👨🏿 E13.1 couple with heart: man, man, medium skin tone, dark skin tone -#1F468 1F3FD 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👨🏽‍❤‍👨🏿 E13.1 couple with heart: man, man, medium skin tone, dark skin tone -#1F468 1F3FE 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👨🏾‍❤️‍👨🏻 E13.1 couple with heart: man, man, medium-dark skin tone, light skin tone -#1F468 1F3FE 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👨🏾‍❤‍👨🏻 E13.1 couple with heart: man, man, medium-dark skin tone, light skin tone -#1F468 1F3FE 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👨🏾‍❤️‍👨🏼 E13.1 couple with heart: man, man, medium-dark skin tone, medium-light skin tone -#1F468 1F3FE 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👨🏾‍❤‍👨🏼 E13.1 couple with heart: man, man, medium-dark skin tone, medium-light skin tone -#1F468 1F3FE 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👨🏾‍❤️‍👨🏽 E13.1 couple with heart: man, man, medium-dark skin tone, medium skin tone -#1F468 1F3FE 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👨🏾‍❤‍👨🏽 E13.1 couple with heart: man, man, medium-dark skin tone, medium skin tone -#1F468 1F3FE 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👨🏾‍❤️‍👨🏾 E13.1 couple with heart: man, man, medium-dark skin tone -#1F468 1F3FE 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👨🏾‍❤‍👨🏾 E13.1 couple with heart: man, man, medium-dark skin tone -#1F468 1F3FE 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👨🏾‍❤️‍👨🏿 E13.1 couple with heart: man, man, medium-dark skin tone, dark skin tone -#1F468 1F3FE 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👨🏾‍❤‍👨🏿 E13.1 couple with heart: man, man, medium-dark skin tone, dark skin tone -#1F468 1F3FF 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👨🏿‍❤️‍👨🏻 E13.1 couple with heart: man, man, dark skin tone, light skin tone -#1F468 1F3FF 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👨🏿‍❤‍👨🏻 E13.1 couple with heart: man, man, dark skin tone, light skin tone -#1F468 1F3FF 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👨🏿‍❤️‍👨🏼 E13.1 couple with heart: man, man, dark skin tone, medium-light skin tone -#1F468 1F3FF 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👨🏿‍❤‍👨🏼 E13.1 couple with heart: man, man, dark skin tone, medium-light skin tone -#1F468 1F3FF 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👨🏿‍❤️‍👨🏽 E13.1 couple with heart: man, man, dark skin tone, medium skin tone -#1F468 1F3FF 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👨🏿‍❤‍👨🏽 E13.1 couple with heart: man, man, dark skin tone, medium skin tone -#1F468 1F3FF 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👨🏿‍❤️‍👨🏾 E13.1 couple with heart: man, man, dark skin tone, medium-dark skin tone -#1F468 1F3FF 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👨🏿‍❤‍👨🏾 E13.1 couple with heart: man, man, dark skin tone, medium-dark skin tone -#1F468 1F3FF 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👨🏿‍❤️‍👨🏿 E13.1 couple with heart: man, man, dark skin tone -#1F468 1F3FF 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👨🏿‍❤‍👨🏿 E13.1 couple with heart: man, man, dark skin tone -#1F469 200D 2764 FE0F 200D 1F469 ; fully-qualified # 👩‍❤️‍👩 E2.0 couple with heart: woman, woman -#1F469 200D 2764 200D 1F469 ; minimally-qualified # 👩‍❤‍👩 E2.0 couple with heart: woman, woman -#1F469 1F3FB 200D 2764 FE0F 200D 1F469 1F3FB ; fully-qualified # 👩🏻‍❤️‍👩🏻 E13.1 couple with heart: woman, woman, light skin tone -#1F469 1F3FB 200D 2764 200D 1F469 1F3FB ; minimally-qualified # 👩🏻‍❤‍👩🏻 E13.1 couple with heart: woman, woman, light skin tone -#1F469 1F3FB 200D 2764 FE0F 200D 1F469 1F3FC ; fully-qualified # 👩🏻‍❤️‍👩🏼 E13.1 couple with heart: woman, woman, light skin tone, medium-light skin tone -#1F469 1F3FB 200D 2764 200D 1F469 1F3FC ; minimally-qualified # 👩🏻‍❤‍👩🏼 E13.1 couple with heart: woman, woman, light skin tone, medium-light skin tone -#1F469 1F3FB 200D 2764 FE0F 200D 1F469 1F3FD ; fully-qualified # 👩🏻‍❤️‍👩🏽 E13.1 couple with heart: woman, woman, light skin tone, medium skin tone -#1F469 1F3FB 200D 2764 200D 1F469 1F3FD ; minimally-qualified # 👩🏻‍❤‍👩🏽 E13.1 couple with heart: woman, woman, light skin tone, medium skin tone -#1F469 1F3FB 200D 2764 FE0F 200D 1F469 1F3FE ; fully-qualified # 👩🏻‍❤️‍👩🏾 E13.1 couple with heart: woman, woman, light skin tone, medium-dark skin tone -#1F469 1F3FB 200D 2764 200D 1F469 1F3FE ; minimally-qualified # 👩🏻‍❤‍👩🏾 E13.1 couple with heart: woman, woman, light skin tone, medium-dark skin tone -#1F469 1F3FB 200D 2764 FE0F 200D 1F469 1F3FF ; fully-qualified # 👩🏻‍❤️‍👩🏿 E13.1 couple with heart: woman, woman, light skin tone, dark skin tone -#1F469 1F3FB 200D 2764 200D 1F469 1F3FF ; minimally-qualified # 👩🏻‍❤‍👩🏿 E13.1 couple with heart: woman, woman, light skin tone, dark skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F469 1F3FB ; fully-qualified # 👩🏼‍❤️‍👩🏻 E13.1 couple with heart: woman, woman, medium-light skin tone, light skin tone -#1F469 1F3FC 200D 2764 200D 1F469 1F3FB ; minimally-qualified # 👩🏼‍❤‍👩🏻 E13.1 couple with heart: woman, woman, medium-light skin tone, light skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F469 1F3FC ; fully-qualified # 👩🏼‍❤️‍👩🏼 E13.1 couple with heart: woman, woman, medium-light skin tone -#1F469 1F3FC 200D 2764 200D 1F469 1F3FC ; minimally-qualified # 👩🏼‍❤‍👩🏼 E13.1 couple with heart: woman, woman, medium-light skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F469 1F3FD ; fully-qualified # 👩🏼‍❤️‍👩🏽 E13.1 couple with heart: woman, woman, medium-light skin tone, medium skin tone -#1F469 1F3FC 200D 2764 200D 1F469 1F3FD ; minimally-qualified # 👩🏼‍❤‍👩🏽 E13.1 couple with heart: woman, woman, medium-light skin tone, medium skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F469 1F3FE ; fully-qualified # 👩🏼‍❤️‍👩🏾 E13.1 couple with heart: woman, woman, medium-light skin tone, medium-dark skin tone -#1F469 1F3FC 200D 2764 200D 1F469 1F3FE ; minimally-qualified # 👩🏼‍❤‍👩🏾 E13.1 couple with heart: woman, woman, medium-light skin tone, medium-dark skin tone -#1F469 1F3FC 200D 2764 FE0F 200D 1F469 1F3FF ; fully-qualified # 👩🏼‍❤️‍👩🏿 E13.1 couple with heart: woman, woman, medium-light skin tone, dark skin tone -#1F469 1F3FC 200D 2764 200D 1F469 1F3FF ; minimally-qualified # 👩🏼‍❤‍👩🏿 E13.1 couple with heart: woman, woman, medium-light skin tone, dark skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F469 1F3FB ; fully-qualified # 👩🏽‍❤️‍👩🏻 E13.1 couple with heart: woman, woman, medium skin tone, light skin tone -#1F469 1F3FD 200D 2764 200D 1F469 1F3FB ; minimally-qualified # 👩🏽‍❤‍👩🏻 E13.1 couple with heart: woman, woman, medium skin tone, light skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F469 1F3FC ; fully-qualified # 👩🏽‍❤️‍👩🏼 E13.1 couple with heart: woman, woman, medium skin tone, medium-light skin tone -#1F469 1F3FD 200D 2764 200D 1F469 1F3FC ; minimally-qualified # 👩🏽‍❤‍👩🏼 E13.1 couple with heart: woman, woman, medium skin tone, medium-light skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F469 1F3FD ; fully-qualified # 👩🏽‍❤️‍👩🏽 E13.1 couple with heart: woman, woman, medium skin tone -#1F469 1F3FD 200D 2764 200D 1F469 1F3FD ; minimally-qualified # 👩🏽‍❤‍👩🏽 E13.1 couple with heart: woman, woman, medium skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F469 1F3FE ; fully-qualified # 👩🏽‍❤️‍👩🏾 E13.1 couple with heart: woman, woman, medium skin tone, medium-dark skin tone -#1F469 1F3FD 200D 2764 200D 1F469 1F3FE ; minimally-qualified # 👩🏽‍❤‍👩🏾 E13.1 couple with heart: woman, woman, medium skin tone, medium-dark skin tone -#1F469 1F3FD 200D 2764 FE0F 200D 1F469 1F3FF ; fully-qualified # 👩🏽‍❤️‍👩🏿 E13.1 couple with heart: woman, woman, medium skin tone, dark skin tone -#1F469 1F3FD 200D 2764 200D 1F469 1F3FF ; minimally-qualified # 👩🏽‍❤‍👩🏿 E13.1 couple with heart: woman, woman, medium skin tone, dark skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F469 1F3FB ; fully-qualified # 👩🏾‍❤️‍👩🏻 E13.1 couple with heart: woman, woman, medium-dark skin tone, light skin tone -#1F469 1F3FE 200D 2764 200D 1F469 1F3FB ; minimally-qualified # 👩🏾‍❤‍👩🏻 E13.1 couple with heart: woman, woman, medium-dark skin tone, light skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F469 1F3FC ; fully-qualified # 👩🏾‍❤️‍👩🏼 E13.1 couple with heart: woman, woman, medium-dark skin tone, medium-light skin tone -#1F469 1F3FE 200D 2764 200D 1F469 1F3FC ; minimally-qualified # 👩🏾‍❤‍👩🏼 E13.1 couple with heart: woman, woman, medium-dark skin tone, medium-light skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F469 1F3FD ; fully-qualified # 👩🏾‍❤️‍👩🏽 E13.1 couple with heart: woman, woman, medium-dark skin tone, medium skin tone -#1F469 1F3FE 200D 2764 200D 1F469 1F3FD ; minimally-qualified # 👩🏾‍❤‍👩🏽 E13.1 couple with heart: woman, woman, medium-dark skin tone, medium skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F469 1F3FE ; fully-qualified # 👩🏾‍❤️‍👩🏾 E13.1 couple with heart: woman, woman, medium-dark skin tone -#1F469 1F3FE 200D 2764 200D 1F469 1F3FE ; minimally-qualified # 👩🏾‍❤‍👩🏾 E13.1 couple with heart: woman, woman, medium-dark skin tone -#1F469 1F3FE 200D 2764 FE0F 200D 1F469 1F3FF ; fully-qualified # 👩🏾‍❤️‍👩🏿 E13.1 couple with heart: woman, woman, medium-dark skin tone, dark skin tone -#1F469 1F3FE 200D 2764 200D 1F469 1F3FF ; minimally-qualified # 👩🏾‍❤‍👩🏿 E13.1 couple with heart: woman, woman, medium-dark skin tone, dark skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F469 1F3FB ; fully-qualified # 👩🏿‍❤️‍👩🏻 E13.1 couple with heart: woman, woman, dark skin tone, light skin tone -#1F469 1F3FF 200D 2764 200D 1F469 1F3FB ; minimally-qualified # 👩🏿‍❤‍👩🏻 E13.1 couple with heart: woman, woman, dark skin tone, light skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F469 1F3FC ; fully-qualified # 👩🏿‍❤️‍👩🏼 E13.1 couple with heart: woman, woman, dark skin tone, medium-light skin tone -#1F469 1F3FF 200D 2764 200D 1F469 1F3FC ; minimally-qualified # 👩🏿‍❤‍👩🏼 E13.1 couple with heart: woman, woman, dark skin tone, medium-light skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F469 1F3FD ; fully-qualified # 👩🏿‍❤️‍👩🏽 E13.1 couple with heart: woman, woman, dark skin tone, medium skin tone -#1F469 1F3FF 200D 2764 200D 1F469 1F3FD ; minimally-qualified # 👩🏿‍❤‍👩🏽 E13.1 couple with heart: woman, woman, dark skin tone, medium skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F469 1F3FE ; fully-qualified # 👩🏿‍❤️‍👩🏾 E13.1 couple with heart: woman, woman, dark skin tone, medium-dark skin tone -#1F469 1F3FF 200D 2764 200D 1F469 1F3FE ; minimally-qualified # 👩🏿‍❤‍👩🏾 E13.1 couple with heart: woman, woman, dark skin tone, medium-dark skin tone -#1F469 1F3FF 200D 2764 FE0F 200D 1F469 1F3FF ; fully-qualified # 👩🏿‍❤️‍👩🏿 E13.1 couple with heart: woman, woman, dark skin tone -#1F469 1F3FF 200D 2764 200D 1F469 1F3FF ; minimally-qualified # 👩🏿‍❤‍👩🏿 E13.1 couple with heart: woman, woman, dark skin tone -#1F46A ; fully-qualified # 👪 E0.6 family -#1F468 200D 1F469 200D 1F466 ; fully-qualified # 👨‍👩‍👦 E2.0 family: man, woman, boy -#1F468 200D 1F469 200D 1F467 ; fully-qualified # 👨‍👩‍👧 E2.0 family: man, woman, girl -#1F468 200D 1F469 200D 1F467 200D 1F466 ; fully-qualified # 👨‍👩‍👧‍👦 E2.0 family: man, woman, girl, boy -#1F468 200D 1F469 200D 1F466 200D 1F466 ; fully-qualified # 👨‍👩‍👦‍👦 E2.0 family: man, woman, boy, boy -#1F468 200D 1F469 200D 1F467 200D 1F467 ; fully-qualified # 👨‍👩‍👧‍👧 E2.0 family: man, woman, girl, girl -#1F468 200D 1F468 200D 1F466 ; fully-qualified # 👨‍👨‍👦 E2.0 family: man, man, boy -#1F468 200D 1F468 200D 1F467 ; fully-qualified # 👨‍👨‍👧 E2.0 family: man, man, girl -#1F468 200D 1F468 200D 1F467 200D 1F466 ; fully-qualified # 👨‍👨‍👧‍👦 E2.0 family: man, man, girl, boy -#1F468 200D 1F468 200D 1F466 200D 1F466 ; fully-qualified # 👨‍👨‍👦‍👦 E2.0 family: man, man, boy, boy -#1F468 200D 1F468 200D 1F467 200D 1F467 ; fully-qualified # 👨‍👨‍👧‍👧 E2.0 family: man, man, girl, girl -#1F469 200D 1F469 200D 1F466 ; fully-qualified # 👩‍👩‍👦 E2.0 family: woman, woman, boy -#1F469 200D 1F469 200D 1F467 ; fully-qualified # 👩‍👩‍👧 E2.0 family: woman, woman, girl -#1F469 200D 1F469 200D 1F467 200D 1F466 ; fully-qualified # 👩‍👩‍👧‍👦 E2.0 family: woman, woman, girl, boy -#1F469 200D 1F469 200D 1F466 200D 1F466 ; fully-qualified # 👩‍👩‍👦‍👦 E2.0 family: woman, woman, boy, boy -#1F469 200D 1F469 200D 1F467 200D 1F467 ; fully-qualified # 👩‍👩‍👧‍👧 E2.0 family: woman, woman, girl, girl -#1F468 200D 1F466 ; fully-qualified # 👨‍👦 E4.0 family: man, boy -#1F468 200D 1F466 200D 1F466 ; fully-qualified # 👨‍👦‍👦 E4.0 family: man, boy, boy -#1F468 200D 1F467 ; fully-qualified # 👨‍👧 E4.0 family: man, girl -#1F468 200D 1F467 200D 1F466 ; fully-qualified # 👨‍👧‍👦 E4.0 family: man, girl, boy -#1F468 200D 1F467 200D 1F467 ; fully-qualified # 👨‍👧‍👧 E4.0 family: man, girl, girl -#1F469 200D 1F466 ; fully-qualified # 👩‍👦 E4.0 family: woman, boy -#1F469 200D 1F466 200D 1F466 ; fully-qualified # 👩‍👦‍👦 E4.0 family: woman, boy, boy -#1F469 200D 1F467 ; fully-qualified # 👩‍👧 E4.0 family: woman, girl -#1F469 200D 1F467 200D 1F466 ; fully-qualified # 👩‍👧‍👦 E4.0 family: woman, girl, boy -#1F469 200D 1F467 200D 1F467 ; fully-qualified # 👩‍👧‍👧 E4.0 family: woman, girl, girl -# -## subgroup: person-symbol -#1F5E3 FE0F ; fully-qualified # 🗣️ E0.7 speaking head -#1F5E3 ; unqualified # 🗣 E0.7 speaking head -#1F464 ; fully-qualified # 👤 E0.6 bust in silhouette -#1F465 ; fully-qualified # 👥 E1.0 busts in silhouette -#1FAC2 ; fully-qualified # 🫂 E13.0 people hugging -#1F463 ; fully-qualified # 👣 E0.6 footprints -# -## People & Body subtotal: 2986 -## People & Body subtotal: 506 w/o modifiers -# -## group: Component -# -## subgroup: skin-tone -#1F3FB ; component # 🏻 E1.0 light skin tone -#1F3FC ; component # 🏼 E1.0 medium-light skin tone -#1F3FD ; component # 🏽 E1.0 medium skin tone -#1F3FE ; component # 🏾 E1.0 medium-dark skin tone -#1F3FF ; component # 🏿 E1.0 dark skin tone -# -## subgroup: hair-style -#1F9B0 ; component # 🦰 E11.0 red hair -#1F9B1 ; component # 🦱 E11.0 curly hair -#1F9B3 ; component # 🦳 E11.0 white hair -#1F9B2 ; component # 🦲 E11.0 bald -# -## Component subtotal: 9 -## Component subtotal: 4 w/o modifiers -# -## group: Animals & Nature -# -## subgroup: animal-mammal -1F435 ; fully-qualified # 🐵 E0.6 monkey face -1F412 ; fully-qualified # 🐒 E0.6 monkey -1F98D ; fully-qualified # 🦍 E3.0 gorilla -1F9A7 ; fully-qualified # 🦧 E12.0 orangutan -1F436 ; fully-qualified # 🐶 E0.6 dog face -1F415 ; fully-qualified # 🐕 E0.7 dog -1F9AE ; fully-qualified # 🦮 E12.0 guide dog -1F415 200D 1F9BA ; fully-qualified # 🐕‍🦺 E12.0 service dog -1F429 ; fully-qualified # 🐩 E0.6 poodle -1F43A ; fully-qualified # 🐺 E0.6 wolf -1F98A ; fully-qualified # 🦊 E3.0 fox -1F99D ; fully-qualified # 🦝 E11.0 raccoon -1F431 ; fully-qualified # 🐱 E0.6 cat face -1F408 ; fully-qualified # 🐈 E0.7 cat -1F408 200D 2B1B ; fully-qualified # 🐈‍⬛ E13.0 black cat -1F981 ; fully-qualified # 🦁 E1.0 lion -1F42F ; fully-qualified # 🐯 E0.6 tiger face -1F405 ; fully-qualified # 🐅 E1.0 tiger -1F406 ; fully-qualified # 🐆 E1.0 leopard -1F434 ; fully-qualified # 🐴 E0.6 horse face -1F40E ; fully-qualified # 🐎 E0.6 horse -1F984 ; fully-qualified # 🦄 E1.0 unicorn -1F993 ; fully-qualified # 🦓 E5.0 zebra -1F98C ; fully-qualified # 🦌 E3.0 deer -1F9AC ; fully-qualified # 🦬 E13.0 bison -1F42E ; fully-qualified # 🐮 E0.6 cow face -1F402 ; fully-qualified # 🐂 E1.0 ox -1F403 ; fully-qualified # 🐃 E1.0 water buffalo -1F404 ; fully-qualified # 🐄 E1.0 cow -1F437 ; fully-qualified # 🐷 E0.6 pig face -1F416 ; fully-qualified # 🐖 E1.0 pig -1F417 ; fully-qualified # 🐗 E0.6 boar -1F43D ; fully-qualified # 🐽 E0.6 pig nose -1F40F ; fully-qualified # 🐏 E1.0 ram -1F411 ; fully-qualified # 🐑 E0.6 ewe -1F410 ; fully-qualified # 🐐 E1.0 goat -1F42A ; fully-qualified # 🐪 E1.0 camel -1F42B ; fully-qualified # 🐫 E0.6 two-hump camel -1F999 ; fully-qualified # 🦙 E11.0 llama -1F992 ; fully-qualified # 🦒 E5.0 giraffe -1F418 ; fully-qualified # 🐘 E0.6 elephant -1F9A3 ; fully-qualified # 🦣 E13.0 mammoth -1F98F ; fully-qualified # 🦏 E3.0 rhinoceros -1F99B ; fully-qualified # 🦛 E11.0 hippopotamus -1F42D ; fully-qualified # 🐭 E0.6 mouse face -1F401 ; fully-qualified # 🐁 E1.0 mouse -1F400 ; fully-qualified # 🐀 E1.0 rat -1F439 ; fully-qualified # 🐹 E0.6 hamster -1F430 ; fully-qualified # 🐰 E0.6 rabbit face -1F407 ; fully-qualified # 🐇 E1.0 rabbit -1F43F FE0F ; fully-qualified # 🐿️ E0.7 chipmunk -1F43F ; unqualified # 🐿 E0.7 chipmunk -1F9AB ; fully-qualified # 🦫 E13.0 beaver -1F994 ; fully-qualified # 🦔 E5.0 hedgehog -1F987 ; fully-qualified # 🦇 E3.0 bat -1F43B ; fully-qualified # 🐻 E0.6 bear -1F43B 200D 2744 FE0F ; fully-qualified # 🐻‍❄️ E13.0 polar bear -1F43B 200D 2744 ; minimally-qualified # 🐻‍❄ E13.0 polar bear -1F428 ; fully-qualified # 🐨 E0.6 koala -1F43C ; fully-qualified # 🐼 E0.6 panda -1F9A5 ; fully-qualified # 🦥 E12.0 sloth -1F9A6 ; fully-qualified # 🦦 E12.0 otter -1F9A8 ; fully-qualified # 🦨 E12.0 skunk -1F998 ; fully-qualified # 🦘 E11.0 kangaroo -1F9A1 ; fully-qualified # 🦡 E11.0 badger -1F43E ; fully-qualified # 🐾 E0.6 paw prints -# -## subgroup: animal-bird -1F983 ; fully-qualified # 🦃 E1.0 turkey -1F414 ; fully-qualified # 🐔 E0.6 chicken -1F413 ; fully-qualified # 🐓 E1.0 rooster -1F423 ; fully-qualified # 🐣 E0.6 hatching chick -1F424 ; fully-qualified # 🐤 E0.6 baby chick -1F425 ; fully-qualified # 🐥 E0.6 front-facing baby chick -1F426 ; fully-qualified # 🐦 E0.6 bird -1F427 ; fully-qualified # 🐧 E0.6 penguin -1F54A FE0F ; fully-qualified # 🕊️ E0.7 dove -1F54A ; unqualified # 🕊 E0.7 dove -1F985 ; fully-qualified # 🦅 E3.0 eagle -1F986 ; fully-qualified # 🦆 E3.0 duck -1F9A2 ; fully-qualified # 🦢 E11.0 swan -1F989 ; fully-qualified # 🦉 E3.0 owl -1F9A4 ; fully-qualified # 🦤 E13.0 dodo -1FAB6 ; fully-qualified # 🪶 E13.0 feather -1F9A9 ; fully-qualified # 🦩 E12.0 flamingo -1F99A ; fully-qualified # 🦚 E11.0 peacock -1F99C ; fully-qualified # 🦜 E11.0 parrot -# -## subgroup: animal-amphibian -#1F438 ; fully-qualified # 🐸 E0.6 frog -# -## subgroup: animal-reptile -1F40A ; fully-qualified # 🐊 E1.0 crocodile -1F422 ; fully-qualified # 🐢 E0.6 turtle -1F98E ; fully-qualified # 🦎 E3.0 lizard -1F40D ; fully-qualified # 🐍 E0.6 snake -1F432 ; fully-qualified # 🐲 E0.6 dragon face -1F409 ; fully-qualified # 🐉 E1.0 dragon -1F995 ; fully-qualified # 🦕 E5.0 sauropod -1F996 ; fully-qualified # 🦖 E5.0 T-Rex -# -## subgroup: animal-marine -1F433 ; fully-qualified # 🐳 E0.6 spouting whale -1F40B ; fully-qualified # 🐋 E1.0 whale -1F42C ; fully-qualified # 🐬 E0.6 dolphin -1F9AD ; fully-qualified # 🦭 E13.0 seal -1F41F ; fully-qualified # 🐟 E0.6 fish -1F420 ; fully-qualified # 🐠 E0.6 tropical fish -1F421 ; fully-qualified # 🐡 E0.6 blowfish -1F988 ; fully-qualified # 🦈 E3.0 shark -1F419 ; fully-qualified # 🐙 E0.6 octopus -1F41A ; fully-qualified # 🐚 E0.6 spiral shell -1FAB8 ; fully-qualified # 🪸 E14.0 coral -# -## subgroup: animal-bug -1F40C ; fully-qualified # 🐌 E0.6 snail -1F98B ; fully-qualified # 🦋 E3.0 butterfly -1F41B ; fully-qualified # 🐛 E0.6 bug -1F41C ; fully-qualified # 🐜 E0.6 ant -1F41D ; fully-qualified # 🐝 E0.6 honeybee -1FAB2 ; fully-qualified # 🪲 E13.0 beetle -1F41E ; fully-qualified # 🐞 E0.6 lady beetle -1F997 ; fully-qualified # 🦗 E5.0 cricket -1FAB3 ; fully-qualified # 🪳 E13.0 cockroach -1F577 FE0F ; fully-qualified # 🕷️ E0.7 spider -1F577 ; unqualified # 🕷 E0.7 spider -1F578 FE0F ; fully-qualified # 🕸️ E0.7 spider web -1F578 ; unqualified # 🕸 E0.7 spider web -1F982 ; fully-qualified # 🦂 E1.0 scorpion -1F99F ; fully-qualified # 🦟 E11.0 mosquito -1FAB0 ; fully-qualified # 🪰 E13.0 fly -1FAB1 ; fully-qualified # 🪱 E13.0 worm -1F9A0 ; fully-qualified # 🦠 E11.0 microbe -# -## subgroup: plant-flower -1F490 ; fully-qualified # 💐 E0.6 bouquet -1F338 ; fully-qualified # 🌸 E0.6 cherry blossom -1F4AE ; fully-qualified # 💮 E0.6 white flower -1FAB7 ; fully-qualified # 🪷 E14.0 lotus -1F3F5 FE0F ; fully-qualified # 🏵️ E0.7 rosette -1F3F5 ; unqualified # 🏵 E0.7 rosette -1F339 ; fully-qualified # 🌹 E0.6 rose -1F940 ; fully-qualified # 🥀 E3.0 wilted flower -1F33A ; fully-qualified # 🌺 E0.6 hibiscus -1F33B ; fully-qualified # 🌻 E0.6 sunflower -1F33C ; fully-qualified # 🌼 E0.6 blossom -1F337 ; fully-qualified # 🌷 E0.6 tulip -# -## subgroup: plant-other -1F331 ; fully-qualified # 🌱 E0.6 seedling -1FAB4 ; fully-qualified # 🪴 E13.0 potted plant -1F332 ; fully-qualified # 🌲 E1.0 evergreen tree -1F333 ; fully-qualified # 🌳 E1.0 deciduous tree -1F334 ; fully-qualified # 🌴 E0.6 palm tree -1F335 ; fully-qualified # 🌵 E0.6 cactus -1F33E ; fully-qualified # 🌾 E0.6 sheaf of rice -1F33F ; fully-qualified # 🌿 E0.6 herb -2618 FE0F ; fully-qualified # ☘️ E1.0 shamrock -2618 ; unqualified # ☘ E1.0 shamrock -1F340 ; fully-qualified # 🍀 E0.6 four leaf clover -1F341 ; fully-qualified # 🍁 E0.6 maple leaf -1F342 ; fully-qualified # 🍂 E0.6 fallen leaf -1F343 ; fully-qualified # 🍃 E0.6 leaf fluttering in wind -1FAB9 ; fully-qualified # 🪹 E14.0 empty nest -1FABA ; fully-qualified # 🪺 E14.0 nest with eggs -# -## Animals & Nature subtotal: 151 -## Animals & Nature subtotal: 151 w/o modifiers -# -## group: Food & Drink -# -## subgroup: food-fruit -1F347 ; fully-qualified # 🍇 E0.6 grapes -1F348 ; fully-qualified # 🍈 E0.6 melon -1F349 ; fully-qualified # 🍉 E0.6 watermelon -1F34A ; fully-qualified # 🍊 E0.6 tangerine -1F34B ; fully-qualified # 🍋 E1.0 lemon -1F34C ; fully-qualified # 🍌 E0.6 banana -1F34D ; fully-qualified # 🍍 E0.6 pineapple -1F96D ; fully-qualified # 🥭 E11.0 mango -1F34E ; fully-qualified # 🍎 E0.6 red apple -1F34F ; fully-qualified # 🍏 E0.6 green apple -1F350 ; fully-qualified # 🍐 E1.0 pear -1F351 ; fully-qualified # 🍑 E0.6 peach -1F352 ; fully-qualified # 🍒 E0.6 cherries -1F353 ; fully-qualified # 🍓 E0.6 strawberry -1FAD0 ; fully-qualified # 🫐 E13.0 blueberries -1F95D ; fully-qualified # 🥝 E3.0 kiwi fruit -1F345 ; fully-qualified # 🍅 E0.6 tomato -1FAD2 ; fully-qualified # 🫒 E13.0 olive -1F965 ; fully-qualified # 🥥 E5.0 coconut -# -## subgroup: food-vegetable -1F951 ; fully-qualified # 🥑 E3.0 avocado -1F346 ; fully-qualified # 🍆 E0.6 eggplant -1F954 ; fully-qualified # 🥔 E3.0 potato -1F955 ; fully-qualified # 🥕 E3.0 carrot -1F33D ; fully-qualified # 🌽 E0.6 ear of corn -1F336 FE0F ; fully-qualified # 🌶️ E0.7 hot pepper -1F336 ; unqualified # 🌶 E0.7 hot pepper -1FAD1 ; fully-qualified # 🫑 E13.0 bell pepper -1F952 ; fully-qualified # 🥒 E3.0 cucumber -1F96C ; fully-qualified # 🥬 E11.0 leafy green -1F966 ; fully-qualified # 🥦 E5.0 broccoli -1F9C4 ; fully-qualified # 🧄 E12.0 garlic -1F9C5 ; fully-qualified # 🧅 E12.0 onion -1F344 ; fully-qualified # 🍄 E0.6 mushroom -1F95C ; fully-qualified # 🥜 E3.0 peanuts -1FAD8 ; fully-qualified # 🫘 E14.0 beans -1F330 ; fully-qualified # 🌰 E0.6 chestnut -# -## subgroup: food-prepared -1F35E ; fully-qualified # 🍞 E0.6 bread -1F950 ; fully-qualified # 🥐 E3.0 croissant -1F956 ; fully-qualified # 🥖 E3.0 baguette bread -1FAD3 ; fully-qualified # 🫓 E13.0 flatbread -1F968 ; fully-qualified # 🥨 E5.0 pretzel -1F96F ; fully-qualified # 🥯 E11.0 bagel -1F95E ; fully-qualified # 🥞 E3.0 pancakes -1F9C7 ; fully-qualified # 🧇 E12.0 waffle -1F9C0 ; fully-qualified # 🧀 E1.0 cheese wedge -1F356 ; fully-qualified # 🍖 E0.6 meat on bone -1F357 ; fully-qualified # 🍗 E0.6 poultry leg -1F969 ; fully-qualified # 🥩 E5.0 cut of meat -1F953 ; fully-qualified # 🥓 E3.0 bacon -1F354 ; fully-qualified # 🍔 E0.6 hamburger -1F35F ; fully-qualified # 🍟 E0.6 french fries -1F355 ; fully-qualified # 🍕 E0.6 pizza -1F32D ; fully-qualified # 🌭 E1.0 hot dog -1F96A ; fully-qualified # 🥪 E5.0 sandwich -1F32E ; fully-qualified # 🌮 E1.0 taco -1F32F ; fully-qualified # 🌯 E1.0 burrito -1FAD4 ; fully-qualified # 🫔 E13.0 tamale -1F959 ; fully-qualified # 🥙 E3.0 stuffed flatbread -1F9C6 ; fully-qualified # 🧆 E12.0 falafel -1F95A ; fully-qualified # 🥚 E3.0 egg -1F373 ; fully-qualified # 🍳 E0.6 cooking -1F958 ; fully-qualified # 🥘 E3.0 shallow pan of food -1F372 ; fully-qualified # 🍲 E0.6 pot of food -1FAD5 ; fully-qualified # 🫕 E13.0 fondue -1F963 ; fully-qualified # 🥣 E5.0 bowl with spoon -1F957 ; fully-qualified # 🥗 E3.0 green salad -1F37F ; fully-qualified # 🍿 E1.0 popcorn -1F9C8 ; fully-qualified # 🧈 E12.0 butter -1F9C2 ; fully-qualified # 🧂 E11.0 salt -1F96B ; fully-qualified # 🥫 E5.0 canned food -# -## subgroup: food-asian -1F371 ; fully-qualified # 🍱 E0.6 bento box -1F358 ; fully-qualified # 🍘 E0.6 rice cracker -1F359 ; fully-qualified # 🍙 E0.6 rice ball -1F35A ; fully-qualified # 🍚 E0.6 cooked rice -1F35B ; fully-qualified # 🍛 E0.6 curry rice -1F35C ; fully-qualified # 🍜 E0.6 steaming bowl -1F35D ; fully-qualified # 🍝 E0.6 spaghetti -1F360 ; fully-qualified # 🍠 E0.6 roasted sweet potato -1F362 ; fully-qualified # 🍢 E0.6 oden -1F363 ; fully-qualified # 🍣 E0.6 sushi -1F364 ; fully-qualified # 🍤 E0.6 fried shrimp -1F365 ; fully-qualified # 🍥 E0.6 fish cake with swirl -1F96E ; fully-qualified # 🥮 E11.0 moon cake -1F361 ; fully-qualified # 🍡 E0.6 dango -1F95F ; fully-qualified # 🥟 E5.0 dumpling -1F960 ; fully-qualified # 🥠 E5.0 fortune cookie -1F961 ; fully-qualified # 🥡 E5.0 takeout box -# -## subgroup: food-marine -1F980 ; fully-qualified # 🦀 E1.0 crab -1F99E ; fully-qualified # 🦞 E11.0 lobster -1F990 ; fully-qualified # 🦐 E3.0 shrimp -1F991 ; fully-qualified # 🦑 E3.0 squid -1F9AA ; fully-qualified # 🦪 E12.0 oyster -# -## subgroup: food-sweet -1F366 ; fully-qualified # 🍦 E0.6 soft ice cream -1F367 ; fully-qualified # 🍧 E0.6 shaved ice -1F368 ; fully-qualified # 🍨 E0.6 ice cream -1F369 ; fully-qualified # 🍩 E0.6 doughnut -1F36A ; fully-qualified # 🍪 E0.6 cookie -1F382 ; fully-qualified # 🎂 E0.6 birthday cake -1F370 ; fully-qualified # 🍰 E0.6 shortcake -1F9C1 ; fully-qualified # 🧁 E11.0 cupcake -1F967 ; fully-qualified # 🥧 E5.0 pie -1F36B ; fully-qualified # 🍫 E0.6 chocolate bar -1F36C ; fully-qualified # 🍬 E0.6 candy -1F36D ; fully-qualified # 🍭 E0.6 lollipop -1F36E ; fully-qualified # 🍮 E0.6 custard -1F36F ; fully-qualified # 🍯 E0.6 honey pot -# -## subgroup: drink -1F37C ; fully-qualified # 🍼 E1.0 baby bottle -1F95B ; fully-qualified # 🥛 E3.0 glass of milk -2615 ; fully-qualified # ☕ E0.6 hot beverage -1FAD6 ; fully-qualified # 🫖 E13.0 teapot -1F375 ; fully-qualified # 🍵 E0.6 teacup without handle -1F376 ; fully-qualified # 🍶 E0.6 sake -1F37E ; fully-qualified # 🍾 E1.0 bottle with popping cork -1F377 ; fully-qualified # 🍷 E0.6 wine glass -1F378 ; fully-qualified # 🍸 E0.6 cocktail glass -1F379 ; fully-qualified # 🍹 E0.6 tropical drink -1F37A ; fully-qualified # 🍺 E0.6 beer mug -1F37B ; fully-qualified # 🍻 E0.6 clinking beer mugs -1F942 ; fully-qualified # 🥂 E3.0 clinking glasses -1F943 ; fully-qualified # 🥃 E3.0 tumbler glass -1FAD7 ; fully-qualified # 🫗 E14.0 pouring liquid -1F964 ; fully-qualified # 🥤 E5.0 cup with straw -1F9CB ; fully-qualified # 🧋 E13.0 bubble tea -1F9C3 ; fully-qualified # 🧃 E12.0 beverage box -1F9C9 ; fully-qualified # 🧉 E12.0 mate -1F9CA ; fully-qualified # 🧊 E12.0 ice -# -## subgroup: dishware -#1F962 ; fully-qualified # 🥢 E5.0 chopsticks -#1F37D FE0F ; fully-qualified # 🍽️ E0.7 fork and knife with plate -#1F37D ; unqualified # 🍽 E0.7 fork and knife with plate -#1F374 ; fully-qualified # 🍴 E0.6 fork and knife -#1F944 ; fully-qualified # 🥄 E3.0 spoon -#1F52A ; fully-qualified # 🔪 E0.6 kitchen knife -#1FAD9 ; fully-qualified # 🫙 E14.0 jar -#1F3FA ; fully-qualified # 🏺 E1.0 amphora -# -## Food & Drink subtotal: 134 -## Food & Drink subtotal: 134 w/o modifiers -# -## group: Travel & Places -# -## subgroup: place-map -#1F30D ; fully-qualified # 🌍 E0.7 globe showing Europe-Africa -#1F30E ; fully-qualified # 🌎 E0.7 globe showing Americas -#1F30F ; fully-qualified # 🌏 E0.6 globe showing Asia-Australia -#1F310 ; fully-qualified # 🌐 E1.0 globe with meridians -#1F5FA FE0F ; fully-qualified # 🗺️ E0.7 world map -#1F5FA ; unqualified # 🗺 E0.7 world map -#1F5FE ; fully-qualified # 🗾 E0.6 map of Japan -#1F9ED ; fully-qualified # 🧭 E11.0 compass -# -## subgroup: place-geographic -#1F3D4 FE0F ; fully-qualified # 🏔️ E0.7 snow-capped mountain -#1F3D4 ; unqualified # 🏔 E0.7 snow-capped mountain -#26F0 FE0F ; fully-qualified # ⛰️ E0.7 mountain -#26F0 ; unqualified # ⛰ E0.7 mountain -#1F30B ; fully-qualified # 🌋 E0.6 volcano -#1F5FB ; fully-qualified # 🗻 E0.6 mount fuji -#1F3D5 FE0F ; fully-qualified # 🏕️ E0.7 camping -#1F3D5 ; unqualified # 🏕 E0.7 camping -#1F3D6 FE0F ; fully-qualified # 🏖️ E0.7 beach with umbrella -#1F3D6 ; unqualified # 🏖 E0.7 beach with umbrella -#1F3DC FE0F ; fully-qualified # 🏜️ E0.7 desert -#1F3DC ; unqualified # 🏜 E0.7 desert -#1F3DD FE0F ; fully-qualified # 🏝️ E0.7 desert island -#1F3DD ; unqualified # 🏝 E0.7 desert island -#1F3DE FE0F ; fully-qualified # 🏞️ E0.7 national park -#1F3DE ; unqualified # 🏞 E0.7 national park -# -## subgroup: place-building -#1F3DF FE0F ; fully-qualified # 🏟️ E0.7 stadium -#1F3DF ; unqualified # 🏟 E0.7 stadium -#1F3DB FE0F ; fully-qualified # 🏛️ E0.7 classical building -#1F3DB ; unqualified # 🏛 E0.7 classical building -#1F3D7 FE0F ; fully-qualified # 🏗️ E0.7 building construction -#1F3D7 ; unqualified # 🏗 E0.7 building construction -#1F9F1 ; fully-qualified # 🧱 E11.0 brick -#1FAA8 ; fully-qualified # 🪨 E13.0 rock -#1FAB5 ; fully-qualified # 🪵 E13.0 wood -#1F6D6 ; fully-qualified # 🛖 E13.0 hut -#1F3D8 FE0F ; fully-qualified # 🏘️ E0.7 houses -#1F3D8 ; unqualified # 🏘 E0.7 houses -#1F3DA FE0F ; fully-qualified # 🏚️ E0.7 derelict house -#1F3DA ; unqualified # 🏚 E0.7 derelict house -#1F3E0 ; fully-qualified # 🏠 E0.6 house -#1F3E1 ; fully-qualified # 🏡 E0.6 house with garden -#1F3E2 ; fully-qualified # 🏢 E0.6 office building -#1F3E3 ; fully-qualified # 🏣 E0.6 Japanese post office -#1F3E4 ; fully-qualified # 🏤 E1.0 post office -#1F3E5 ; fully-qualified # 🏥 E0.6 hospital -#1F3E6 ; fully-qualified # 🏦 E0.6 bank -#1F3E8 ; fully-qualified # 🏨 E0.6 hotel -#1F3E9 ; fully-qualified # 🏩 E0.6 love hotel -#1F3EA ; fully-qualified # 🏪 E0.6 convenience store -#1F3EB ; fully-qualified # 🏫 E0.6 school -#1F3EC ; fully-qualified # 🏬 E0.6 department store -#1F3ED ; fully-qualified # 🏭 E0.6 factory -#1F3EF ; fully-qualified # 🏯 E0.6 Japanese castle -#1F3F0 ; fully-qualified # 🏰 E0.6 castle -#1F492 ; fully-qualified # 💒 E0.6 wedding -#1F5FC ; fully-qualified # 🗼 E0.6 Tokyo tower -#1F5FD ; fully-qualified # 🗽 E0.6 Statue of Liberty -# -## subgroup: place-religious -#26EA ; fully-qualified # ⛪ E0.6 church -#1F54C ; fully-qualified # 🕌 E1.0 mosque -#1F6D5 ; fully-qualified # 🛕 E12.0 hindu temple -#1F54D ; fully-qualified # 🕍 E1.0 synagogue -#26E9 FE0F ; fully-qualified # ⛩️ E0.7 shinto shrine -#26E9 ; unqualified # ⛩ E0.7 shinto shrine -#1F54B ; fully-qualified # 🕋 E1.0 kaaba -# -## subgroup: place-other -#26F2 ; fully-qualified # ⛲ E0.6 fountain -#26FA ; fully-qualified # ⛺ E0.6 tent -#1F301 ; fully-qualified # 🌁 E0.6 foggy -#1F303 ; fully-qualified # 🌃 E0.6 night with stars -#1F3D9 FE0F ; fully-qualified # 🏙️ E0.7 cityscape -#1F3D9 ; unqualified # 🏙 E0.7 cityscape -#1F304 ; fully-qualified # 🌄 E0.6 sunrise over mountains -#1F305 ; fully-qualified # 🌅 E0.6 sunrise -#1F306 ; fully-qualified # 🌆 E0.6 cityscape at dusk -#1F307 ; fully-qualified # 🌇 E0.6 sunset -#1F309 ; fully-qualified # 🌉 E0.6 bridge at night -#2668 FE0F ; fully-qualified # ♨️ E0.6 hot springs -#2668 ; unqualified # ♨ E0.6 hot springs -#1F3A0 ; fully-qualified # 🎠 E0.6 carousel horse -#1F6DD ; fully-qualified # 🛝 E14.0 playground slide -#1F3A1 ; fully-qualified # 🎡 E0.6 ferris wheel -#1F3A2 ; fully-qualified # 🎢 E0.6 roller coaster -#1F488 ; fully-qualified # 💈 E0.6 barber pole -#1F3AA ; fully-qualified # 🎪 E0.6 circus tent -# -## subgroup: transport-ground -#1F682 ; fully-qualified # 🚂 E1.0 locomotive -#1F683 ; fully-qualified # 🚃 E0.6 railway car -#1F684 ; fully-qualified # 🚄 E0.6 high-speed train -#1F685 ; fully-qualified # 🚅 E0.6 bullet train -#1F686 ; fully-qualified # 🚆 E1.0 train -#1F687 ; fully-qualified # 🚇 E0.6 metro -#1F688 ; fully-qualified # 🚈 E1.0 light rail -#1F689 ; fully-qualified # 🚉 E0.6 station -#1F68A ; fully-qualified # 🚊 E1.0 tram -#1F69D ; fully-qualified # 🚝 E1.0 monorail -#1F69E ; fully-qualified # 🚞 E1.0 mountain railway -#1F68B ; fully-qualified # 🚋 E1.0 tram car -#1F68C ; fully-qualified # 🚌 E0.6 bus -#1F68D ; fully-qualified # 🚍 E0.7 oncoming bus -#1F68E ; fully-qualified # 🚎 E1.0 trolleybus -#1F690 ; fully-qualified # 🚐 E1.0 minibus -#1F691 ; fully-qualified # 🚑 E0.6 ambulance -#1F692 ; fully-qualified # 🚒 E0.6 fire engine -#1F693 ; fully-qualified # 🚓 E0.6 police car -#1F694 ; fully-qualified # 🚔 E0.7 oncoming police car -#1F695 ; fully-qualified # 🚕 E0.6 taxi -#1F696 ; fully-qualified # 🚖 E1.0 oncoming taxi -#1F697 ; fully-qualified # 🚗 E0.6 automobile -#1F698 ; fully-qualified # 🚘 E0.7 oncoming automobile -#1F699 ; fully-qualified # 🚙 E0.6 sport utility vehicle -#1F6FB ; fully-qualified # 🛻 E13.0 pickup truck -#1F69A ; fully-qualified # 🚚 E0.6 delivery truck -#1F69B ; fully-qualified # 🚛 E1.0 articulated lorry -#1F69C ; fully-qualified # 🚜 E1.0 tractor -#1F3CE FE0F ; fully-qualified # 🏎️ E0.7 racing car -#1F3CE ; unqualified # 🏎 E0.7 racing car -#1F3CD FE0F ; fully-qualified # 🏍️ E0.7 motorcycle -#1F3CD ; unqualified # 🏍 E0.7 motorcycle -#1F6F5 ; fully-qualified # 🛵 E3.0 motor scooter -#1F9BD ; fully-qualified # 🦽 E12.0 manual wheelchair -#1F9BC ; fully-qualified # 🦼 E12.0 motorized wheelchair -#1F6FA ; fully-qualified # 🛺 E12.0 auto rickshaw -#1F6B2 ; fully-qualified # 🚲 E0.6 bicycle -#1F6F4 ; fully-qualified # 🛴 E3.0 kick scooter -#1F6F9 ; fully-qualified # 🛹 E11.0 skateboard -#1F6FC ; fully-qualified # 🛼 E13.0 roller skate -#1F68F ; fully-qualified # 🚏 E0.6 bus stop -#1F6E3 FE0F ; fully-qualified # 🛣️ E0.7 motorway -#1F6E3 ; unqualified # 🛣 E0.7 motorway -#1F6E4 FE0F ; fully-qualified # 🛤️ E0.7 railway track -#1F6E4 ; unqualified # 🛤 E0.7 railway track -#1F6E2 FE0F ; fully-qualified # 🛢️ E0.7 oil drum -#1F6E2 ; unqualified # 🛢 E0.7 oil drum -#26FD ; fully-qualified # ⛽ E0.6 fuel pump -#1F6DE ; fully-qualified # 🛞 E14.0 wheel -#1F6A8 ; fully-qualified # 🚨 E0.6 police car light -#1F6A5 ; fully-qualified # 🚥 E0.6 horizontal traffic light -#1F6A6 ; fully-qualified # 🚦 E1.0 vertical traffic light -#1F6D1 ; fully-qualified # 🛑 E3.0 stop sign -#1F6A7 ; fully-qualified # 🚧 E0.6 construction -# -## subgroup: transport-water -#2693 ; fully-qualified # ⚓ E0.6 anchor -#1F6DF ; fully-qualified # 🛟 E14.0 ring buoy -#26F5 ; fully-qualified # ⛵ E0.6 sailboat -#1F6F6 ; fully-qualified # 🛶 E3.0 canoe -#1F6A4 ; fully-qualified # 🚤 E0.6 speedboat -#1F6F3 FE0F ; fully-qualified # 🛳️ E0.7 passenger ship -#1F6F3 ; unqualified # 🛳 E0.7 passenger ship -#26F4 FE0F ; fully-qualified # ⛴️ E0.7 ferry -#26F4 ; unqualified # ⛴ E0.7 ferry -#1F6E5 FE0F ; fully-qualified # 🛥️ E0.7 motor boat -#1F6E5 ; unqualified # 🛥 E0.7 motor boat -#1F6A2 ; fully-qualified # 🚢 E0.6 ship -# -## subgroup: transport-air -#2708 FE0F ; fully-qualified # ✈️ E0.6 airplane -#2708 ; unqualified # ✈ E0.6 airplane -#1F6E9 FE0F ; fully-qualified # 🛩️ E0.7 small airplane -#1F6E9 ; unqualified # 🛩 E0.7 small airplane -#1F6EB ; fully-qualified # 🛫 E1.0 airplane departure -#1F6EC ; fully-qualified # 🛬 E1.0 airplane arrival -#1FA82 ; fully-qualified # 🪂 E12.0 parachute -#1F4BA ; fully-qualified # 💺 E0.6 seat -#1F681 ; fully-qualified # 🚁 E1.0 helicopter -#1F69F ; fully-qualified # 🚟 E1.0 suspension railway -#1F6A0 ; fully-qualified # 🚠 E1.0 mountain cableway -#1F6A1 ; fully-qualified # 🚡 E1.0 aerial tramway -#1F6F0 FE0F ; fully-qualified # 🛰️ E0.7 satellite -#1F6F0 ; unqualified # 🛰 E0.7 satellite -#1F680 ; fully-qualified # 🚀 E0.6 rocket -#1F6F8 ; fully-qualified # 🛸 E5.0 flying saucer -# -## subgroup: hotel -#1F6CE FE0F ; fully-qualified # 🛎️ E0.7 bellhop bell -#1F6CE ; unqualified # 🛎 E0.7 bellhop bell -#1F9F3 ; fully-qualified # 🧳 E11.0 luggage -# -## subgroup: time -#231B ; fully-qualified # ⌛ E0.6 hourglass done -#23F3 ; fully-qualified # ⏳ E0.6 hourglass not done -#231A ; fully-qualified # ⌚ E0.6 watch -#23F0 ; fully-qualified # ⏰ E0.6 alarm clock -#23F1 FE0F ; fully-qualified # ⏱️ E1.0 stopwatch -#23F1 ; unqualified # ⏱ E1.0 stopwatch -#23F2 FE0F ; fully-qualified # ⏲️ E1.0 timer clock -#23F2 ; unqualified # ⏲ E1.0 timer clock -#1F570 FE0F ; fully-qualified # 🕰️ E0.7 mantelpiece clock -#1F570 ; unqualified # 🕰 E0.7 mantelpiece clock -#1F55B ; fully-qualified # 🕛 E0.6 twelve o’clock -#1F567 ; fully-qualified # 🕧 E0.7 twelve-thirty -#1F550 ; fully-qualified # 🕐 E0.6 one o’clock -#1F55C ; fully-qualified # 🕜 E0.7 one-thirty -#1F551 ; fully-qualified # 🕑 E0.6 two o’clock -#1F55D ; fully-qualified # 🕝 E0.7 two-thirty -#1F552 ; fully-qualified # 🕒 E0.6 three o’clock -#1F55E ; fully-qualified # 🕞 E0.7 three-thirty -#1F553 ; fully-qualified # 🕓 E0.6 four o’clock -#1F55F ; fully-qualified # 🕟 E0.7 four-thirty -#1F554 ; fully-qualified # 🕔 E0.6 five o’clock -#1F560 ; fully-qualified # 🕠 E0.7 five-thirty -#1F555 ; fully-qualified # 🕕 E0.6 six o’clock -#1F561 ; fully-qualified # 🕡 E0.7 six-thirty -#1F556 ; fully-qualified # 🕖 E0.6 seven o’clock -#1F562 ; fully-qualified # 🕢 E0.7 seven-thirty -#1F557 ; fully-qualified # 🕗 E0.6 eight o’clock -#1F563 ; fully-qualified # 🕣 E0.7 eight-thirty -#1F558 ; fully-qualified # 🕘 E0.6 nine o’clock -#1F564 ; fully-qualified # 🕤 E0.7 nine-thirty -#1F559 ; fully-qualified # 🕙 E0.6 ten o’clock -#1F565 ; fully-qualified # 🕥 E0.7 ten-thirty -#1F55A ; fully-qualified # 🕚 E0.6 eleven o’clock -#1F566 ; fully-qualified # 🕦 E0.7 eleven-thirty -# -## subgroup: sky & weather -#1F311 ; fully-qualified # 🌑 E0.6 new moon -#1F312 ; fully-qualified # 🌒 E1.0 waxing crescent moon -#1F313 ; fully-qualified # 🌓 E0.6 first quarter moon -#1F314 ; fully-qualified # 🌔 E0.6 waxing gibbous moon -#1F315 ; fully-qualified # 🌕 E0.6 full moon -#1F316 ; fully-qualified # 🌖 E1.0 waning gibbous moon -#1F317 ; fully-qualified # 🌗 E1.0 last quarter moon -#1F318 ; fully-qualified # 🌘 E1.0 waning crescent moon -#1F319 ; fully-qualified # 🌙 E0.6 crescent moon -#1F31A ; fully-qualified # 🌚 E1.0 new moon face -#1F31B ; fully-qualified # 🌛 E0.6 first quarter moon face -#1F31C ; fully-qualified # 🌜 E0.7 last quarter moon face -#1F321 FE0F ; fully-qualified # 🌡️ E0.7 thermometer -#1F321 ; unqualified # 🌡 E0.7 thermometer -#2600 FE0F ; fully-qualified # ☀️ E0.6 sun -#2600 ; unqualified # ☀ E0.6 sun -#1F31D ; fully-qualified # 🌝 E1.0 full moon face -#1F31E ; fully-qualified # 🌞 E1.0 sun with face -#1FA90 ; fully-qualified # 🪐 E12.0 ringed planet -#2B50 ; fully-qualified # ⭐ E0.6 star -#1F31F ; fully-qualified # 🌟 E0.6 glowing star -#1F320 ; fully-qualified # 🌠 E0.6 shooting star -#1F30C ; fully-qualified # 🌌 E0.6 milky way -#2601 FE0F ; fully-qualified # ☁️ E0.6 cloud -#2601 ; unqualified # ☁ E0.6 cloud -#26C5 ; fully-qualified # ⛅ E0.6 sun behind cloud -#26C8 FE0F ; fully-qualified # ⛈️ E0.7 cloud with lightning and rain -#26C8 ; unqualified # ⛈ E0.7 cloud with lightning and rain -#1F324 FE0F ; fully-qualified # 🌤️ E0.7 sun behind small cloud -#1F324 ; unqualified # 🌤 E0.7 sun behind small cloud -#1F325 FE0F ; fully-qualified # 🌥️ E0.7 sun behind large cloud -#1F325 ; unqualified # 🌥 E0.7 sun behind large cloud -#1F326 FE0F ; fully-qualified # 🌦️ E0.7 sun behind rain cloud -#1F326 ; unqualified # 🌦 E0.7 sun behind rain cloud -#1F327 FE0F ; fully-qualified # 🌧️ E0.7 cloud with rain -#1F327 ; unqualified # 🌧 E0.7 cloud with rain -#1F328 FE0F ; fully-qualified # 🌨️ E0.7 cloud with snow -#1F328 ; unqualified # 🌨 E0.7 cloud with snow -#1F329 FE0F ; fully-qualified # 🌩️ E0.7 cloud with lightning -#1F329 ; unqualified # 🌩 E0.7 cloud with lightning -#1F32A FE0F ; fully-qualified # 🌪️ E0.7 tornado -#1F32A ; unqualified # 🌪 E0.7 tornado -#1F32B FE0F ; fully-qualified # 🌫️ E0.7 fog -#1F32B ; unqualified # 🌫 E0.7 fog -#1F32C FE0F ; fully-qualified # 🌬️ E0.7 wind face -#1F32C ; unqualified # 🌬 E0.7 wind face -#1F300 ; fully-qualified # 🌀 E0.6 cyclone -#1F308 ; fully-qualified # 🌈 E0.6 rainbow -#1F302 ; fully-qualified # 🌂 E0.6 closed umbrella -#2602 FE0F ; fully-qualified # ☂️ E0.7 umbrella -#2602 ; unqualified # ☂ E0.7 umbrella -#2614 ; fully-qualified # ☔ E0.6 umbrella with rain drops -#26F1 FE0F ; fully-qualified # ⛱️ E0.7 umbrella on ground -#26F1 ; unqualified # ⛱ E0.7 umbrella on ground -#26A1 ; fully-qualified # ⚡ E0.6 high voltage -#2744 FE0F ; fully-qualified # ❄️ E0.6 snowflake -#2744 ; unqualified # ❄ E0.6 snowflake -#2603 FE0F ; fully-qualified # ☃️ E0.7 snowman -#2603 ; unqualified # ☃ E0.7 snowman -#26C4 ; fully-qualified # ⛄ E0.6 snowman without snow -#2604 FE0F ; fully-qualified # ☄️ E1.0 comet -#2604 ; unqualified # ☄ E1.0 comet -#1F525 ; fully-qualified # 🔥 E0.6 fire -#1F4A7 ; fully-qualified # 💧 E0.6 droplet -#1F30A ; fully-qualified # 🌊 E0.6 water wave -# -## Travel & Places subtotal: 267 -## Travel & Places subtotal: 267 w/o modifiers -# -## group: Activities -# -## subgroup: event -#1F383 ; fully-qualified # 🎃 E0.6 jack-o-lantern -#1F384 ; fully-qualified # 🎄 E0.6 Christmas tree -#1F386 ; fully-qualified # 🎆 E0.6 fireworks -#1F387 ; fully-qualified # 🎇 E0.6 sparkler -#1F9E8 ; fully-qualified # 🧨 E11.0 firecracker -#2728 ; fully-qualified # ✨ E0.6 sparkles -#1F388 ; fully-qualified # 🎈 E0.6 balloon -#1F389 ; fully-qualified # 🎉 E0.6 party popper -#1F38A ; fully-qualified # 🎊 E0.6 confetti ball -#1F38B ; fully-qualified # 🎋 E0.6 tanabata tree -#1F38D ; fully-qualified # 🎍 E0.6 pine decoration -#1F38E ; fully-qualified # 🎎 E0.6 Japanese dolls -#1F38F ; fully-qualified # 🎏 E0.6 carp streamer -#1F390 ; fully-qualified # 🎐 E0.6 wind chime -#1F391 ; fully-qualified # 🎑 E0.6 moon viewing ceremony -#1F9E7 ; fully-qualified # 🧧 E11.0 red envelope -#1F380 ; fully-qualified # 🎀 E0.6 ribbon -#1F381 ; fully-qualified # 🎁 E0.6 wrapped gift -#1F397 FE0F ; fully-qualified # 🎗️ E0.7 reminder ribbon -#1F397 ; unqualified # 🎗 E0.7 reminder ribbon -#1F39F FE0F ; fully-qualified # 🎟️ E0.7 admission tickets -#1F39F ; unqualified # 🎟 E0.7 admission tickets -#1F3AB ; fully-qualified # 🎫 E0.6 ticket -# -## subgroup: award-medal -#1F396 FE0F ; fully-qualified # 🎖️ E0.7 military medal -#1F396 ; unqualified # 🎖 E0.7 military medal -#1F3C6 ; fully-qualified # 🏆 E0.6 trophy -#1F3C5 ; fully-qualified # 🏅 E1.0 sports medal -#1F947 ; fully-qualified # 🥇 E3.0 1st place medal -#1F948 ; fully-qualified # 🥈 E3.0 2nd place medal -#1F949 ; fully-qualified # 🥉 E3.0 3rd place medal -# -## subgroup: sport -#26BD ; fully-qualified # ⚽ E0.6 soccer ball -#26BE ; fully-qualified # ⚾ E0.6 baseball -#1F94E ; fully-qualified # 🥎 E11.0 softball -#1F3C0 ; fully-qualified # 🏀 E0.6 basketball -#1F3D0 ; fully-qualified # 🏐 E1.0 volleyball -#1F3C8 ; fully-qualified # 🏈 E0.6 american football -#1F3C9 ; fully-qualified # 🏉 E1.0 rugby football -#1F3BE ; fully-qualified # 🎾 E0.6 tennis -#1F94F ; fully-qualified # 🥏 E11.0 flying disc -#1F3B3 ; fully-qualified # 🎳 E0.6 bowling -#1F3CF ; fully-qualified # 🏏 E1.0 cricket game -#1F3D1 ; fully-qualified # 🏑 E1.0 field hockey -#1F3D2 ; fully-qualified # 🏒 E1.0 ice hockey -#1F94D ; fully-qualified # 🥍 E11.0 lacrosse -#1F3D3 ; fully-qualified # 🏓 E1.0 ping pong -#1F3F8 ; fully-qualified # 🏸 E1.0 badminton -#1F94A ; fully-qualified # 🥊 E3.0 boxing glove -#1F94B ; fully-qualified # 🥋 E3.0 martial arts uniform -#1F945 ; fully-qualified # 🥅 E3.0 goal net -#26F3 ; fully-qualified # ⛳ E0.6 flag in hole -#26F8 FE0F ; fully-qualified # ⛸️ E0.7 ice skate -#26F8 ; unqualified # ⛸ E0.7 ice skate -#1F3A3 ; fully-qualified # 🎣 E0.6 fishing pole -#1F93F ; fully-qualified # 🤿 E12.0 diving mask -#1F3BD ; fully-qualified # 🎽 E0.6 running shirt -#1F3BF ; fully-qualified # 🎿 E0.6 skis -#1F6F7 ; fully-qualified # 🛷 E5.0 sled -#1F94C ; fully-qualified # 🥌 E5.0 curling stone -# -## subgroup: game -#1F3AF ; fully-qualified # 🎯 E0.6 bullseye -#1FA80 ; fully-qualified # 🪀 E12.0 yo-yo -#1FA81 ; fully-qualified # 🪁 E12.0 kite -#1F3B1 ; fully-qualified # 🎱 E0.6 pool 8 ball -#1F52E ; fully-qualified # 🔮 E0.6 crystal ball -#1FA84 ; fully-qualified # 🪄 E13.0 magic wand -#1F9FF ; fully-qualified # 🧿 E11.0 nazar amulet -#1FAAC ; fully-qualified # 🪬 E14.0 hamsa -#1F3AE ; fully-qualified # 🎮 E0.6 video game -#1F579 FE0F ; fully-qualified # 🕹️ E0.7 joystick -#1F579 ; unqualified # 🕹 E0.7 joystick -#1F3B0 ; fully-qualified # 🎰 E0.6 slot machine -#1F3B2 ; fully-qualified # 🎲 E0.6 game die -#1F9E9 ; fully-qualified # 🧩 E11.0 puzzle piece -#1F9F8 ; fully-qualified # 🧸 E11.0 teddy bear -#1FA85 ; fully-qualified # 🪅 E13.0 piñata -#1FAA9 ; fully-qualified # 🪩 E14.0 mirror ball -#1FA86 ; fully-qualified # 🪆 E13.0 nesting dolls -#2660 FE0F ; fully-qualified # ♠️ E0.6 spade suit -#2660 ; unqualified # ♠ E0.6 spade suit -#2665 FE0F ; fully-qualified # ♥️ E0.6 heart suit -#2665 ; unqualified # ♥ E0.6 heart suit -#2666 FE0F ; fully-qualified # ♦️ E0.6 diamond suit -#2666 ; unqualified # ♦ E0.6 diamond suit -#2663 FE0F ; fully-qualified # ♣️ E0.6 club suit -#2663 ; unqualified # ♣ E0.6 club suit -#265F FE0F ; fully-qualified # ♟️ E11.0 chess pawn -#265F ; unqualified # ♟ E11.0 chess pawn -#1F0CF ; fully-qualified # 🃏 E0.6 joker -#1F004 ; fully-qualified # 🀄 E0.6 mahjong red dragon -#1F3B4 ; fully-qualified # 🎴 E0.6 flower playing cards -# -## subgroup: arts & crafts -#1F3AD ; fully-qualified # 🎭 E0.6 performing arts -#1F5BC FE0F ; fully-qualified # 🖼️ E0.7 framed picture -#1F5BC ; unqualified # 🖼 E0.7 framed picture -#1F3A8 ; fully-qualified # 🎨 E0.6 artist palette -#1F9F5 ; fully-qualified # 🧵 E11.0 thread -#1FAA1 ; fully-qualified # 🪡 E13.0 sewing needle -#1F9F6 ; fully-qualified # 🧶 E11.0 yarn -#1FAA2 ; fully-qualified # 🪢 E13.0 knot -# -## Activities subtotal: 97 -## Activities subtotal: 97 w/o modifiers -# -## group: Objects -# -## subgroup: clothing -#1F453 ; fully-qualified # 👓 E0.6 glasses -#1F576 FE0F ; fully-qualified # 🕶️ E0.7 sunglasses -#1F576 ; unqualified # 🕶 E0.7 sunglasses -#1F97D ; fully-qualified # 🥽 E11.0 goggles -#1F97C ; fully-qualified # 🥼 E11.0 lab coat -#1F9BA ; fully-qualified # 🦺 E12.0 safety vest -#1F454 ; fully-qualified # 👔 E0.6 necktie -#1F455 ; fully-qualified # 👕 E0.6 t-shirt -#1F456 ; fully-qualified # 👖 E0.6 jeans -#1F9E3 ; fully-qualified # 🧣 E5.0 scarf -#1F9E4 ; fully-qualified # 🧤 E5.0 gloves -#1F9E5 ; fully-qualified # 🧥 E5.0 coat -#1F9E6 ; fully-qualified # 🧦 E5.0 socks -#1F457 ; fully-qualified # 👗 E0.6 dress -#1F458 ; fully-qualified # 👘 E0.6 kimono -#1F97B ; fully-qualified # 🥻 E12.0 sari -#1FA71 ; fully-qualified # 🩱 E12.0 one-piece swimsuit -#1FA72 ; fully-qualified # 🩲 E12.0 briefs -#1FA73 ; fully-qualified # 🩳 E12.0 shorts -#1F459 ; fully-qualified # 👙 E0.6 bikini -#1F45A ; fully-qualified # 👚 E0.6 woman’s clothes -#1F45B ; fully-qualified # 👛 E0.6 purse -#1F45C ; fully-qualified # 👜 E0.6 handbag -#1F45D ; fully-qualified # 👝 E0.6 clutch bag -#1F6CD FE0F ; fully-qualified # 🛍️ E0.7 shopping bags -#1F6CD ; unqualified # 🛍 E0.7 shopping bags -#1F392 ; fully-qualified # 🎒 E0.6 backpack -#1FA74 ; fully-qualified # 🩴 E13.0 thong sandal -#1F45E ; fully-qualified # 👞 E0.6 man’s shoe -#1F45F ; fully-qualified # 👟 E0.6 running shoe -#1F97E ; fully-qualified # 🥾 E11.0 hiking boot -#1F97F ; fully-qualified # 🥿 E11.0 flat shoe -#1F460 ; fully-qualified # 👠 E0.6 high-heeled shoe -#1F461 ; fully-qualified # 👡 E0.6 woman’s sandal -#1FA70 ; fully-qualified # 🩰 E12.0 ballet shoes -#1F462 ; fully-qualified # 👢 E0.6 woman’s boot -#1F451 ; fully-qualified # 👑 E0.6 crown -#1F452 ; fully-qualified # 👒 E0.6 woman’s hat -#1F3A9 ; fully-qualified # 🎩 E0.6 top hat -#1F393 ; fully-qualified # 🎓 E0.6 graduation cap -#1F9E2 ; fully-qualified # 🧢 E5.0 billed cap -#1FA96 ; fully-qualified # 🪖 E13.0 military helmet -#26D1 FE0F ; fully-qualified # ⛑️ E0.7 rescue worker’s helmet -#26D1 ; unqualified # ⛑ E0.7 rescue worker’s helmet -#1F4FF ; fully-qualified # 📿 E1.0 prayer beads -#1F484 ; fully-qualified # 💄 E0.6 lipstick -#1F48D ; fully-qualified # 💍 E0.6 ring -#1F48E ; fully-qualified # 💎 E0.6 gem stone -# -## subgroup: sound -#1F507 ; fully-qualified # 🔇 E1.0 muted speaker -#1F508 ; fully-qualified # 🔈 E0.7 speaker low volume -#1F509 ; fully-qualified # 🔉 E1.0 speaker medium volume -#1F50A ; fully-qualified # 🔊 E0.6 speaker high volume -#1F4E2 ; fully-qualified # 📢 E0.6 loudspeaker -#1F4E3 ; fully-qualified # 📣 E0.6 megaphone -#1F4EF ; fully-qualified # 📯 E1.0 postal horn -#1F514 ; fully-qualified # 🔔 E0.6 bell -#1F515 ; fully-qualified # 🔕 E1.0 bell with slash -# -## subgroup: music -#1F3BC ; fully-qualified # 🎼 E0.6 musical score -#1F3B5 ; fully-qualified # 🎵 E0.6 musical note -#1F3B6 ; fully-qualified # 🎶 E0.6 musical notes -#1F399 FE0F ; fully-qualified # 🎙️ E0.7 studio microphone -#1F399 ; unqualified # 🎙 E0.7 studio microphone -#1F39A FE0F ; fully-qualified # 🎚️ E0.7 level slider -#1F39A ; unqualified # 🎚 E0.7 level slider -#1F39B FE0F ; fully-qualified # 🎛️ E0.7 control knobs -#1F39B ; unqualified # 🎛 E0.7 control knobs -#1F3A4 ; fully-qualified # 🎤 E0.6 microphone -#1F3A7 ; fully-qualified # 🎧 E0.6 headphone -#1F4FB ; fully-qualified # 📻 E0.6 radio -# -## subgroup: musical-instrument -#1F3B7 ; fully-qualified # 🎷 E0.6 saxophone -#1FA97 ; fully-qualified # 🪗 E13.0 accordion -#1F3B8 ; fully-qualified # 🎸 E0.6 guitar -#1F3B9 ; fully-qualified # 🎹 E0.6 musical keyboard -#1F3BA ; fully-qualified # 🎺 E0.6 trumpet -#1F3BB ; fully-qualified # 🎻 E0.6 violin -#1FA95 ; fully-qualified # 🪕 E12.0 banjo -#1F941 ; fully-qualified # 🥁 E3.0 drum -#1FA98 ; fully-qualified # 🪘 E13.0 long drum -# -## subgroup: phone -#1F4F1 ; fully-qualified # 📱 E0.6 mobile phone -#1F4F2 ; fully-qualified # 📲 E0.6 mobile phone with arrow -#260E FE0F ; fully-qualified # ☎️ E0.6 telephone -#260E ; unqualified # ☎ E0.6 telephone -#1F4DE ; fully-qualified # 📞 E0.6 telephone receiver -#1F4DF ; fully-qualified # 📟 E0.6 pager -#1F4E0 ; fully-qualified # 📠 E0.6 fax machine -# -## subgroup: computer -#1F50B ; fully-qualified # 🔋 E0.6 battery -#1FAAB ; fully-qualified # 🪫 E14.0 low battery -#1F50C ; fully-qualified # 🔌 E0.6 electric plug -#1F4BB ; fully-qualified # 💻 E0.6 laptop -#1F5A5 FE0F ; fully-qualified # 🖥️ E0.7 desktop computer -#1F5A5 ; unqualified # 🖥 E0.7 desktop computer -#1F5A8 FE0F ; fully-qualified # 🖨️ E0.7 printer -#1F5A8 ; unqualified # 🖨 E0.7 printer -#2328 FE0F ; fully-qualified # ⌨️ E1.0 keyboard -#2328 ; unqualified # ⌨ E1.0 keyboard -#1F5B1 FE0F ; fully-qualified # 🖱️ E0.7 computer mouse -#1F5B1 ; unqualified # 🖱 E0.7 computer mouse -#1F5B2 FE0F ; fully-qualified # 🖲️ E0.7 trackball -#1F5B2 ; unqualified # 🖲 E0.7 trackball -#1F4BD ; fully-qualified # 💽 E0.6 computer disk -#1F4BE ; fully-qualified # 💾 E0.6 floppy disk -#1F4BF ; fully-qualified # 💿 E0.6 optical disk -#1F4C0 ; fully-qualified # 📀 E0.6 dvd -#1F9EE ; fully-qualified # 🧮 E11.0 abacus -# -## subgroup: light & video -#1F3A5 ; fully-qualified # 🎥 E0.6 movie camera -#1F39E FE0F ; fully-qualified # 🎞️ E0.7 film frames -#1F39E ; unqualified # 🎞 E0.7 film frames -#1F4FD FE0F ; fully-qualified # 📽️ E0.7 film projector -#1F4FD ; unqualified # 📽 E0.7 film projector -#1F3AC ; fully-qualified # 🎬 E0.6 clapper board -#1F4FA ; fully-qualified # 📺 E0.6 television -#1F4F7 ; fully-qualified # 📷 E0.6 camera -#1F4F8 ; fully-qualified # 📸 E1.0 camera with flash -#1F4F9 ; fully-qualified # 📹 E0.6 video camera -#1F4FC ; fully-qualified # 📼 E0.6 videocassette -#1F50D ; fully-qualified # 🔍 E0.6 magnifying glass tilted left -#1F50E ; fully-qualified # 🔎 E0.6 magnifying glass tilted right -#1F56F FE0F ; fully-qualified # 🕯️ E0.7 candle -#1F56F ; unqualified # 🕯 E0.7 candle -#1F4A1 ; fully-qualified # 💡 E0.6 light bulb -#1F526 ; fully-qualified # 🔦 E0.6 flashlight -#1F3EE ; fully-qualified # 🏮 E0.6 red paper lantern -#1FA94 ; fully-qualified # 🪔 E12.0 diya lamp -# -## subgroup: book-paper -#1F4D4 ; fully-qualified # 📔 E0.6 notebook with decorative cover -#1F4D5 ; fully-qualified # 📕 E0.6 closed book -#1F4D6 ; fully-qualified # 📖 E0.6 open book -#1F4D7 ; fully-qualified # 📗 E0.6 green book -#1F4D8 ; fully-qualified # 📘 E0.6 blue book -#1F4D9 ; fully-qualified # 📙 E0.6 orange book -#1F4DA ; fully-qualified # 📚 E0.6 books -#1F4D3 ; fully-qualified # 📓 E0.6 notebook -#1F4D2 ; fully-qualified # 📒 E0.6 ledger -#1F4C3 ; fully-qualified # 📃 E0.6 page with curl -#1F4DC ; fully-qualified # 📜 E0.6 scroll -#1F4C4 ; fully-qualified # 📄 E0.6 page facing up -#1F4F0 ; fully-qualified # 📰 E0.6 newspaper -#1F5DE FE0F ; fully-qualified # 🗞️ E0.7 rolled-up newspaper -#1F5DE ; unqualified # 🗞 E0.7 rolled-up newspaper -#1F4D1 ; fully-qualified # 📑 E0.6 bookmark tabs -#1F516 ; fully-qualified # 🔖 E0.6 bookmark -#1F3F7 FE0F ; fully-qualified # 🏷️ E0.7 label -#1F3F7 ; unqualified # 🏷 E0.7 label -# -## subgroup: money -#1F4B0 ; fully-qualified # 💰 E0.6 money bag -#1FA99 ; fully-qualified # 🪙 E13.0 coin -#1F4B4 ; fully-qualified # 💴 E0.6 yen banknote -#1F4B5 ; fully-qualified # 💵 E0.6 dollar banknote -#1F4B6 ; fully-qualified # 💶 E1.0 euro banknote -#1F4B7 ; fully-qualified # 💷 E1.0 pound banknote -#1F4B8 ; fully-qualified # 💸 E0.6 money with wings -#1F4B3 ; fully-qualified # 💳 E0.6 credit card -#1F9FE ; fully-qualified # 🧾 E11.0 receipt -#1F4B9 ; fully-qualified # 💹 E0.6 chart increasing with yen -# -## subgroup: mail -#2709 FE0F ; fully-qualified # ✉️ E0.6 envelope -#2709 ; unqualified # ✉ E0.6 envelope -#1F4E7 ; fully-qualified # 📧 E0.6 e-mail -#1F4E8 ; fully-qualified # 📨 E0.6 incoming envelope -#1F4E9 ; fully-qualified # 📩 E0.6 envelope with arrow -#1F4E4 ; fully-qualified # 📤 E0.6 outbox tray -#1F4E5 ; fully-qualified # 📥 E0.6 inbox tray -#1F4E6 ; fully-qualified # 📦 E0.6 package -#1F4EB ; fully-qualified # 📫 E0.6 closed mailbox with raised flag -#1F4EA ; fully-qualified # 📪 E0.6 closed mailbox with lowered flag -#1F4EC ; fully-qualified # 📬 E0.7 open mailbox with raised flag -#1F4ED ; fully-qualified # 📭 E0.7 open mailbox with lowered flag -#1F4EE ; fully-qualified # 📮 E0.6 postbox -#1F5F3 FE0F ; fully-qualified # 🗳️ E0.7 ballot box with ballot -#1F5F3 ; unqualified # 🗳 E0.7 ballot box with ballot -# -## subgroup: writing -#270F FE0F ; fully-qualified # ✏️ E0.6 pencil -#270F ; unqualified # ✏ E0.6 pencil -#2712 FE0F ; fully-qualified # ✒️ E0.6 black nib -#2712 ; unqualified # ✒ E0.6 black nib -#1F58B FE0F ; fully-qualified # 🖋️ E0.7 fountain pen -#1F58B ; unqualified # 🖋 E0.7 fountain pen -#1F58A FE0F ; fully-qualified # 🖊️ E0.7 pen -#1F58A ; unqualified # 🖊 E0.7 pen -#1F58C FE0F ; fully-qualified # 🖌️ E0.7 paintbrush -#1F58C ; unqualified # 🖌 E0.7 paintbrush -#1F58D FE0F ; fully-qualified # 🖍️ E0.7 crayon -#1F58D ; unqualified # 🖍 E0.7 crayon -#1F4DD ; fully-qualified # 📝 E0.6 memo -# -## subgroup: office -#1F4BC ; fully-qualified # 💼 E0.6 briefcase -#1F4C1 ; fully-qualified # 📁 E0.6 file folder -#1F4C2 ; fully-qualified # 📂 E0.6 open file folder -#1F5C2 FE0F ; fully-qualified # 🗂️ E0.7 card index dividers -#1F5C2 ; unqualified # 🗂 E0.7 card index dividers -#1F4C5 ; fully-qualified # 📅 E0.6 calendar -#1F4C6 ; fully-qualified # 📆 E0.6 tear-off calendar -#1F5D2 FE0F ; fully-qualified # 🗒️ E0.7 spiral notepad -#1F5D2 ; unqualified # 🗒 E0.7 spiral notepad -#1F5D3 FE0F ; fully-qualified # 🗓️ E0.7 spiral calendar -#1F5D3 ; unqualified # 🗓 E0.7 spiral calendar -#1F4C7 ; fully-qualified # 📇 E0.6 card index -#1F4C8 ; fully-qualified # 📈 E0.6 chart increasing -#1F4C9 ; fully-qualified # 📉 E0.6 chart decreasing -#1F4CA ; fully-qualified # 📊 E0.6 bar chart -#1F4CB ; fully-qualified # 📋 E0.6 clipboard -#1F4CC ; fully-qualified # 📌 E0.6 pushpin -#1F4CD ; fully-qualified # 📍 E0.6 round pushpin -#1F4CE ; fully-qualified # 📎 E0.6 paperclip -#1F587 FE0F ; fully-qualified # 🖇️ E0.7 linked paperclips -#1F587 ; unqualified # 🖇 E0.7 linked paperclips -#1F4CF ; fully-qualified # 📏 E0.6 straight ruler -#1F4D0 ; fully-qualified # 📐 E0.6 triangular ruler -#2702 FE0F ; fully-qualified # ✂️ E0.6 scissors -#2702 ; unqualified # ✂ E0.6 scissors -#1F5C3 FE0F ; fully-qualified # 🗃️ E0.7 card file box -#1F5C3 ; unqualified # 🗃 E0.7 card file box -#1F5C4 FE0F ; fully-qualified # 🗄️ E0.7 file cabinet -#1F5C4 ; unqualified # 🗄 E0.7 file cabinet -#1F5D1 FE0F ; fully-qualified # 🗑️ E0.7 wastebasket -#1F5D1 ; unqualified # 🗑 E0.7 wastebasket -# -## subgroup: lock -#1F512 ; fully-qualified # 🔒 E0.6 locked -#1F513 ; fully-qualified # 🔓 E0.6 unlocked -#1F50F ; fully-qualified # 🔏 E0.6 locked with pen -#1F510 ; fully-qualified # 🔐 E0.6 locked with key -#1F511 ; fully-qualified # 🔑 E0.6 key -#1F5DD FE0F ; fully-qualified # 🗝️ E0.7 old key -#1F5DD ; unqualified # 🗝 E0.7 old key -# -## subgroup: tool -#1F528 ; fully-qualified # 🔨 E0.6 hammer -#1FA93 ; fully-qualified # 🪓 E12.0 axe -#26CF FE0F ; fully-qualified # ⛏️ E0.7 pick -#26CF ; unqualified # ⛏ E0.7 pick -#2692 FE0F ; fully-qualified # ⚒️ E1.0 hammer and pick -#2692 ; unqualified # ⚒ E1.0 hammer and pick -#1F6E0 FE0F ; fully-qualified # 🛠️ E0.7 hammer and wrench -#1F6E0 ; unqualified # 🛠 E0.7 hammer and wrench -#1F5E1 FE0F ; fully-qualified # 🗡️ E0.7 dagger -#1F5E1 ; unqualified # 🗡 E0.7 dagger -#2694 FE0F ; fully-qualified # ⚔️ E1.0 crossed swords -#2694 ; unqualified # ⚔ E1.0 crossed swords -#1F52B ; fully-qualified # 🔫 E0.6 water pistol -#1FA83 ; fully-qualified # 🪃 E13.0 boomerang -#1F3F9 ; fully-qualified # 🏹 E1.0 bow and arrow -#1F6E1 FE0F ; fully-qualified # 🛡️ E0.7 shield -#1F6E1 ; unqualified # 🛡 E0.7 shield -#1FA9A ; fully-qualified # 🪚 E13.0 carpentry saw -#1F527 ; fully-qualified # 🔧 E0.6 wrench -#1FA9B ; fully-qualified # 🪛 E13.0 screwdriver -#1F529 ; fully-qualified # 🔩 E0.6 nut and bolt -#2699 FE0F ; fully-qualified # ⚙️ E1.0 gear -#2699 ; unqualified # ⚙ E1.0 gear -#1F5DC FE0F ; fully-qualified # 🗜️ E0.7 clamp -#1F5DC ; unqualified # 🗜 E0.7 clamp -#2696 FE0F ; fully-qualified # ⚖️ E1.0 balance scale -#2696 ; unqualified # ⚖ E1.0 balance scale -#1F9AF ; fully-qualified # 🦯 E12.0 white cane -#1F517 ; fully-qualified # 🔗 E0.6 link -#26D3 FE0F ; fully-qualified # ⛓️ E0.7 chains -#26D3 ; unqualified # ⛓ E0.7 chains -#1FA9D ; fully-qualified # 🪝 E13.0 hook -#1F9F0 ; fully-qualified # 🧰 E11.0 toolbox -#1F9F2 ; fully-qualified # 🧲 E11.0 magnet -#1FA9C ; fully-qualified # 🪜 E13.0 ladder -# -## subgroup: science -#2697 FE0F ; fully-qualified # ⚗️ E1.0 alembic -#2697 ; unqualified # ⚗ E1.0 alembic -#1F9EA ; fully-qualified # 🧪 E11.0 test tube -#1F9EB ; fully-qualified # 🧫 E11.0 petri dish -#1F9EC ; fully-qualified # 🧬 E11.0 dna -#1F52C ; fully-qualified # 🔬 E1.0 microscope -#1F52D ; fully-qualified # 🔭 E1.0 telescope -#1F4E1 ; fully-qualified # 📡 E0.6 satellite antenna -# -## subgroup: medical -#1F489 ; fully-qualified # 💉 E0.6 syringe -#1FA78 ; fully-qualified # 🩸 E12.0 drop of blood -#1F48A ; fully-qualified # 💊 E0.6 pill -#1FA79 ; fully-qualified # 🩹 E12.0 adhesive bandage -#1FA7C ; fully-qualified # 🩼 E14.0 crutch -#1FA7A ; fully-qualified # 🩺 E12.0 stethoscope -#1FA7B ; fully-qualified # 🩻 E14.0 x-ray -# -## subgroup: household -#1F6AA ; fully-qualified # 🚪 E0.6 door -#1F6D7 ; fully-qualified # 🛗 E13.0 elevator -#1FA9E ; fully-qualified # 🪞 E13.0 mirror -#1FA9F ; fully-qualified # 🪟 E13.0 window -#1F6CF FE0F ; fully-qualified # 🛏️ E0.7 bed -#1F6CF ; unqualified # 🛏 E0.7 bed -#1F6CB FE0F ; fully-qualified # 🛋️ E0.7 couch and lamp -#1F6CB ; unqualified # 🛋 E0.7 couch and lamp -#1FA91 ; fully-qualified # 🪑 E12.0 chair -#1F6BD ; fully-qualified # 🚽 E0.6 toilet -#1FAA0 ; fully-qualified # 🪠 E13.0 plunger -#1F6BF ; fully-qualified # 🚿 E1.0 shower -#1F6C1 ; fully-qualified # 🛁 E1.0 bathtub -#1FAA4 ; fully-qualified # 🪤 E13.0 mouse trap -#1FA92 ; fully-qualified # 🪒 E12.0 razor -#1F9F4 ; fully-qualified # 🧴 E11.0 lotion bottle -#1F9F7 ; fully-qualified # 🧷 E11.0 safety pin -#1F9F9 ; fully-qualified # 🧹 E11.0 broom -#1F9FA ; fully-qualified # 🧺 E11.0 basket -#1F9FB ; fully-qualified # 🧻 E11.0 roll of paper -#1FAA3 ; fully-qualified # 🪣 E13.0 bucket -#1F9FC ; fully-qualified # 🧼 E11.0 soap -#1FAE7 ; fully-qualified # 🫧 E14.0 bubbles -#1FAA5 ; fully-qualified # 🪥 E13.0 toothbrush -#1F9FD ; fully-qualified # 🧽 E11.0 sponge -#1F9EF ; fully-qualified # 🧯 E11.0 fire extinguisher -#1F6D2 ; fully-qualified # 🛒 E3.0 shopping cart -# -## subgroup: other-object -#1F6AC ; fully-qualified # 🚬 E0.6 cigarette -#26B0 FE0F ; fully-qualified # ⚰️ E1.0 coffin -#26B0 ; unqualified # ⚰ E1.0 coffin -#1FAA6 ; fully-qualified # 🪦 E13.0 headstone -#26B1 FE0F ; fully-qualified # ⚱️ E1.0 funeral urn -#26B1 ; unqualified # ⚱ E1.0 funeral urn -#1F5FF ; fully-qualified # 🗿 E0.6 moai -#1FAA7 ; fully-qualified # 🪧 E13.0 placard -#1FAAA ; fully-qualified # 🪪 E14.0 identification card -# -## Objects subtotal: 304 -## Objects subtotal: 304 w/o modifiers -# -## group: Symbols -# -## subgroup: transport-sign -#1F3E7 ; fully-qualified # 🏧 E0.6 ATM sign -#1F6AE ; fully-qualified # 🚮 E1.0 litter in bin sign -#1F6B0 ; fully-qualified # 🚰 E1.0 potable water -#267F ; fully-qualified # ♿ E0.6 wheelchair symbol -#1F6B9 ; fully-qualified # 🚹 E0.6 men’s room -#1F6BA ; fully-qualified # 🚺 E0.6 women’s room -#1F6BB ; fully-qualified # 🚻 E0.6 restroom -#1F6BC ; fully-qualified # 🚼 E0.6 baby symbol -#1F6BE ; fully-qualified # 🚾 E0.6 water closet -#1F6C2 ; fully-qualified # 🛂 E1.0 passport control -#1F6C3 ; fully-qualified # 🛃 E1.0 customs -#1F6C4 ; fully-qualified # 🛄 E1.0 baggage claim -#1F6C5 ; fully-qualified # 🛅 E1.0 left luggage -# -## subgroup: warning -#26A0 FE0F ; fully-qualified # ⚠️ E0.6 warning -#26A0 ; unqualified # ⚠ E0.6 warning -#1F6B8 ; fully-qualified # 🚸 E1.0 children crossing -#26D4 ; fully-qualified # ⛔ E0.6 no entry -#1F6AB ; fully-qualified # 🚫 E0.6 prohibited -#1F6B3 ; fully-qualified # 🚳 E1.0 no bicycles -#1F6AD ; fully-qualified # 🚭 E0.6 no smoking -#1F6AF ; fully-qualified # 🚯 E1.0 no littering -#1F6B1 ; fully-qualified # 🚱 E1.0 non-potable water -#1F6B7 ; fully-qualified # 🚷 E1.0 no pedestrians -#1F4F5 ; fully-qualified # 📵 E1.0 no mobile phones -#1F51E ; fully-qualified # 🔞 E0.6 no one under eighteen -#2622 FE0F ; fully-qualified # ☢️ E1.0 radioactive -#2622 ; unqualified # ☢ E1.0 radioactive -#2623 FE0F ; fully-qualified # ☣️ E1.0 biohazard -#2623 ; unqualified # ☣ E1.0 biohazard -# -## subgroup: arrow -#2B06 FE0F ; fully-qualified # ⬆️ E0.6 up arrow -#2B06 ; unqualified # ⬆ E0.6 up arrow -#2197 FE0F ; fully-qualified # ↗️ E0.6 up-right arrow -#2197 ; unqualified # ↗ E0.6 up-right arrow -#27A1 FE0F ; fully-qualified # ➡️ E0.6 right arrow -#27A1 ; unqualified # ➡ E0.6 right arrow -#2198 FE0F ; fully-qualified # ↘️ E0.6 down-right arrow -#2198 ; unqualified # ↘ E0.6 down-right arrow -#2B07 FE0F ; fully-qualified # ⬇️ E0.6 down arrow -#2B07 ; unqualified # ⬇ E0.6 down arrow -#2199 FE0F ; fully-qualified # ↙️ E0.6 down-left arrow -#2199 ; unqualified # ↙ E0.6 down-left arrow -#2B05 FE0F ; fully-qualified # ⬅️ E0.6 left arrow -#2B05 ; unqualified # ⬅ E0.6 left arrow -#2196 FE0F ; fully-qualified # ↖️ E0.6 up-left arrow -#2196 ; unqualified # ↖ E0.6 up-left arrow -#2195 FE0F ; fully-qualified # ↕️ E0.6 up-down arrow -#2195 ; unqualified # ↕ E0.6 up-down arrow -#2194 FE0F ; fully-qualified # ↔️ E0.6 left-right arrow -#2194 ; unqualified # ↔ E0.6 left-right arrow -#21A9 FE0F ; fully-qualified # ↩️ E0.6 right arrow curving left -#21A9 ; unqualified # ↩ E0.6 right arrow curving left -#21AA FE0F ; fully-qualified # ↪️ E0.6 left arrow curving right -#21AA ; unqualified # ↪ E0.6 left arrow curving right -#2934 FE0F ; fully-qualified # ⤴️ E0.6 right arrow curving up -#2934 ; unqualified # ⤴ E0.6 right arrow curving up -#2935 FE0F ; fully-qualified # ⤵️ E0.6 right arrow curving down -#2935 ; unqualified # ⤵ E0.6 right arrow curving down -#1F503 ; fully-qualified # 🔃 E0.6 clockwise vertical arrows -#1F504 ; fully-qualified # 🔄 E1.0 counterclockwise arrows button -#1F519 ; fully-qualified # 🔙 E0.6 BACK arrow -#1F51A ; fully-qualified # 🔚 E0.6 END arrow -#1F51B ; fully-qualified # 🔛 E0.6 ON! arrow -#1F51C ; fully-qualified # 🔜 E0.6 SOON arrow -#1F51D ; fully-qualified # 🔝 E0.6 TOP arrow -# -## subgroup: religion -#1F6D0 ; fully-qualified # 🛐 E1.0 place of worship -#269B FE0F ; fully-qualified # ⚛️ E1.0 atom symbol -#269B ; unqualified # ⚛ E1.0 atom symbol -#1F549 FE0F ; fully-qualified # 🕉️ E0.7 om -#1F549 ; unqualified # 🕉 E0.7 om -#2721 FE0F ; fully-qualified # ✡️ E0.7 star of David -#2721 ; unqualified # ✡ E0.7 star of David -#2638 FE0F ; fully-qualified # ☸️ E0.7 wheel of dharma -#2638 ; unqualified # ☸ E0.7 wheel of dharma -#262F FE0F ; fully-qualified # ☯️ E0.7 yin yang -#262F ; unqualified # ☯ E0.7 yin yang -#271D FE0F ; fully-qualified # ✝️ E0.7 latin cross -#271D ; unqualified # ✝ E0.7 latin cross -#2626 FE0F ; fully-qualified # ☦️ E1.0 orthodox cross -#2626 ; unqualified # ☦ E1.0 orthodox cross -#262A FE0F ; fully-qualified # ☪️ E0.7 star and crescent -#262A ; unqualified # ☪ E0.7 star and crescent -#262E FE0F ; fully-qualified # ☮️ E1.0 peace symbol -#262E ; unqualified # ☮ E1.0 peace symbol -#1F54E ; fully-qualified # 🕎 E1.0 menorah -#1F52F ; fully-qualified # 🔯 E0.6 dotted six-pointed star -# -## subgroup: zodiac -#2648 ; fully-qualified # ♈ E0.6 Aries -#2649 ; fully-qualified # ♉ E0.6 Taurus -#264A ; fully-qualified # ♊ E0.6 Gemini -#264B ; fully-qualified # ♋ E0.6 Cancer -#264C ; fully-qualified # ♌ E0.6 Leo -#264D ; fully-qualified # ♍ E0.6 Virgo -#264E ; fully-qualified # ♎ E0.6 Libra -#264F ; fully-qualified # ♏ E0.6 Scorpio -#2650 ; fully-qualified # ♐ E0.6 Sagittarius -#2651 ; fully-qualified # ♑ E0.6 Capricorn -#2652 ; fully-qualified # ♒ E0.6 Aquarius -#2653 ; fully-qualified # ♓ E0.6 Pisces -#26CE ; fully-qualified # ⛎ E0.6 Ophiuchus -# -## subgroup: av-symbol -#1F500 ; fully-qualified # 🔀 E1.0 shuffle tracks button -#1F501 ; fully-qualified # 🔁 E1.0 repeat button -#1F502 ; fully-qualified # 🔂 E1.0 repeat single button -#25B6 FE0F ; fully-qualified # ▶️ E0.6 play button -#25B6 ; unqualified # ▶ E0.6 play button -#23E9 ; fully-qualified # ⏩ E0.6 fast-forward button -#23ED FE0F ; fully-qualified # ⏭️ E0.7 next track button -#23ED ; unqualified # ⏭ E0.7 next track button -#23EF FE0F ; fully-qualified # ⏯️ E1.0 play or pause button -#23EF ; unqualified # ⏯ E1.0 play or pause button -#25C0 FE0F ; fully-qualified # ◀️ E0.6 reverse button -#25C0 ; unqualified # ◀ E0.6 reverse button -#23EA ; fully-qualified # ⏪ E0.6 fast reverse button -#23EE FE0F ; fully-qualified # ⏮️ E0.7 last track button -#23EE ; unqualified # ⏮ E0.7 last track button -#1F53C ; fully-qualified # 🔼 E0.6 upwards button -#23EB ; fully-qualified # ⏫ E0.6 fast up button -#1F53D ; fully-qualified # 🔽 E0.6 downwards button -#23EC ; fully-qualified # ⏬ E0.6 fast down button -#23F8 FE0F ; fully-qualified # ⏸️ E0.7 pause button -#23F8 ; unqualified # ⏸ E0.7 pause button -#23F9 FE0F ; fully-qualified # ⏹️ E0.7 stop button -#23F9 ; unqualified # ⏹ E0.7 stop button -#23FA FE0F ; fully-qualified # ⏺️ E0.7 record button -#23FA ; unqualified # ⏺ E0.7 record button -#23CF FE0F ; fully-qualified # ⏏️ E1.0 eject button -#23CF ; unqualified # ⏏ E1.0 eject button -#1F3A6 ; fully-qualified # 🎦 E0.6 cinema -#1F505 ; fully-qualified # 🔅 E1.0 dim button -#1F506 ; fully-qualified # 🔆 E1.0 bright button -#1F4F6 ; fully-qualified # 📶 E0.6 antenna bars -#1F4F3 ; fully-qualified # 📳 E0.6 vibration mode -#1F4F4 ; fully-qualified # 📴 E0.6 mobile phone off -# -## subgroup: gender -#2640 FE0F ; fully-qualified # ♀️ E4.0 female sign -#2640 ; unqualified # ♀ E4.0 female sign -#2642 FE0F ; fully-qualified # ♂️ E4.0 male sign -#2642 ; unqualified # ♂ E4.0 male sign -#26A7 FE0F ; fully-qualified # ⚧️ E13.0 transgender symbol -#26A7 ; unqualified # ⚧ E13.0 transgender symbol -# -## subgroup: math -#2716 FE0F ; fully-qualified # ✖️ E0.6 multiply -#2716 ; unqualified # ✖ E0.6 multiply -#2795 ; fully-qualified # ➕ E0.6 plus -#2796 ; fully-qualified # ➖ E0.6 minus -#2797 ; fully-qualified # ➗ E0.6 divide -#1F7F0 ; fully-qualified # 🟰 E14.0 heavy equals sign -#267E FE0F ; fully-qualified # ♾️ E11.0 infinity -#267E ; unqualified # ♾ E11.0 infinity -# -## subgroup: punctuation -#203C FE0F ; fully-qualified # ‼️ E0.6 double exclamation mark -#203C ; unqualified # ‼ E0.6 double exclamation mark -#2049 FE0F ; fully-qualified # ⁉️ E0.6 exclamation question mark -#2049 ; unqualified # ⁉ E0.6 exclamation question mark -#2753 ; fully-qualified # ❓ E0.6 red question mark -#2754 ; fully-qualified # ❔ E0.6 white question mark -#2755 ; fully-qualified # ❕ E0.6 white exclamation mark -#2757 ; fully-qualified # ❗ E0.6 red exclamation mark -#3030 FE0F ; fully-qualified # 〰️ E0.6 wavy dash -#3030 ; unqualified # 〰 E0.6 wavy dash -# -## subgroup: currency -#1F4B1 ; fully-qualified # 💱 E0.6 currency exchange -#1F4B2 ; fully-qualified # 💲 E0.6 heavy dollar sign -# -## subgroup: other-symbol -#2695 FE0F ; fully-qualified # ⚕️ E4.0 medical symbol -#2695 ; unqualified # ⚕ E4.0 medical symbol -#267B FE0F ; fully-qualified # ♻️ E0.6 recycling symbol -#267B ; unqualified # ♻ E0.6 recycling symbol -#269C FE0F ; fully-qualified # ⚜️ E1.0 fleur-de-lis -#269C ; unqualified # ⚜ E1.0 fleur-de-lis -#1F531 ; fully-qualified # 🔱 E0.6 trident emblem -#1F4DB ; fully-qualified # 📛 E0.6 name badge -#1F530 ; fully-qualified # 🔰 E0.6 Japanese symbol for beginner -#2B55 ; fully-qualified # ⭕ E0.6 hollow red circle -#2705 ; fully-qualified # ✅ E0.6 check mark button -#2611 FE0F ; fully-qualified # ☑️ E0.6 check box with check -#2611 ; unqualified # ☑ E0.6 check box with check -#2714 FE0F ; fully-qualified # ✔️ E0.6 check mark -#2714 ; unqualified # ✔ E0.6 check mark -#274C ; fully-qualified # ❌ E0.6 cross mark -#274E ; fully-qualified # ❎ E0.6 cross mark button -#27B0 ; fully-qualified # ➰ E0.6 curly loop -#27BF ; fully-qualified # ➿ E1.0 double curly loop -#303D FE0F ; fully-qualified # 〽️ E0.6 part alternation mark -#303D ; unqualified # 〽 E0.6 part alternation mark -#2733 FE0F ; fully-qualified # ✳️ E0.6 eight-spoked asterisk -#2733 ; unqualified # ✳ E0.6 eight-spoked asterisk -#2734 FE0F ; fully-qualified # ✴️ E0.6 eight-pointed star -#2734 ; unqualified # ✴ E0.6 eight-pointed star -#2747 FE0F ; fully-qualified # ❇️ E0.6 sparkle -#2747 ; unqualified # ❇ E0.6 sparkle -#00A9 FE0F ; fully-qualified # ©️ E0.6 copyright -#00A9 ; unqualified # © E0.6 copyright -#00AE FE0F ; fully-qualified # ®️ E0.6 registered -#00AE ; unqualified # ® E0.6 registered -#2122 FE0F ; fully-qualified # ™️ E0.6 trade mark -#2122 ; unqualified # ™ E0.6 trade mark -# -## subgroup: keycap -#0023 FE0F 20E3 ; fully-qualified # #️⃣ E0.6 keycap: # -#0023 20E3 ; unqualified # #⃣ E0.6 keycap: # -#002A FE0F 20E3 ; fully-qualified # *️⃣ E2.0 keycap: * -#002A 20E3 ; unqualified # *⃣ E2.0 keycap: * -#0030 FE0F 20E3 ; fully-qualified # 0️⃣ E0.6 keycap: 0 -#0030 20E3 ; unqualified # 0⃣ E0.6 keycap: 0 -#0031 FE0F 20E3 ; fully-qualified # 1️⃣ E0.6 keycap: 1 -#0031 20E3 ; unqualified # 1⃣ E0.6 keycap: 1 -#0032 FE0F 20E3 ; fully-qualified # 2️⃣ E0.6 keycap: 2 -#0032 20E3 ; unqualified # 2⃣ E0.6 keycap: 2 -#0033 FE0F 20E3 ; fully-qualified # 3️⃣ E0.6 keycap: 3 -#0033 20E3 ; unqualified # 3⃣ E0.6 keycap: 3 -#0034 FE0F 20E3 ; fully-qualified # 4️⃣ E0.6 keycap: 4 -#0034 20E3 ; unqualified # 4⃣ E0.6 keycap: 4 -#0035 FE0F 20E3 ; fully-qualified # 5️⃣ E0.6 keycap: 5 -#0035 20E3 ; unqualified # 5⃣ E0.6 keycap: 5 -#0036 FE0F 20E3 ; fully-qualified # 6️⃣ E0.6 keycap: 6 -#0036 20E3 ; unqualified # 6⃣ E0.6 keycap: 6 -#0037 FE0F 20E3 ; fully-qualified # 7️⃣ E0.6 keycap: 7 -#0037 20E3 ; unqualified # 7⃣ E0.6 keycap: 7 -#0038 FE0F 20E3 ; fully-qualified # 8️⃣ E0.6 keycap: 8 -#0038 20E3 ; unqualified # 8⃣ E0.6 keycap: 8 -#0039 FE0F 20E3 ; fully-qualified # 9️⃣ E0.6 keycap: 9 -#0039 20E3 ; unqualified # 9⃣ E0.6 keycap: 9 -#1F51F ; fully-qualified # 🔟 E0.6 keycap: 10 -# -## subgroup: alphanum -#1F520 ; fully-qualified # 🔠 E0.6 input latin uppercase -#1F521 ; fully-qualified # 🔡 E0.6 input latin lowercase -#1F522 ; fully-qualified # 🔢 E0.6 input numbers -#1F523 ; fully-qualified # 🔣 E0.6 input symbols -#1F524 ; fully-qualified # 🔤 E0.6 input latin letters -#1F170 FE0F ; fully-qualified # 🅰️ E0.6 A button (blood type) -#1F170 ; unqualified # 🅰 E0.6 A button (blood type) -#1F18E ; fully-qualified # 🆎 E0.6 AB button (blood type) -#1F171 FE0F ; fully-qualified # 🅱️ E0.6 B button (blood type) -#1F171 ; unqualified # 🅱 E0.6 B button (blood type) -#1F191 ; fully-qualified # 🆑 E0.6 CL button -#1F192 ; fully-qualified # 🆒 E0.6 COOL button -#1F193 ; fully-qualified # 🆓 E0.6 FREE button -#2139 FE0F ; fully-qualified # ℹ️ E0.6 information -#2139 ; unqualified # ℹ E0.6 information -#1F194 ; fully-qualified # 🆔 E0.6 ID button -#24C2 FE0F ; fully-qualified # Ⓜ️ E0.6 circled M -#24C2 ; unqualified # Ⓜ E0.6 circled M -#1F195 ; fully-qualified # 🆕 E0.6 NEW button -#1F196 ; fully-qualified # 🆖 E0.6 NG button -#1F17E FE0F ; fully-qualified # 🅾️ E0.6 O button (blood type) -#1F17E ; unqualified # 🅾 E0.6 O button (blood type) -#1F197 ; fully-qualified # 🆗 E0.6 OK button -#1F17F FE0F ; fully-qualified # 🅿️ E0.6 P button -#1F17F ; unqualified # 🅿 E0.6 P button -#1F198 ; fully-qualified # 🆘 E0.6 SOS button -#1F199 ; fully-qualified # 🆙 E0.6 UP! button -#1F19A ; fully-qualified # 🆚 E0.6 VS button -#1F201 ; fully-qualified # 🈁 E0.6 Japanese “here” button -#1F202 FE0F ; fully-qualified # 🈂️ E0.6 Japanese “service charge” button -#1F202 ; unqualified # 🈂 E0.6 Japanese “service charge” button -#1F237 FE0F ; fully-qualified # 🈷️ E0.6 Japanese “monthly amount” button -#1F237 ; unqualified # 🈷 E0.6 Japanese “monthly amount” button -#1F236 ; fully-qualified # 🈶 E0.6 Japanese “not free of charge” button -#1F22F ; fully-qualified # 🈯 E0.6 Japanese “reserved” button -#1F250 ; fully-qualified # 🉐 E0.6 Japanese “bargain” button -#1F239 ; fully-qualified # 🈹 E0.6 Japanese “discount” button -#1F21A ; fully-qualified # 🈚 E0.6 Japanese “free of charge” button -#1F232 ; fully-qualified # 🈲 E0.6 Japanese “prohibited” button -#1F251 ; fully-qualified # 🉑 E0.6 Japanese “acceptable” button -#1F238 ; fully-qualified # 🈸 E0.6 Japanese “application” button -#1F234 ; fully-qualified # 🈴 E0.6 Japanese “passing grade” button -#1F233 ; fully-qualified # 🈳 E0.6 Japanese “vacancy” button -#3297 FE0F ; fully-qualified # ㊗️ E0.6 Japanese “congratulations” button -#3297 ; unqualified # ㊗ E0.6 Japanese “congratulations” button -#3299 FE0F ; fully-qualified # ㊙️ E0.6 Japanese “secret” button -#3299 ; unqualified # ㊙ E0.6 Japanese “secret” button -#1F23A ; fully-qualified # 🈺 E0.6 Japanese “open for business” button -#1F235 ; fully-qualified # 🈵 E0.6 Japanese “no vacancy” button -# -## subgroup: geometric -#1F534 ; fully-qualified # 🔴 E0.6 red circle -#1F7E0 ; fully-qualified # 🟠 E12.0 orange circle -#1F7E1 ; fully-qualified # 🟡 E12.0 yellow circle -#1F7E2 ; fully-qualified # 🟢 E12.0 green circle -#1F535 ; fully-qualified # 🔵 E0.6 blue circle -#1F7E3 ; fully-qualified # 🟣 E12.0 purple circle -#1F7E4 ; fully-qualified # 🟤 E12.0 brown circle -#26AB ; fully-qualified # ⚫ E0.6 black circle -#26AA ; fully-qualified # ⚪ E0.6 white circle -#1F7E5 ; fully-qualified # 🟥 E12.0 red square -#1F7E7 ; fully-qualified # 🟧 E12.0 orange square -#1F7E8 ; fully-qualified # 🟨 E12.0 yellow square -#1F7E9 ; fully-qualified # 🟩 E12.0 green square -#1F7E6 ; fully-qualified # 🟦 E12.0 blue square -#1F7EA ; fully-qualified # 🟪 E12.0 purple square -#1F7EB ; fully-qualified # 🟫 E12.0 brown square -#2B1B ; fully-qualified # ⬛ E0.6 black large square -#2B1C ; fully-qualified # ⬜ E0.6 white large square -#25FC FE0F ; fully-qualified # ◼️ E0.6 black medium square -#25FC ; unqualified # ◼ E0.6 black medium square -#25FB FE0F ; fully-qualified # ◻️ E0.6 white medium square -#25FB ; unqualified # ◻ E0.6 white medium square -#25FE ; fully-qualified # ◾ E0.6 black medium-small square -#25FD ; fully-qualified # ◽ E0.6 white medium-small square -#25AA FE0F ; fully-qualified # ▪️ E0.6 black small square -#25AA ; unqualified # ▪ E0.6 black small square -#25AB FE0F ; fully-qualified # ▫️ E0.6 white small square -#25AB ; unqualified # ▫ E0.6 white small square -#1F536 ; fully-qualified # 🔶 E0.6 large orange diamond -#1F537 ; fully-qualified # 🔷 E0.6 large blue diamond -#1F538 ; fully-qualified # 🔸 E0.6 small orange diamond -#1F539 ; fully-qualified # 🔹 E0.6 small blue diamond -#1F53A ; fully-qualified # 🔺 E0.6 red triangle pointed up -#1F53B ; fully-qualified # 🔻 E0.6 red triangle pointed down -#1F4A0 ; fully-qualified # 💠 E0.6 diamond with a dot -#1F518 ; fully-qualified # 🔘 E0.6 radio button -#1F533 ; fully-qualified # 🔳 E0.6 white square button -#1F532 ; fully-qualified # 🔲 E0.6 black square button -# -## Symbols subtotal: 302 -## Symbols subtotal: 302 w/o modifiers -# -## group: Flags -# -## subgroup: flag -#1F3C1 ; fully-qualified # 🏁 E0.6 chequered flag -#1F6A9 ; fully-qualified # 🚩 E0.6 triangular flag -#1F38C ; fully-qualified # 🎌 E0.6 crossed flags -#1F3F4 ; fully-qualified # 🏴 E1.0 black flag -#1F3F3 FE0F ; fully-qualified # 🏳️ E0.7 white flag -#1F3F3 ; unqualified # 🏳 E0.7 white flag -#1F3F3 FE0F 200D 1F308 ; fully-qualified # 🏳️‍🌈 E4.0 rainbow flag -#1F3F3 200D 1F308 ; unqualified # 🏳‍🌈 E4.0 rainbow flag -#1F3F3 FE0F 200D 26A7 FE0F ; fully-qualified # 🏳️‍⚧️ E13.0 transgender flag -#1F3F3 200D 26A7 FE0F ; unqualified # 🏳‍⚧️ E13.0 transgender flag -#1F3F3 FE0F 200D 26A7 ; unqualified # 🏳️‍⚧ E13.0 transgender flag -#1F3F3 200D 26A7 ; unqualified # 🏳‍⚧ E13.0 transgender flag -#1F3F4 200D 2620 FE0F ; fully-qualified # 🏴‍☠️ E11.0 pirate flag -#1F3F4 200D 2620 ; minimally-qualified # 🏴‍☠ E11.0 pirate flag -# -## subgroup: country-flag -#1F1E6 1F1E8 ; fully-qualified # 🇦🇨 E2.0 flag: Ascension Island -#1F1E6 1F1E9 ; fully-qualified # 🇦🇩 E2.0 flag: Andorra -#1F1E6 1F1EA ; fully-qualified # 🇦🇪 E2.0 flag: United Arab Emirates -#1F1E6 1F1EB ; fully-qualified # 🇦🇫 E2.0 flag: Afghanistan -#1F1E6 1F1EC ; fully-qualified # 🇦🇬 E2.0 flag: Antigua & Barbuda -#1F1E6 1F1EE ; fully-qualified # 🇦🇮 E2.0 flag: Anguilla -#1F1E6 1F1F1 ; fully-qualified # 🇦🇱 E2.0 flag: Albania -#1F1E6 1F1F2 ; fully-qualified # 🇦🇲 E2.0 flag: Armenia -#1F1E6 1F1F4 ; fully-qualified # 🇦🇴 E2.0 flag: Angola -#1F1E6 1F1F6 ; fully-qualified # 🇦🇶 E2.0 flag: Antarctica -#1F1E6 1F1F7 ; fully-qualified # 🇦🇷 E2.0 flag: Argentina -#1F1E6 1F1F8 ; fully-qualified # 🇦🇸 E2.0 flag: American Samoa -#1F1E6 1F1F9 ; fully-qualified # 🇦🇹 E2.0 flag: Austria -#1F1E6 1F1FA ; fully-qualified # 🇦🇺 E2.0 flag: Australia -#1F1E6 1F1FC ; fully-qualified # 🇦🇼 E2.0 flag: Aruba -#1F1E6 1F1FD ; fully-qualified # 🇦🇽 E2.0 flag: Åland Islands -#1F1E6 1F1FF ; fully-qualified # 🇦🇿 E2.0 flag: Azerbaijan -#1F1E7 1F1E6 ; fully-qualified # 🇧🇦 E2.0 flag: Bosnia & Herzegovina -#1F1E7 1F1E7 ; fully-qualified # 🇧🇧 E2.0 flag: Barbados -#1F1E7 1F1E9 ; fully-qualified # 🇧🇩 E2.0 flag: Bangladesh -#1F1E7 1F1EA ; fully-qualified # 🇧🇪 E2.0 flag: Belgium -#1F1E7 1F1EB ; fully-qualified # 🇧🇫 E2.0 flag: Burkina Faso -#1F1E7 1F1EC ; fully-qualified # 🇧🇬 E2.0 flag: Bulgaria -#1F1E7 1F1ED ; fully-qualified # 🇧🇭 E2.0 flag: Bahrain -#1F1E7 1F1EE ; fully-qualified # 🇧🇮 E2.0 flag: Burundi -#1F1E7 1F1EF ; fully-qualified # 🇧🇯 E2.0 flag: Benin -#1F1E7 1F1F1 ; fully-qualified # 🇧🇱 E2.0 flag: St. Barthélemy -#1F1E7 1F1F2 ; fully-qualified # 🇧🇲 E2.0 flag: Bermuda -#1F1E7 1F1F3 ; fully-qualified # 🇧🇳 E2.0 flag: Brunei -#1F1E7 1F1F4 ; fully-qualified # 🇧🇴 E2.0 flag: Bolivia -#1F1E7 1F1F6 ; fully-qualified # 🇧🇶 E2.0 flag: Caribbean Netherlands -#1F1E7 1F1F7 ; fully-qualified # 🇧🇷 E2.0 flag: Brazil -#1F1E7 1F1F8 ; fully-qualified # 🇧🇸 E2.0 flag: Bahamas -#1F1E7 1F1F9 ; fully-qualified # 🇧🇹 E2.0 flag: Bhutan -#1F1E7 1F1FB ; fully-qualified # 🇧🇻 E2.0 flag: Bouvet Island -#1F1E7 1F1FC ; fully-qualified # 🇧🇼 E2.0 flag: Botswana -#1F1E7 1F1FE ; fully-qualified # 🇧🇾 E2.0 flag: Belarus -#1F1E7 1F1FF ; fully-qualified # 🇧🇿 E2.0 flag: Belize -#1F1E8 1F1E6 ; fully-qualified # 🇨🇦 E2.0 flag: Canada -#1F1E8 1F1E8 ; fully-qualified # 🇨🇨 E2.0 flag: Cocos (Keeling) Islands -#1F1E8 1F1E9 ; fully-qualified # 🇨🇩 E2.0 flag: Congo - Kinshasa -#1F1E8 1F1EB ; fully-qualified # 🇨🇫 E2.0 flag: Central African Republic -#1F1E8 1F1EC ; fully-qualified # 🇨🇬 E2.0 flag: Congo - Brazzaville -#1F1E8 1F1ED ; fully-qualified # 🇨🇭 E2.0 flag: Switzerland -#1F1E8 1F1EE ; fully-qualified # 🇨🇮 E2.0 flag: Côte d’Ivoire -#1F1E8 1F1F0 ; fully-qualified # 🇨🇰 E2.0 flag: Cook Islands -#1F1E8 1F1F1 ; fully-qualified # 🇨🇱 E2.0 flag: Chile -#1F1E8 1F1F2 ; fully-qualified # 🇨🇲 E2.0 flag: Cameroon -#1F1E8 1F1F3 ; fully-qualified # 🇨🇳 E0.6 flag: China -#1F1E8 1F1F4 ; fully-qualified # 🇨🇴 E2.0 flag: Colombia -#1F1E8 1F1F5 ; fully-qualified # 🇨🇵 E2.0 flag: Clipperton Island -#1F1E8 1F1F7 ; fully-qualified # 🇨🇷 E2.0 flag: Costa Rica -#1F1E8 1F1FA ; fully-qualified # 🇨🇺 E2.0 flag: Cuba -#1F1E8 1F1FB ; fully-qualified # 🇨🇻 E2.0 flag: Cape Verde -#1F1E8 1F1FC ; fully-qualified # 🇨🇼 E2.0 flag: Curaçao -#1F1E8 1F1FD ; fully-qualified # 🇨🇽 E2.0 flag: Christmas Island -#1F1E8 1F1FE ; fully-qualified # 🇨🇾 E2.0 flag: Cyprus -#1F1E8 1F1FF ; fully-qualified # 🇨🇿 E2.0 flag: Czechia -#1F1E9 1F1EA ; fully-qualified # 🇩🇪 E0.6 flag: Germany -#1F1E9 1F1EC ; fully-qualified # 🇩🇬 E2.0 flag: Diego Garcia -#1F1E9 1F1EF ; fully-qualified # 🇩🇯 E2.0 flag: Djibouti -#1F1E9 1F1F0 ; fully-qualified # 🇩🇰 E2.0 flag: Denmark -#1F1E9 1F1F2 ; fully-qualified # 🇩🇲 E2.0 flag: Dominica -#1F1E9 1F1F4 ; fully-qualified # 🇩🇴 E2.0 flag: Dominican Republic -#1F1E9 1F1FF ; fully-qualified # 🇩🇿 E2.0 flag: Algeria -#1F1EA 1F1E6 ; fully-qualified # 🇪🇦 E2.0 flag: Ceuta & Melilla -#1F1EA 1F1E8 ; fully-qualified # 🇪🇨 E2.0 flag: Ecuador -#1F1EA 1F1EA ; fully-qualified # 🇪🇪 E2.0 flag: Estonia -#1F1EA 1F1EC ; fully-qualified # 🇪🇬 E2.0 flag: Egypt -#1F1EA 1F1ED ; fully-qualified # 🇪🇭 E2.0 flag: Western Sahara -#1F1EA 1F1F7 ; fully-qualified # 🇪🇷 E2.0 flag: Eritrea -#1F1EA 1F1F8 ; fully-qualified # 🇪🇸 E0.6 flag: Spain -#1F1EA 1F1F9 ; fully-qualified # 🇪🇹 E2.0 flag: Ethiopia -#1F1EA 1F1FA ; fully-qualified # 🇪🇺 E2.0 flag: European Union -#1F1EB 1F1EE ; fully-qualified # 🇫🇮 E2.0 flag: Finland -#1F1EB 1F1EF ; fully-qualified # 🇫🇯 E2.0 flag: Fiji -#1F1EB 1F1F0 ; fully-qualified # 🇫🇰 E2.0 flag: Falkland Islands -#1F1EB 1F1F2 ; fully-qualified # 🇫🇲 E2.0 flag: Micronesia -#1F1EB 1F1F4 ; fully-qualified # 🇫🇴 E2.0 flag: Faroe Islands -#1F1EB 1F1F7 ; fully-qualified # 🇫🇷 E0.6 flag: France -#1F1EC 1F1E6 ; fully-qualified # 🇬🇦 E2.0 flag: Gabon -#1F1EC 1F1E7 ; fully-qualified # 🇬🇧 E0.6 flag: United Kingdom -#1F1EC 1F1E9 ; fully-qualified # 🇬🇩 E2.0 flag: Grenada -#1F1EC 1F1EA ; fully-qualified # 🇬🇪 E2.0 flag: Georgia -#1F1EC 1F1EB ; fully-qualified # 🇬🇫 E2.0 flag: French Guiana -#1F1EC 1F1EC ; fully-qualified # 🇬🇬 E2.0 flag: Guernsey -#1F1EC 1F1ED ; fully-qualified # 🇬🇭 E2.0 flag: Ghana -#1F1EC 1F1EE ; fully-qualified # 🇬🇮 E2.0 flag: Gibraltar -#1F1EC 1F1F1 ; fully-qualified # 🇬🇱 E2.0 flag: Greenland -#1F1EC 1F1F2 ; fully-qualified # 🇬🇲 E2.0 flag: Gambia -#1F1EC 1F1F3 ; fully-qualified # 🇬🇳 E2.0 flag: Guinea -#1F1EC 1F1F5 ; fully-qualified # 🇬🇵 E2.0 flag: Guadeloupe -#1F1EC 1F1F6 ; fully-qualified # 🇬🇶 E2.0 flag: Equatorial Guinea -#1F1EC 1F1F7 ; fully-qualified # 🇬🇷 E2.0 flag: Greece -#1F1EC 1F1F8 ; fully-qualified # 🇬🇸 E2.0 flag: South Georgia & South Sandwich Islands -#1F1EC 1F1F9 ; fully-qualified # 🇬🇹 E2.0 flag: Guatemala -#1F1EC 1F1FA ; fully-qualified # 🇬🇺 E2.0 flag: Guam -#1F1EC 1F1FC ; fully-qualified # 🇬🇼 E2.0 flag: Guinea-Bissau -#1F1EC 1F1FE ; fully-qualified # 🇬🇾 E2.0 flag: Guyana -#1F1ED 1F1F0 ; fully-qualified # 🇭🇰 E2.0 flag: Hong Kong SAR China -#1F1ED 1F1F2 ; fully-qualified # 🇭🇲 E2.0 flag: Heard & McDonald Islands -#1F1ED 1F1F3 ; fully-qualified # 🇭🇳 E2.0 flag: Honduras -#1F1ED 1F1F7 ; fully-qualified # 🇭🇷 E2.0 flag: Croatia -#1F1ED 1F1F9 ; fully-qualified # 🇭🇹 E2.0 flag: Haiti -#1F1ED 1F1FA ; fully-qualified # 🇭🇺 E2.0 flag: Hungary -#1F1EE 1F1E8 ; fully-qualified # 🇮🇨 E2.0 flag: Canary Islands -#1F1EE 1F1E9 ; fully-qualified # 🇮🇩 E2.0 flag: Indonesia -#1F1EE 1F1EA ; fully-qualified # 🇮🇪 E2.0 flag: Ireland -#1F1EE 1F1F1 ; fully-qualified # 🇮🇱 E2.0 flag: Israel -#1F1EE 1F1F2 ; fully-qualified # 🇮🇲 E2.0 flag: Isle of Man -#1F1EE 1F1F3 ; fully-qualified # 🇮🇳 E2.0 flag: India -#1F1EE 1F1F4 ; fully-qualified # 🇮🇴 E2.0 flag: British Indian Ocean Territory -#1F1EE 1F1F6 ; fully-qualified # 🇮🇶 E2.0 flag: Iraq -#1F1EE 1F1F7 ; fully-qualified # 🇮🇷 E2.0 flag: Iran -#1F1EE 1F1F8 ; fully-qualified # 🇮🇸 E2.0 flag: Iceland -#1F1EE 1F1F9 ; fully-qualified # 🇮🇹 E0.6 flag: Italy -#1F1EF 1F1EA ; fully-qualified # 🇯🇪 E2.0 flag: Jersey -#1F1EF 1F1F2 ; fully-qualified # 🇯🇲 E2.0 flag: Jamaica -#1F1EF 1F1F4 ; fully-qualified # 🇯🇴 E2.0 flag: Jordan -#1F1EF 1F1F5 ; fully-qualified # 🇯🇵 E0.6 flag: Japan -#1F1F0 1F1EA ; fully-qualified # 🇰🇪 E2.0 flag: Kenya -#1F1F0 1F1EC ; fully-qualified # 🇰🇬 E2.0 flag: Kyrgyzstan -#1F1F0 1F1ED ; fully-qualified # 🇰🇭 E2.0 flag: Cambodia -#1F1F0 1F1EE ; fully-qualified # 🇰🇮 E2.0 flag: Kiribati -#1F1F0 1F1F2 ; fully-qualified # 🇰🇲 E2.0 flag: Comoros -#1F1F0 1F1F3 ; fully-qualified # 🇰🇳 E2.0 flag: St. Kitts & Nevis -#1F1F0 1F1F5 ; fully-qualified # 🇰🇵 E2.0 flag: North Korea -#1F1F0 1F1F7 ; fully-qualified # 🇰🇷 E0.6 flag: South Korea -#1F1F0 1F1FC ; fully-qualified # 🇰🇼 E2.0 flag: Kuwait -#1F1F0 1F1FE ; fully-qualified # 🇰🇾 E2.0 flag: Cayman Islands -#1F1F0 1F1FF ; fully-qualified # 🇰🇿 E2.0 flag: Kazakhstan -#1F1F1 1F1E6 ; fully-qualified # 🇱🇦 E2.0 flag: Laos -#1F1F1 1F1E7 ; fully-qualified # 🇱🇧 E2.0 flag: Lebanon -#1F1F1 1F1E8 ; fully-qualified # 🇱🇨 E2.0 flag: St. Lucia -#1F1F1 1F1EE ; fully-qualified # 🇱🇮 E2.0 flag: Liechtenstein -#1F1F1 1F1F0 ; fully-qualified # 🇱🇰 E2.0 flag: Sri Lanka -#1F1F1 1F1F7 ; fully-qualified # 🇱🇷 E2.0 flag: Liberia -#1F1F1 1F1F8 ; fully-qualified # 🇱🇸 E2.0 flag: Lesotho -#1F1F1 1F1F9 ; fully-qualified # 🇱🇹 E2.0 flag: Lithuania -#1F1F1 1F1FA ; fully-qualified # 🇱🇺 E2.0 flag: Luxembourg -#1F1F1 1F1FB ; fully-qualified # 🇱🇻 E2.0 flag: Latvia -#1F1F1 1F1FE ; fully-qualified # 🇱🇾 E2.0 flag: Libya -#1F1F2 1F1E6 ; fully-qualified # 🇲🇦 E2.0 flag: Morocco -#1F1F2 1F1E8 ; fully-qualified # 🇲🇨 E2.0 flag: Monaco -#1F1F2 1F1E9 ; fully-qualified # 🇲🇩 E2.0 flag: Moldova -#1F1F2 1F1EA ; fully-qualified # 🇲🇪 E2.0 flag: Montenegro -#1F1F2 1F1EB ; fully-qualified # 🇲🇫 E2.0 flag: St. Martin -#1F1F2 1F1EC ; fully-qualified # 🇲🇬 E2.0 flag: Madagascar -#1F1F2 1F1ED ; fully-qualified # 🇲🇭 E2.0 flag: Marshall Islands -#1F1F2 1F1F0 ; fully-qualified # 🇲🇰 E2.0 flag: North Macedonia -#1F1F2 1F1F1 ; fully-qualified # 🇲🇱 E2.0 flag: Mali -#1F1F2 1F1F2 ; fully-qualified # 🇲🇲 E2.0 flag: Myanmar (Burma) -#1F1F2 1F1F3 ; fully-qualified # 🇲🇳 E2.0 flag: Mongolia -#1F1F2 1F1F4 ; fully-qualified # 🇲🇴 E2.0 flag: Macao SAR China -#1F1F2 1F1F5 ; fully-qualified # 🇲🇵 E2.0 flag: Northern Mariana Islands -#1F1F2 1F1F6 ; fully-qualified # 🇲🇶 E2.0 flag: Martinique -#1F1F2 1F1F7 ; fully-qualified # 🇲🇷 E2.0 flag: Mauritania -#1F1F2 1F1F8 ; fully-qualified # 🇲🇸 E2.0 flag: Montserrat -#1F1F2 1F1F9 ; fully-qualified # 🇲🇹 E2.0 flag: Malta -#1F1F2 1F1FA ; fully-qualified # 🇲🇺 E2.0 flag: Mauritius -#1F1F2 1F1FB ; fully-qualified # 🇲🇻 E2.0 flag: Maldives -#1F1F2 1F1FC ; fully-qualified # 🇲🇼 E2.0 flag: Malawi -#1F1F2 1F1FD ; fully-qualified # 🇲🇽 E2.0 flag: Mexico -#1F1F2 1F1FE ; fully-qualified # 🇲🇾 E2.0 flag: Malaysia -#1F1F2 1F1FF ; fully-qualified # 🇲🇿 E2.0 flag: Mozambique -#1F1F3 1F1E6 ; fully-qualified # 🇳🇦 E2.0 flag: Namibia -#1F1F3 1F1E8 ; fully-qualified # 🇳🇨 E2.0 flag: New Caledonia -#1F1F3 1F1EA ; fully-qualified # 🇳🇪 E2.0 flag: Niger -#1F1F3 1F1EB ; fully-qualified # 🇳🇫 E2.0 flag: Norfolk Island -#1F1F3 1F1EC ; fully-qualified # 🇳🇬 E2.0 flag: Nigeria -#1F1F3 1F1EE ; fully-qualified # 🇳🇮 E2.0 flag: Nicaragua -#1F1F3 1F1F1 ; fully-qualified # 🇳🇱 E2.0 flag: Netherlands -#1F1F3 1F1F4 ; fully-qualified # 🇳🇴 E2.0 flag: Norway -#1F1F3 1F1F5 ; fully-qualified # 🇳🇵 E2.0 flag: Nepal -#1F1F3 1F1F7 ; fully-qualified # 🇳🇷 E2.0 flag: Nauru -#1F1F3 1F1FA ; fully-qualified # 🇳🇺 E2.0 flag: Niue -#1F1F3 1F1FF ; fully-qualified # 🇳🇿 E2.0 flag: New Zealand -#1F1F4 1F1F2 ; fully-qualified # 🇴🇲 E2.0 flag: Oman -#1F1F5 1F1E6 ; fully-qualified # 🇵🇦 E2.0 flag: Panama -#1F1F5 1F1EA ; fully-qualified # 🇵🇪 E2.0 flag: Peru -#1F1F5 1F1EB ; fully-qualified # 🇵🇫 E2.0 flag: French Polynesia -#1F1F5 1F1EC ; fully-qualified # 🇵🇬 E2.0 flag: Papua New Guinea -#1F1F5 1F1ED ; fully-qualified # 🇵🇭 E2.0 flag: Philippines -#1F1F5 1F1F0 ; fully-qualified # 🇵🇰 E2.0 flag: Pakistan -#1F1F5 1F1F1 ; fully-qualified # 🇵🇱 E2.0 flag: Poland -#1F1F5 1F1F2 ; fully-qualified # 🇵🇲 E2.0 flag: St. Pierre & Miquelon -#1F1F5 1F1F3 ; fully-qualified # 🇵🇳 E2.0 flag: Pitcairn Islands -#1F1F5 1F1F7 ; fully-qualified # 🇵🇷 E2.0 flag: Puerto Rico -#1F1F5 1F1F8 ; fully-qualified # 🇵🇸 E2.0 flag: Palestinian Territories -#1F1F5 1F1F9 ; fully-qualified # 🇵🇹 E2.0 flag: Portugal -#1F1F5 1F1FC ; fully-qualified # 🇵🇼 E2.0 flag: Palau -#1F1F5 1F1FE ; fully-qualified # 🇵🇾 E2.0 flag: Paraguay -#1F1F6 1F1E6 ; fully-qualified # 🇶🇦 E2.0 flag: Qatar -#1F1F7 1F1EA ; fully-qualified # 🇷🇪 E2.0 flag: Réunion -#1F1F7 1F1F4 ; fully-qualified # 🇷🇴 E2.0 flag: Romania -#1F1F7 1F1F8 ; fully-qualified # 🇷🇸 E2.0 flag: Serbia -#1F1F7 1F1FA ; fully-qualified # 🇷🇺 E0.6 flag: Russia -#1F1F7 1F1FC ; fully-qualified # 🇷🇼 E2.0 flag: Rwanda -#1F1F8 1F1E6 ; fully-qualified # 🇸🇦 E2.0 flag: Saudi Arabia -#1F1F8 1F1E7 ; fully-qualified # 🇸🇧 E2.0 flag: Solomon Islands -#1F1F8 1F1E8 ; fully-qualified # 🇸🇨 E2.0 flag: Seychelles -#1F1F8 1F1E9 ; fully-qualified # 🇸🇩 E2.0 flag: Sudan -#1F1F8 1F1EA ; fully-qualified # 🇸🇪 E2.0 flag: Sweden -#1F1F8 1F1EC ; fully-qualified # 🇸🇬 E2.0 flag: Singapore -#1F1F8 1F1ED ; fully-qualified # 🇸🇭 E2.0 flag: St. Helena -#1F1F8 1F1EE ; fully-qualified # 🇸🇮 E2.0 flag: Slovenia -#1F1F8 1F1EF ; fully-qualified # 🇸🇯 E2.0 flag: Svalbard & Jan Mayen -#1F1F8 1F1F0 ; fully-qualified # 🇸🇰 E2.0 flag: Slovakia -#1F1F8 1F1F1 ; fully-qualified # 🇸🇱 E2.0 flag: Sierra Leone -#1F1F8 1F1F2 ; fully-qualified # 🇸🇲 E2.0 flag: San Marino -#1F1F8 1F1F3 ; fully-qualified # 🇸🇳 E2.0 flag: Senegal -#1F1F8 1F1F4 ; fully-qualified # 🇸🇴 E2.0 flag: Somalia -#1F1F8 1F1F7 ; fully-qualified # 🇸🇷 E2.0 flag: Suriname -#1F1F8 1F1F8 ; fully-qualified # 🇸🇸 E2.0 flag: South Sudan -#1F1F8 1F1F9 ; fully-qualified # 🇸🇹 E2.0 flag: São Tomé & Príncipe -#1F1F8 1F1FB ; fully-qualified # 🇸🇻 E2.0 flag: El Salvador -#1F1F8 1F1FD ; fully-qualified # 🇸🇽 E2.0 flag: Sint Maarten -#1F1F8 1F1FE ; fully-qualified # 🇸🇾 E2.0 flag: Syria -#1F1F8 1F1FF ; fully-qualified # 🇸🇿 E2.0 flag: Eswatini -#1F1F9 1F1E6 ; fully-qualified # 🇹🇦 E2.0 flag: Tristan da Cunha -#1F1F9 1F1E8 ; fully-qualified # 🇹🇨 E2.0 flag: Turks & Caicos Islands -#1F1F9 1F1E9 ; fully-qualified # 🇹🇩 E2.0 flag: Chad -#1F1F9 1F1EB ; fully-qualified # 🇹🇫 E2.0 flag: French Southern Territories -#1F1F9 1F1EC ; fully-qualified # 🇹🇬 E2.0 flag: Togo -#1F1F9 1F1ED ; fully-qualified # 🇹🇭 E2.0 flag: Thailand -#1F1F9 1F1EF ; fully-qualified # 🇹🇯 E2.0 flag: Tajikistan -#1F1F9 1F1F0 ; fully-qualified # 🇹🇰 E2.0 flag: Tokelau -#1F1F9 1F1F1 ; fully-qualified # 🇹🇱 E2.0 flag: Timor-Leste -#1F1F9 1F1F2 ; fully-qualified # 🇹🇲 E2.0 flag: Turkmenistan -#1F1F9 1F1F3 ; fully-qualified # 🇹🇳 E2.0 flag: Tunisia -#1F1F9 1F1F4 ; fully-qualified # 🇹🇴 E2.0 flag: Tonga -#1F1F9 1F1F7 ; fully-qualified # 🇹🇷 E2.0 flag: Turkey -#1F1F9 1F1F9 ; fully-qualified # 🇹🇹 E2.0 flag: Trinidad & Tobago -#1F1F9 1F1FB ; fully-qualified # 🇹🇻 E2.0 flag: Tuvalu -#1F1F9 1F1FC ; fully-qualified # 🇹🇼 E2.0 flag: Taiwan -#1F1F9 1F1FF ; fully-qualified # 🇹🇿 E2.0 flag: Tanzania -#1F1FA 1F1E6 ; fully-qualified # 🇺🇦 E2.0 flag: Ukraine -#1F1FA 1F1EC ; fully-qualified # 🇺🇬 E2.0 flag: Uganda -#1F1FA 1F1F2 ; fully-qualified # 🇺🇲 E2.0 flag: U.S. Outlying Islands -#1F1FA 1F1F3 ; fully-qualified # 🇺🇳 E4.0 flag: United Nations -#1F1FA 1F1F8 ; fully-qualified # 🇺🇸 E0.6 flag: United States -#1F1FA 1F1FE ; fully-qualified # 🇺🇾 E2.0 flag: Uruguay -#1F1FA 1F1FF ; fully-qualified # 🇺🇿 E2.0 flag: Uzbekistan -#1F1FB 1F1E6 ; fully-qualified # 🇻🇦 E2.0 flag: Vatican City -#1F1FB 1F1E8 ; fully-qualified # 🇻🇨 E2.0 flag: St. Vincent & Grenadines -#1F1FB 1F1EA ; fully-qualified # 🇻🇪 E2.0 flag: Venezuela -#1F1FB 1F1EC ; fully-qualified # 🇻🇬 E2.0 flag: British Virgin Islands -#1F1FB 1F1EE ; fully-qualified # 🇻🇮 E2.0 flag: U.S. Virgin Islands -#1F1FB 1F1F3 ; fully-qualified # 🇻🇳 E2.0 flag: Vietnam -#1F1FB 1F1FA ; fully-qualified # 🇻🇺 E2.0 flag: Vanuatu -#1F1FC 1F1EB ; fully-qualified # 🇼🇫 E2.0 flag: Wallis & Futuna -#1F1FC 1F1F8 ; fully-qualified # 🇼🇸 E2.0 flag: Samoa -#1F1FD 1F1F0 ; fully-qualified # 🇽🇰 E2.0 flag: Kosovo -#1F1FE 1F1EA ; fully-qualified # 🇾🇪 E2.0 flag: Yemen -#1F1FE 1F1F9 ; fully-qualified # 🇾🇹 E2.0 flag: Mayotte -#1F1FF 1F1E6 ; fully-qualified # 🇿🇦 E2.0 flag: South Africa -#1F1FF 1F1F2 ; fully-qualified # 🇿🇲 E2.0 flag: Zambia -#1F1FF 1F1FC ; fully-qualified # 🇿🇼 E2.0 flag: Zimbabwe -# -## subgroup: subdivision-flag -#1F3F4 E0067 E0062 E0065 E006E E0067 E007F ; fully-qualified # 🏴󠁧󠁢󠁥󠁮󠁧󠁿 E5.0 flag: England -#1F3F4 E0067 E0062 E0073 E0063 E0074 E007F ; fully-qualified # 🏴󠁧󠁢󠁳󠁣󠁴󠁿 E5.0 flag: Scotland -#1F3F4 E0067 E0062 E0077 E006C E0073 E007F ; fully-qualified # 🏴󠁧󠁢󠁷󠁬󠁳󠁿 E5.0 flag: Wales -# -## Flags subtotal: 275 -## Flags subtotal: 275 w/o modifiers -# -## Status Counts -## fully-qualified : 3624 -## minimally-qualified : 817 -## unqualified : 252 -## component : 9 -# -##EOF diff --git a/src/content/register.md b/src/content/register.md deleted file mode 100644 index abda0b9..0000000 --- a/src/content/register.md +++ /dev/null @@ -1,19 +0,0 @@ -# Register - -
-
- - -
Who are u ?
-
-
- - -
We'll never share your email with anyone else.
-
-
- - -
- -
diff --git a/src/db.ml b/src/db.ml deleted file mode 100644 index eb865c8..0000000 --- a/src/db.ml +++ /dev/null @@ -1,48 +0,0 @@ -open Caqti_request.Infix - -let db_root = App.data_dir - -let () = - match Bos.OS.Dir.create (Fpath.v db_root) with - | Ok true -> Dream.log "created %s" db_root - | Ok false -> Dream.log "%s already exists" db_root - | Error (`Msg _) -> - Dream.warning (fun log -> log "error when creating %s" db_root) - -let db = Filename.concat db_root "permap.db" - -let db_uri = Format.sprintf "sqlite3://%s" db - -module Db = - (val Caqti_blocking.connect (Uri.of_string db_uri) |> Caqti_blocking.or_fail) - -let () = - let set_foreign_keys_on = - Caqti_type.(unit ->. unit) "PRAGMA foreign_keys = ON" - in - if Result.is_error (Db.exec set_foreign_keys_on ()) then - Dream.error (fun log -> log "can't set foreign_keys on") - -let () = - let query = - Caqti_type.(unit ->. unit) - "CREATE TABLE IF NOT EXISTS dream_session (id TEXT PRIMARY KEY, label \ - TEXT NOT NULL, expires_at REAL NOT NULL, payload TEXT NOT NULL)" - in - match Db.exec query () with - | Ok () -> () - | Error _e -> - Format.eprintf "db error@\n"; - exit 1 - -let unwrap_err = function - | Error e -> Error (Format.sprintf "db error: %s" (Caqti_error.show e)) - | Ok _ as ok -> ok - -let exec q v = Db.exec q v |> unwrap_err - -let find q v = Db.find q v |> unwrap_err - -let find_opt q v = Db.find_opt q v |> unwrap_err - -let collect_list q v = Db.collect_list q v |> unwrap_err diff --git a/src/db.mli b/src/db.mli deleted file mode 100644 index d9ed38b..0000000 --- a/src/db.mli +++ /dev/null @@ -1,13 +0,0 @@ -val exec : ('a, unit, [< `Zero ]) Caqti_request.t -> 'a -> (unit, string) result - -val find : ('a, 'b, [< `One ]) Caqti_request.t -> 'a -> ('b, string) result - -val find_opt : - ('a, 'b, [< `One | `Zero ]) Caqti_request.t - -> 'a - -> ('b option, string) result - -val collect_list : - ('a, 'b, [ `Many | `One | `Zero ]) Caqti_request.t - -> 'a - -> ('b list, string) result diff --git a/src/db_image.ml b/src/db_image.ml new file mode 100644 index 0000000..6201fd7 --- /dev/null +++ b/src/db_image.ml @@ -0,0 +1,162 @@ +(* TODO + - delete: error if does not exists + - better upload (insert/update) + - use join *) +open Syntax +open Err +open Types +open Caqti_request.Infix +open Caqti_type +open Caqti_db + +let post_tbl_prefix, post_key_reference = ("post_", "post(id)") + +let avatar_tbl_prefix, avatar_key_reference = ("user_", "user(user_id)") + +let () = + let mk_tables id_kind prefix reference_tbl = + [| Fmt.kstr (unit ->. unit) + "CREATE TABLE IF NOT EXISTS %simage_info (id %s, md5 TEXT, mime TEXT, \ + w INTEGER, h INTEGER, thumb_w INTEGER, thumb_h INTEGER, name TEXT, \ + alt TEXT, FOREIGN KEY(id) REFERENCES %s ON DELETE CASCADE)" + prefix id_kind reference_tbl + ; Fmt.kstr (unit ->. unit) + "CREATE TABLE IF NOT EXISTS %simage_content (id %s, content TEXT, \ + FOREIGN KEY(id) REFERENCES %s ON DELETE CASCADE)" + prefix id_kind reference_tbl + ; Fmt.kstr (unit ->. unit) + "CREATE TABLE IF NOT EXISTS %simage_thumbnail (id %s, content TEXT, \ + FOREIGN KEY(id) REFERENCES %s ON DELETE CASCADE)" + prefix id_kind reference_tbl + |] + in + let tables = + Array.concat + [ mk_tables "INTEGER" post_tbl_prefix post_key_reference + ; mk_tables "TEXT" avatar_tbl_prefix avatar_key_reference + ] + in + Array.iter (fun query -> Db.exec_unsafe query ()) tables + +module type T = sig + type t + + val info : t -> img_info option result + + val data : t -> string option result + + val thumbnail_data : t -> string option result + + val delete : t -> unit result + + val upload : t -> img -> unit result +end + +module type A = sig + type t + + val caqti_t : t Caqti_type.t + + val prefix : string + + val replace_image : bool +end + +(* Make(A) => T *) +module Make (M : A) = struct + include M + + let upload_info = + Db.exec + @@ Fmt.kstr + (t9 caqti_t string string int int int int string string ->. unit) + "INSERT INTO %simage_info VALUES (?,?,?,?,?,?,?,?,?)" prefix + + let upload_data = + Db.exec + @@ Fmt.kstr + (t2 caqti_t string ->. unit) + "INSERT INTO %simage_content VALUES (?,?)" prefix + + let upload_thumbnail_data = + Db.exec + @@ Fmt.kstr + (t2 caqti_t string ->. unit) + "INSERT INTO %simage_thumbnail VALUES (?,?)" prefix + + let info = + let f = + Db.find_opt + (Fmt.kstr + (caqti_t ->? t9 caqti_t string string int int int int string string) + "SELECT * FROM %simage_info WHERE id=?" prefix ) + in + fun id -> + let+ opt = f id in + Option.map + (fun (_id, md5, mime, w, h, thumb_w, thumb_h, name, alt) -> + { md5; mime; w; h; thumb_w; thumb_h; name; alt } ) + opt + + let data = + Db.find_opt + @@ Fmt.kstr (caqti_t ->? string) + "SELECT content FROM %simage_content WHERE id=?" prefix + + let thumbnail_data = + Db.find_opt + @@ Fmt.kstr (caqti_t ->? string) + "SELECT content FROM %simage_thumbnail WHERE id=?" prefix + + let delete_info = + Db.exec + @@ Fmt.kstr (caqti_t ->. unit) "DELETE FROM %simage_info WHERE id=?" prefix + + let delete_content = + Db.exec + @@ Fmt.kstr (caqti_t ->. unit) "DELETE FROM %simage_content WHERE id=?" + prefix + + let delete_thumbnail = + Db.exec + @@ Fmt.kstr (caqti_t ->. unit) "DELETE FROM %simage_thumbnail WHERE id=?" + prefix + + (* TODO error if does not exists *) + let delete id = + let* () = delete_info id in + let* () = delete_content id in + delete_thumbnail id + + let upload id image = + (* TODO do something like + https://stackoverflow.com/questions/418898/upsert-not-insert-or-replace/4330694#4330694 + instead of deleting then re-inserting to update(or insert on first time).. *) + let* () = if replace_image then delete id else Ok () in + (* -- *) + let { info; data; thumbnail_data } = image in + let { md5; mime; w; h; thumb_w; thumb_h; name; alt } = info in + let* () = upload_info (id, md5, mime, w, h, thumb_w, thumb_h, name, alt) in + let* () = upload_data (id, data) in + upload_thumbnail_data (id, thumbnail_data) +end + +module P = Make (struct + type t = int + + let caqti_t = Caqti_type.int + + let prefix = post_tbl_prefix + + let replace_image = false +end) + +module U = Make (struct + type t = string + + let caqti_t = Caqti_type.string + + let prefix = avatar_tbl_prefix + + let replace_image = true +end) diff --git a/src/db_image.mli b/src/db_image.mli new file mode 100644 index 0000000..d436cbe --- /dev/null +++ b/src/db_image.mli @@ -0,0 +1,28 @@ +(* TODO sql + - use JOIN for getting image info instead *) +(* no sql transaction are started in this module + two kind of images: post images and user avatars + only difference is avatar image are unique and "persist". + kept in different tables + :^) ~ horrible functor just to factorize code + *) +open Err +open Types + +module type T = sig + type t + + val info : t -> img_info option result + + val data : t -> string option result + + val thumbnail_data : t -> string option result + + val delete : t -> unit result + + val upload : t -> img -> unit result +end + +module P : T with type t = int + +module U : T with type t = string diff --git a/src/db_post.ml b/src/db_post.ml new file mode 100644 index 0000000..349e67c --- /dev/null +++ b/src/db_post.ml @@ -0,0 +1,269 @@ +(* TODO sql + - add index on thread_reply/post_reply + - have a table auto-updated for bump_status + - JOIN : + - use join for image_info + - get all thread reply_l in one join + queries for backlinks + *) +open Syntax +open Err +open Types +open Caqti_request.Infix +open Caqti_type +open Caqti_db + +let () = + let tables = + [| (unit ->. unit) + "CREATE TABLE IF NOT EXISTS post (id INTEGER PRIMARY KEY \ + AUTOINCREMENT, user_id TEXT, date FLOAT, comment TEXT, FOREIGN \ + KEY(user_id) REFERENCES user(user_id) ON DELETE CASCADE)" + ; (unit ->. unit) + "CREATE TABLE IF NOT EXISTS thread (thread_id INTEGER, lat FLOAT, lng \ + FLOAT, subject TEXT, reply_count INTEGER, last_reply_date FLOAT, \ + FOREIGN KEY(thread_id) REFERENCES post(id) ON DELETE CASCADE)" + ; (unit ->. unit) + "CREATE TABLE IF NOT EXISTS thread_reply (thread_id INTEGER, post_id \ + INTEGER, FOREIGN KEY(thread_id) REFERENCES post(id) ON DELETE \ + CASCADE, FOREIGN KEY(post_id) REFERENCES post(id) ON DELETE CASCADE)" + ; (unit ->. unit) + (* backlinks *) + "CREATE TABLE IF NOT EXISTS post_reply (id INTEGER, reply_id INTEGER, \ + FOREIGN KEY(reply_id) REFERENCES post(id) ON DELETE CASCADE)" + |] + in + let indexes = + [| (unit ->. unit) + "CREATE INDEX IF NOT EXISTS index_thread_bump ON thread \ + (last_reply_date)" + |] + in + let triggers = + [| (unit ->. unit) + "CREATE TRIGGER IF NOT EXISTS trigger_incr_reply_count AFTER INSERT \ + ON thread_reply BEGIN UPDATE thread SET reply_count = reply_count + \ + 1 WHERE thread_id = new.thread_id; END;" + ; (unit ->. unit) + "CREATE TRIGGER IF NOT EXISTS trigger_decr_reply_count AFTER DELETE \ + ON thread_reply BEGIN UPDATE thread SET reply_count = reply_count - \ + 1 WHERE thread_id = old.thread_id; END;" + ; (unit ->. unit) + "CREATE TRIGGER IF NOT EXISTS trigger_update_last_reply_date AFTER \ + INSERT ON thread_reply BEGIN UPDATE thread SET last_reply_date = \ + (SELECT date FROM post WHERE id = new.post_id) WHERE thread_id = \ + new.thread_id; END;" + |] + in + Array.iter (fun query -> Db.exec_unsafe query ()) tables; + Array.iter (fun query -> Db.exec_unsafe query ()) indexes; + Array.iter (fun query -> Db.exec_unsafe query ()) triggers; + () + +let add_post = + Db.find + @@ (t3 string float string ->! int) + "INSERT INTO post (user_id,date,comment) VALUES (?,?,?) RETURNING id" + +let add_thread_reply = + Db.exec @@ (t2 int int ->. unit) "INSERT INTO thread_reply VALUES (?,?)" + +let find_post_w_join = + Db.find_opt + @@ (int ->! t6 int string float string int string) + "SELECT p.id, p.user_id, p.date, p.comment, t_r.thread_id, u.nick FROM \ + post p JOIN thread_reply t_r ON p.id=t_r.post_id JOIN user u ON \ + p.user_id = u.user_id WHERE p.id = ?" + +let find_post_thread_id = + Db.find_opt + @@ (int ->! int) "SELECT thread_id FROM thread_reply WHERE post_id=?" + +let add_thread = + Db.exec + @@ (t6 int float float string int float ->. unit) + "INSERT INTO thread VALUES (?,?,?,?,?,?)" + +let find_thread = + Db.find_opt + @@ (int ->! t6 int float float string int float) + "SELECT * FROM thread WHERE thread_id=?" + +let add_post_reply = + Db.exec @@ (t2 int int ->. unit) "INSERT INTO post_reply VALUES (?,?)" + +let get_thread_replies = + Db.collect_list + @@ (int ->* int) "SELECT post_id FROM thread_reply WHERE thread_id = ?" + +let get_post_backlinks = + Db.collect_list + @@ (int ->* int) "SELECT reply_id FROM post_reply WHERE id = ?" + +let get_all_thread_ids = + Db.collect_list @@ (unit ->* int) "SELECT thread_id FROM thread" + +let delete_post = Db.exec @@ (int ->. unit) "DELETE FROM post WHERE id=?" + +let get_thread_bump_rank = + Db.find + @@ (int ->! t2 int int) + "SELECT reply_count, CAST (rank AS INTEGER) FROM ( SELECT thread_id, \ + reply_count, ROW_NUMBER () OVER ( ORDER BY last_reply_date DESC ) rank \ + FROM thread ) WHERE thread_id = ?" +(* ----- *) + +let get_thread_bump_rank id = + (* count rank from 0 + reply_count = nb_row(thread_reply) - 1; because we don't count op *) + let+ reply_count, rank = get_thread_bump_rank id in + let rank = rank - 1 in + if rank > Config.thread_alive_max_count then Locked rank + else if reply_count >= Config.thread_replies_max_count then Locked rank + else Alive rank + +let get_post_thread_id id = + let* opt = find_post_thread_id id in + match opt with + | None -> Error (Internal (Db_not_found (string_of_int id))) + | Some v -> Ok v + +let delete id = + Db.do_transaction @@ fun () -> + let* thread_id = get_post_thread_id id in + let is_op = thread_id = id in + match is_op with + | true -> + let* replies = get_thread_replies thread_id in + let* () = list_iter delete_post replies in + Ok () + | false -> + let+ () = delete_post id in + () + +let find_post id = + let* opt = find_post_w_join id in + match opt with + | None -> Ok None + | Some (id, poster_id, date, comment_str, thread_id, poster_nick) -> + let* comment = + Comment.of_string comment_str + |> Result.map_error (fun s -> Unprocessable (Fmt.str "comment: %s" s)) + in + let* image_info = Db_image.P.info id in + let+ backlinks = get_post_backlinks id in + Some + { id + ; parent_t_id = thread_id + ; date + ; poster_id + ; poster_nick + ; comment + ; image_info + ; backlinks + } + +let get_post id = + let* opt = find_post id in + match opt with + | None -> Error (Internal (Db_not_found (string_of_int id))) + | Some v -> Ok v + +let find_thread id = + let* opt = find_thread id in + match opt with + | None -> Ok None + | Some (id, lat, lng, subject, reply_count, _last_reply_date) -> + let* op = get_post id in + let+ bump_status = get_thread_bump_rank id in + Some { op; subject; lat; lng; bump_status; reply_count } + +let find_thread_w_reply id = + let* opt = find_thread id in + match opt with + | None -> Ok None + | Some { op; subject; lat; lng; bump_status; reply_count } -> + let+ reply_l = + let* ids = get_thread_replies id in + list_map get_post ids + in + Some + Thread_w_reply. + { op; subject; lat; lng; bump_status; reply_count; reply_l } + +let get_thread id = + let* opt = find_thread id in + match opt with + | None -> Error (Internal (Db_not_found (string_of_int id))) + | Some v -> Ok v + +let find_post id = Db.do_transaction @@ fun () -> find_post id + +let find_thread id = Db.do_transaction @@ fun () -> find_thread id + +let find_thread_w_reply id = + Db.do_transaction @@ fun () -> find_thread_w_reply id + +let get_catalog () = + Db.do_transaction @@ fun () -> + let* ids = get_all_thread_ids () in + list_map get_thread ids + +let add_post_aux ~thread_id ~thread_data ~user ~image ~comment = + let poster_id = user.user_id in + let poster_nick = user.user_nick in + let date = Unix.time () in + let image_info = Option.map (fun o -> o.info) image in + let comment_str = Comment.to_string comment in + let* id = add_post (poster_id, date, comment_str) in + let thread_id = Option.value ~default:id thread_id in + let* () = + match thread_data with + | None -> Ok () + | Some (subject, lat, lng) -> + (* don't count op in thread number of replies + -1 because of trigger on thread_reply table insert *) + let nb_reply = -1 in + add_thread (id, lat, lng, subject, nb_reply, date) + in + let* () = add_thread_reply (thread_id, id) in + let* () = + Option.fold ~none:(Ok ()) ~some:(fun img -> Db_image.P.upload id img) image + in + let+ () = + Comment.backlinks comment + |> list_iter (fun cited_id -> add_post_reply (cited_id, id)) + in + { id + ; parent_t_id = thread_id + ; date + ; poster_id + ; poster_nick + ; comment + ; image_info + ; backlinks = + [] (* TODO can be false because of possibility to reply to futur post *) + } + +let add_post ~thread_id ~user ~image ~comment = + Db.do_transaction @@ fun () -> + let+ post = + add_post_aux ~thread_id:(Some thread_id) ~thread_data:None ~user ~image + ~comment + in + post + +let add_thread ~subject ~lat ~lng ~user ~image ~comment = + Db.do_transaction @@ fun () -> + let subject : v_string = subject in + let subject :> string = subject in + let thread_data = Some (subject, lat, lng) in + let+ op = add_post_aux ~thread_id:None ~thread_data ~user ~image ~comment in + Thread_w_reply. + { op + ; subject + ; lat + ; lng + ; bump_status = Alive 0 + ; reply_count = 0 + ; reply_l = [ op ] + } diff --git a/src/db_post.mli b/src/db_post.mli new file mode 100644 index 0000000..6a49c87 --- /dev/null +++ b/src/db_post.mli @@ -0,0 +1,28 @@ +open Err +open Types + +val find_post : post_id -> post option result + +val find_thread : post_id -> thread option result + +val find_thread_w_reply : post_id -> Thread_w_reply.t option result + +val get_catalog : unit -> thread list result + +val delete : post_id -> unit result + +val add_post : + thread_id:post_id + -> user:user + -> image:img option + -> comment:comment + -> post result + +val add_thread : + subject:v_string + -> lat:float + -> lng:float + -> user:user + -> image:img option + -> comment:comment + -> Thread_w_reply.t result diff --git a/src/db_user.ml b/src/db_user.ml new file mode 100644 index 0000000..1f5b122 --- /dev/null +++ b/src/db_user.ml @@ -0,0 +1,125 @@ +open Syntax +open Types +open Caqti_request.Infix +open Caqti_type +open Caqti_db + +let () = + let tables = + [| (unit ->. unit) + "CREATE TABLE IF NOT EXISTS user (user_id TEXT, nick TEXT, password \ + TEXT, email TEXT, bio TEXT, PRIMARY KEY(user_id))" + |] + in + Array.iter (fun query -> Db.exec_unsafe query ()) tables + +let get_password_hash = + Db.find @@ (string ->! string) "SELECT password FROM user WHERE user_id=?" + +let upload_user = + Db.exec + @@ (t5 string string string string string ->. unit) + "INSERT INTO user VALUES (?, ?, ?, ?, ?)" + +let update_bio = + Db.exec @@ (t2 string string ->. unit) "UPDATE user SET bio=? WHERE user_id=?" + +let update_nick = + Db.exec + @@ (t2 string string ->. unit) "UPDATE user SET nick=? WHERE user_id=?" + +let update_email = + Db.exec + @@ (t2 string string ->. unit) "UPDATE user SET email=? WHERE user_id=?" + +let update_password_hash = + Db.exec + @@ (t2 string string ->. unit) "UPDATE user SET password=? WHERE user_id=?" + +let delete_user = + Db.exec @@ (string ->. unit) "DELETE FROM user WHERE user_id=?" + +let find = + Db.find_opt + @@ + (* there is no "tup6" *) + (string ->! t5 string string string string string) + "SELECT * FROM user WHERE user_id=?" + +let find_of_nick = + Db.find_opt + @@ (string ->! t5 string string string string string) + "SELECT * FROM user WHERE nick=?" + +let find_of_email = + Db.find_opt + @@ (string ->! t5 string string string string string) + "SELECT * FROM user WHERE email=?" + +(* ----- *) + +let find_user_private_aux f s = + let* opt = f s in + match opt with + | None -> Ok None + | Some (user_id, user_nick, _password, email, bio) -> + let+ avatar_info = Db_image.U.info user_id in + let user_is_admin = List.mem user_nick Config_serv.admin_l in + Some + User_private. + { user_id; user_nick; user_is_admin; bio; avatar_info; email } + +let private_to_public u = + let User_private. + { user_id; user_nick; user_is_admin; bio; avatar_info; email = _ } = + u + in + { user_id; user_nick; user_is_admin; bio; avatar_info } + +let find_user_aux f s = + let+ opt = find_user_private_aux f s in + Option.map private_to_public opt + +let find_user_of_nick s = + Db.do_transaction @@ fun () -> + let s : v_string = s in + find_user_aux find_of_nick (s :> string) + +let find_user_of_email s = + Db.do_transaction @@ fun () -> + let s : v_string = s in + find_user_aux find_of_email (s :> string) + +let find_user id = Db.do_transaction @@ fun () -> find_user_aux find id + +let find_user_private id = + Db.do_transaction @@ fun () -> find_user_private_aux find id + +let get_password_hash id = Db.do_transaction @@ fun () -> get_password_hash id + +let update_nick user_id s = + Db.do_transaction @@ fun () -> + let s : v_string = s in + update_nick ((s :> string), user_id) + +let update_bio user_id s = + Db.do_transaction @@ fun () -> + let s : v_string = s in + update_bio ((s :> string), user_id) + +let update_email user_id s = + Db.do_transaction @@ fun () -> + let s : v_string = s in + update_email ((s :> string), user_id) + +let update_password_hash user_id password_hash = + Db.do_transaction @@ fun () -> update_password_hash (password_hash, user_id) + +let add_user ~email ~nick ~password_hash = + Db.do_transaction @@ fun () -> + let email : v_string = email in + let nick : v_string = nick in + let user_id = Util.gen_uuid () in + upload_user (user_id, (nick :> string), password_hash, (email :> string), "") + +let delete_user user_id = Db.do_transaction @@ fun () -> delete_user user_id diff --git a/src/db_user.mli b/src/db_user.mli new file mode 100644 index 0000000..edc313f --- /dev/null +++ b/src/db_user.mli @@ -0,0 +1,25 @@ +open Err +open Types + +val find_user : user_id -> user option result + +val find_user_of_nick : v_string -> user option result + +val find_user_of_email : v_string -> user option result + +val find_user_private : user_id -> User_private.t option result + +val get_password_hash : user_id -> string result + +val update_password_hash : user_id -> string -> unit result + +val update_nick : user_id -> v_string -> unit result + +val update_bio : user_id -> v_string -> unit result + +val update_email : user_id -> v_string -> unit result + +val delete_user : user_id -> unit result + +val add_user : + email:v_string -> nick:v_string -> password_hash:string -> unit result diff --git a/src/delete_page.eml.html b/src/delete_page.eml.html deleted file mode 100644 index bce2690..0000000 --- a/src/delete_page.eml.html +++ /dev/null @@ -1,20 +0,0 @@ -let f post_preview post_id request = - - - <%s! post_preview %> -% let url = Format.sprintf "/delete/%s" post_id in -% begin match Dream.session "nick" request with -% | None -> -% let redirect = Dream.to_percent_encoded url in -
Login to delete your post. -% | Some _nick -> -
-
-
-<%s! Dream.form_tag ~action:url request %> - - -
-
-
-% end; diff --git a/src/discuss.ml b/src/discuss.ml deleted file mode 100644 index d5ec97f..0000000 --- a/src/discuss.ml +++ /dev/null @@ -1,129 +0,0 @@ -open Syntax - -(** Creating the table of all messages. - - Each message is made of : - - - an id (msg_id) - - the id of the sender (from_id) - - the id of the receiver (to_id) - - some text (msg) - - TODO: add date ? *) - -module Q = struct - open Caqti_request.Infix - open Caqti_type - - let create_msg_table = - Db.exec - @@ (unit ->. unit) - "CREATE TABLE IF NOT EXISTS msg ( msg_id TEXT, from_id TEXT, to_id \ - TEXT, msg TEXT, PRIMARY KEY(msg_id), FOREIGN KEY(from_id) REFERENCES \ - user(user_id) ON DELETE CASCADE, FOREIGN KEY(to_id) REFERENCES \ - user(user_id) ON DELETE CASCADE)" - - let find_comrades = - Db.collect_list - @@ (tup2 string string ->* tup2 string string) - "SELECT from_id, to_id FROM msg WHERE from_id=? OR to_id=?" - - let find_messages = - Db.collect_list - @@ (tup2 (tup2 string string) (tup2 string string) ->* tup2 string string) - "SELECT from_id, msg FROM msg WHERE (from_id=? AND to_id=?) OR \ - (from_id=? AND to_id=?)" - - let insert_msg = - Db.exec - @@ (tup3 string string string ->. unit) - "INSERT INTO msg VALUES (NULL, ?, ?, ?)" -end - -let () = - Result.iter_error - (fun _e -> Dream.error (fun log -> log "can't create table")) - (Q.create_msg_table ()) - -(** let's find who the user is talking to so we can know if they're dangerous *) -let find_comrades user_id = - let* comrades = Q.find_comrades (user_id, user_id) in - let comrades = - List.map (fun (l, r) -> if l = user_id then r else l) comrades - in - Ok (List.sort_uniq String.compare comrades) - -(** find all messages between two товарищи *) -let find_messages k1 k2 = Q.find_messages ((k1, k2), (k2, k1)) - -(** display the list of discussions *) -let render = - let pp_one_discuss fmt (id, nick) = - Format.fprintf fmt {|
  • %s
  • |} id nick - in - fun request -> - Utils.logged_in_or_redirect request (fun user_id -> - Utils.render_result request - @@ let* comrades = find_comrades user_id in - let* comrades = - Syntax.unwrap_list - (fun id -> - match User.get_nick id with - | Error _e as e -> e - | Ok nick -> Ok (id, nick) ) - comrades - in - Ok - (Format.asprintf "
      %a
    " - (Format.pp_print_list - ~pp_sep:(fun fmt () -> Format.fprintf fmt "
    ") - pp_one_discuss ) - comrades ) ) - -let pp_discussion (request, user_id, comrade_id) = - let path = Format.sprintf "/discuss/%s" comrade_id in - Utils.render_result request - @@ let* msg = find_messages user_id comrade_id in - let* user_nick = User.get_nick user_id in - let* comrade_nick = User.get_nick comrade_id in - let pp_one_msg fmt (from_id, msg) = - Format.fprintf fmt "
  • %s | %s
  • " - (if from_id = user_id then user_nick else comrade_nick) - (Dream.html_escape msg) - in - let pp_all_msg fmt msg = - Format.fprintf fmt "
      %a
    " - (Format.pp_print_list - ~pp_sep:(fun fmt () -> Format.fprintf fmt "
    ") - pp_one_msg ) - msg - in - Ok - (Format.asprintf - {|%a
    - %s - - - |} - pp_all_msg msg - (Dream.form_tag ~action:path request) ) - -(** display one discussion *) -let renderone request = - Utils.logged_in_or_redirect request (fun user_id -> - let comrade_id = Dream.param request "comrade_id" in - pp_discussion (request, user_id, comrade_id) ) - -let insert_msg from_id to_id msg = Q.insert_msg (from_id, to_id, msg) - -(** handle posts *) -let post request = - Utils.logged_in_or_redirect request (fun user_id -> - match%lwt Dream.form request with - | `Ok [ ("msg", msg) ] -> begin - let comrade_id = Dream.param request "comrade_id" in - match insert_msg user_id comrade_id msg with - | Ok () -> pp_discussion (request, user_id, comrade_id) - | Error e -> Utils.render e request - end - | form -> Utils.handle_invalid_form form ) diff --git a/src/dune b/src/dune index 6e79dd3..4959d28 100644 --- a/src/dune +++ b/src/dune @@ -1,30 +1,33 @@ (executable (public_name permap) - (modules - app - babillard - babillard_page - catalog_page - content - db - delete_page - discuss - image - emojid - login - permap - post_form - pp_babillard - register - report_page - syntax - template - thread_page - user - user_account - user_profile - utils) + (modules permap) + (package permap) (libraries + config_serv_impl ; implements config_serv + config_impl ; implements config + shared + permap + ;; + dream + fmt + fpath + uri + prelude) + (preprocess + (pps lwt_ppx)) + (flags + (:standard -open Prelude))) + +(library + (name permap) + (wrapped false) + (modules :standard \ permap json_data syntax types err validate_str) + (libraries + config_serv ; virtual + config ; virtual + shared ; virtual + comment + ;; bos caqti caqti.blocking @@ -32,97 +35,43 @@ conan conan.string conan-database.light - containers-data - directories + digestif dream dream-pure emile - emoji - fpath - lambdasoup - omd + fmt + htmlit safepass - scfg - uri uuidm - yojson) + unix + prelude) (preprocess - (pps lwt_ppx))) + (pps lwt_ppx)) + (flags + (:standard -open Prelude))) + +(library + (name shared) + (wrapped false) + (modules json_data syntax types err validate_str) + (modules_without_implementation types) + (libraries + config ; virtual + comment + ;; + lwt + data-encoding + fmt + prelude) + (flags + (:standard -open Prelude))) (rule - (targets babillard_page.ml) - (deps babillard_page.eml.html) - (action - (run dream_eml %{deps} --workspace %{workspace_root}))) - -(rule - (targets catalog_page.ml) - (deps catalog_page.eml.html) - (action - (run dream_eml %{deps} --workspace %{workspace_root}))) - -(rule - (targets delete_page.ml) - (deps delete_page.eml.html) - (action - (run dream_eml %{deps} --workspace %{workspace_root}))) - -(rule - (targets login.ml) - (deps login.eml.html) - (action - (run dream_eml %{deps} --workspace %{workspace_root}))) - -(rule - (targets post_form.ml) - (deps post_form.eml.html) - (action - (run dream_eml %{deps} --workspace %{workspace_root}))) - -(rule - (targets register.ml) - (deps register.eml.html) - (action - (run dream_eml %{deps} --workspace %{workspace_root}))) - -(rule - (targets report_page.ml) - (deps report_page.eml.html) - (action - (run dream_eml %{deps} --workspace %{workspace_root}))) - -(rule - (targets template.ml) - (deps template.eml.html) - (action - (run dream_eml %{deps} --workspace %{workspace_root}))) - -(rule - (targets thread_page.ml) - (deps thread_page.eml.html) - (action - (run dream_eml %{deps} --workspace %{workspace_root}))) - -(rule - (targets user_account.ml) - (deps user_account.eml.html) - (action - (run dream_eml %{deps} --workspace %{workspace_root}))) - -(rule - (targets user_profile.ml) - (deps user_profile.eml.html) - (action - (run dream_eml %{deps} --workspace %{workspace_root}))) - -(rule - (target content.ml) + (target assets.ml) (deps - (source_tree content) - (file content/assets/js/babillard.js) - (file content/assets/js/catalog.js) - (file content/assets/js/thread.js)) + (source_tree assets) + (file assets/js/client.js)) (action (with-stdout-to %{null} - (run ocaml-crunch -m plain content -o %{target})))) + (run ocaml-crunch -m plain assets -o %{target})))) diff --git a/src/emojid.ml b/src/emojid.ml deleted file mode 100644 index 8691e53..0000000 --- a/src/emojid.ml +++ /dev/null @@ -1,86 +0,0 @@ -open Syntax -open Caqti_request.Infix -open Caqti_type - -(* todo better: make emojid just string and not string list in this module; - problem is we have to split on unicode *) - -module Q = struct - (* we save emojid in a string with emoji separated by '-' *) - let upload_emojid uuid emojid = - let emojid = String.concat "-" emojid in - Db.exec - ((tup2 string string ->. unit) "INSERT INTO uuid_emojid VALUES (?,?)") - (uuid, emojid) - - let get_emojid uuid = - Db.find - ((string ->! string) "SELECT emojid FROM uuid_emojid WHERE uuid=?") - uuid - |> Result.map (String.split_on_char '-') - - let get_all_emojid () = - let* l = - Db.collect_list ((unit ->* string) "SELECT emojid FROM uuid_emojid") () - in - Ok (List.map (String.split_on_char '-') l) -end - -module Trie = CCTrie.Make (struct - type t = string list - - type char_ = string - - let compare = String.compare - - let to_iter o f = List.iter f o - - let of_list = Fun.id -end) - -let max_emojid_lenght = 16 - -let alphabet = - Array.append Emoji.category_animals_and_nature Emoji.category_food_and_drink - -let trie = - let tables = - [| (unit ->. unit) - "CREATE TABLE IF NOT EXISTS uuid_emojid (uuid TEXT, emojid TEXT)" - |] - in - if - Array.exists Result.is_error - (Array.map (fun query -> Db.exec query ()) tables) - then failwith "can't create emojid's tables" - else - match Q.get_all_emojid () with - | Error e -> - failwith (Format.sprintf "Error with Emojid.Q.select_all: %s" e) - | Ok l -> - let l = List.map (fun e -> (e, ())) l in - ref (Trie.of_list l) - -let make uuid = - (* pick a list of emojis *) - let random_emojis = - List.init max_emojid_lenght (fun _i -> - let n = Random.int (Array.length alphabet) in - Array.get alphabet n ) - in - (* pick the smallest emojid possible *) - let longest_prefix = Trie.longest_prefix random_emojis !trie in - (* add one more emoji to longest_prefix *) - match List.nth_opt random_emojis (List.length longest_prefix) with - | None -> - Dream.error (fun log -> log "Emojid error: longest prefix is too long"); - Error "Could not create emojid" - | Some x -> - let emojid = longest_prefix @ [ x ] in - let* () = Q.upload_emojid uuid emojid in - trie := Trie.add emojid () !trie; - Ok (String.concat "" emojid) - -let get uuid = - let* l = Q.get_emojid uuid in - Ok (String.concat "" l) diff --git a/src/emojid.mli b/src/emojid.mli deleted file mode 100644 index 664874c..0000000 --- a/src/emojid.mli +++ /dev/null @@ -1,5 +0,0 @@ -(** [make uuid] creates an emojid for [uuid]; hopefully returns [Ok emojid] *) -val make : string -> (string, string) result - -(** [get uuid] is [Ok emoji] if [uuid] has an emojid *) -val get : string -> (string, string) result diff --git a/src/err.ml b/src/err.ml new file mode 100644 index 0000000..17c8b8d --- /dev/null +++ b/src/err.ml @@ -0,0 +1,46 @@ +type internal_err = + | No_msg (* used to not expose detail to client *) + | Db of string + | Db_not_found of string + | Bos of string + | Conan of string + +type t = + | Internal of internal_err + (* error due to client *) + | Bad_form + | Bad_form_suspicious + | Unauthorized + | Unauthorized_login of string + | Forbidden + | Not_found + | Not_found_thread of int + | Not_found_post of int + | Not_found_user of string + | Not_found_image of int + | Unprocessable of string + +type nonrec 'a result = ('a, t) result + +let pp_internal fmt = function + | No_msg -> Fmt.pf fmt "no msg" + | Db s -> Fmt.pf fmt "db: %s" s + | Db_not_found s -> Fmt.pf fmt "db (not found): %s" s + | Bos s -> Fmt.pf fmt "bos: %s" s + | Conan s -> Fmt.pf fmt "conan: %s" s + +let pp fmt = function + | Internal e -> Fmt.pf fmt "%a" pp_internal e + | Bad_form -> Fmt.pf fmt "bad form" + | Bad_form_suspicious -> Fmt.pf fmt "bad form suspicious" + | Unauthorized -> Fmt.pf fmt "unauthorized" + | Unauthorized_login s -> Fmt.pf fmt "unauthorized login: %s" s + | Forbidden -> Fmt.pf fmt "forbidden" + | Not_found -> Fmt.pf fmt "not found" + | Not_found_thread s -> Fmt.pf fmt "thread not found: %d" s + | Not_found_post s -> Fmt.pf fmt "post not found: %d" s + | Not_found_user s -> Fmt.pf fmt "user not found: %s" s + | Not_found_image s -> Fmt.pf fmt "image not found: %d" s + | Unprocessable s -> Fmt.pf fmt "unprocessable: %s" s + +let hide_internal_err_detail = function Internal _ -> Internal No_msg | e -> e diff --git a/src/html.ml b/src/html.ml new file mode 100644 index 0000000..18eb759 --- /dev/null +++ b/src/html.ml @@ -0,0 +1,35 @@ +open Htmlit + +let page_to_string page = Htmlit.El.to_string ~doctype:true page + +let page_of_res res = + let res_str, msg = + match res with Error msg -> ("Error", msg) | Ok msg -> ("Ok", msg) + in + let title = Fmt.str "%s: %s" res_str msg in + let content = El.div [ El.h1 [ El.txt res_str ]; El.p [ El.txt msg ] ] in + let page = + El.page ~lang:"en" ~styles:[] ~scripts:[] ~more_head:El.void ~title content + in + page + +(* TODO have a loading animation *) +let app_start_page = + let title = "Permap" in + let body = El.body [] in + let styles = + let leaflet_style = "/assets/css/leaflet.css" in + let style = "/assets/css/style.css" in + [ leaflet_style; style ] + in + let more_head = + let icon = + El.link + ~at:At.[ rel "icon"; type' "image/png"; href "/assets/img/favicon.png" ] + () + in + icon + in + let scripts = [ "/assets/js/client.js" ] in + let page = El.page ~lang:"en" ~styles ~scripts ~more_head ~title body in + page diff --git a/src/html.mli b/src/html.mli new file mode 100644 index 0000000..fe14fdd --- /dev/null +++ b/src/html.mli @@ -0,0 +1,7 @@ +open Htmlit.El + +val page_to_string : html -> string + +val page_of_res : (string, string) result -> html + +val app_start_page : html diff --git a/src/image.ml b/src/image.ml index 243309b..ccf2edd 100644 --- a/src/image.ml +++ b/src/image.ml @@ -1,163 +1,87 @@ open Syntax -open Caqti_request.Infix -open Caqti_type +open Err +open Types -type t = - { name : string - ; alt : string - ; content : string - ; thumbnail : string - } - -let () = - let tables = - [| (unit ->. unit) - "CREATE TABLE IF NOT EXISTS image_info (post_id TEXT, image_name \ - TEXT, image_alt TEXT, FOREIGN KEY(post_id) REFERENCES \ - post_user(post_id) ON DELETE CASCADE)" - ; (unit ->. unit) - "CREATE TABLE IF NOT EXISTS image_content (post_id TEXT, content \ - TEXT, FOREIGN KEY(post_id) REFERENCES post_user(post_id) ON DELETE \ - CASCADE)" - ; (unit ->. unit) - "CREATE TABLE IF NOT EXISTS image_thumbnail (post_id TEXT, content \ - TEXT, FOREIGN KEY(post_id) REFERENCES post_user(post_id) ON DELETE \ - CASCADE)" - ; (unit ->. unit) - "CREATE TABLE IF NOT EXISTS user_image_content (user_id TEXT, content \ - TEXT, FOREIGN KEY(user_id) REFERENCES user(user_id) ON DELETE \ - CASCADE)" - ; (unit ->. unit) - "CREATE TABLE IF NOT EXISTS user_image_thumbnail (user_id TEXT, \ - content TEXT, FOREIGN KEY(user_id) REFERENCES user(user_id) ON \ - DELETE CASCADE)" - |] - in - if - Array.exists Result.is_error - (Array.map (fun query -> Db.exec query ()) tables) - then Dream.error (fun log -> log "can't create images tables") - -let upload_info = - Db.exec - @@ (tup3 string string string ->. unit) - "INSERT INTO image_info VALUES (?,?,?)" - -let upload_content = - Db.exec - @@ (tup2 string string ->. unit) "INSERT INTO image_content VALUES (?,?)" - -let upload_thumbnail = - Db.exec - @@ (tup2 string string ->. unit) "INSERT INTO image_thumbnail VALUES (?,?)" - -let get_content = - Db.find_opt - @@ (string ->? string) "SELECT content FROM image_content WHERE post_id=?" - -let get_thumbnail = - Db.find_opt - @@ (string ->? string) "SELECT content FROM image_thumbnail WHERE post_id=?" - -let get_info = - Db.find_opt - @@ (string ->? tup2 string string) - "SELECT image_name,image_alt FROM image_info WHERE post_id=?" - -let upload_user_content = - Db.exec - @@ (tup2 string string ->. unit) "INSERT INTO user_image_content VALUES (?,?)" - -let upload_user_thumbnail = - Db.exec - @@ (tup2 string string ->. unit) - "INSERT INTO user_image_thumbnail VALUES (?,?)" - -let get_user_content = - Db.find_opt - @@ (string ->? string) - "SELECT content FROM user_image_content WHERE user_id=?" - -let get_user_thumbnail = - Db.find_opt - @@ (string ->? string) - "SELECT content FROM user_image_thumbnail WHERE user_id=?" - -let delete_user_content = - Db.exec @@ (string ->. unit) "DELETE FROM user_image_content WHERE user_id=?" - -let delete_user_thumbnail = - Db.exec - @@ (string ->. unit) "DELETE FROM user_image_thumbnail WHERE user_id=?" - -let upload image id = - let* () = upload_info (id, image.name, image.alt) in - let* () = upload_content (id, image.content) in - upload_thumbnail (id, image.thumbnail) - -let upload_avatar image id = - let* () = delete_user_content id in - let* () = delete_user_thumbnail id in - let* () = upload_user_content (id, image.content) in - upload_user_thumbnail (id, image.thumbnail) - -let make_thumbnail content = - let open Bos in - (* jpp *) - let ( let* ) o f = - Result.fold ~ok:f ~error:(function `Msg s -> Result.error s) o - in - - let* image_file = OS.File.tmp "%s" in - let* thumb_file = OS.File.tmp "%s_thumb" in - let* () = OS.File.write image_file content in - let cmd = - Cmd.( - v "convert" % "-define" % "jpeg:size=700x700" % p image_file - % "-auto-orient" % "-thumbnail" % "300x300>" % "-unsharp" % "0x.5" - % "-format" % "jpg" % p thumb_file ) - in - let* () = OS.Cmd.run cmd in - let* thumbnail = OS.File.read thumb_file in - let* () = OS.File.delete image_file in - let* () = OS.File.delete thumb_file in - Ok thumbnail - -let mime = +let read_mime = let database = Conan.Process.database ~tree:Conan_light.tree in - fun content -> - match Conan_string.run ~database content with - | Ok m -> Conan.Metadata.mime m - | Error _ -> None + fun data -> + try + match Conan_string.run ~database data with + | Error (`Msg e) -> Error (Internal (Conan e)) + | Ok m -> ( + match Conan.Metadata.mime m with + | None -> Error (Unprocessable "no mime found") + | Some mime -> + (* Case Closed ~~! *) + Ok mime ) + with _ -> + (* conan is still experimental and can leak exceptions *) + Error (Internal (Conan "conan error")) -let make_image image = - let max_name = 1000 in - let max_alt = 3000 in - let max_content = 4200000 in - - let name, alt, content = image in - let name = - match name with - | Some name -> Dream.html_escape name - | None -> - (* make up random name if no name was given *) - Uuidm.to_string (Uuidm.v4_gen App.random_state ()) +let magick = + let open Bos in + let strip_exif file = + let cmd = Cmd.(v "magick" % p file % "-strip" % p file) in + OS.Cmd.(run cmd) in - let alt = if String.trim alt = "" then name else alt in - if String.length name > max_name then - Error (Format.sprintf "Image name too long: More than %dB" max_name) - else if String.length alt > max_alt then - Error (Format.sprintf "Image description too long: More than %dB" max_alt) - else if String.length content > max_content then - Error (Format.sprintf "Image size too big: More than %dB" max_content) - else - match mime content with - | None -> Error "invalid image type" - | Some mime -> ( - match mime with - | "image/jpeg" | "image/png" | "image/webp" | "image/gif" -> ( - match make_thumbnail content with - | Error e -> Error e - | Ok thumbnail -> Ok { name; alt; content; thumbnail } ) - | _unsupported_mime_type -> - Error (Format.sprintf "unsupported image type: %s" mime) ) + let make_thumbnail in_file out_file = + let cmd = + Cmd.( + v "magick" % "-define" % "jpeg:size=700x700" % p in_file + % "-auto-orient" % "-thumbnail" % "300x300>" % "-unsharp" % "0x.5" + % "-format" % "jpg" % p out_file ) + in + OS.Cmd.run cmd + in + let read_dimension file = + let cmd = Cmd.(v "magick" % "identify" % "-format" % "%w#%h" % p file) in + let* s = OS.Cmd.(run_out cmd |> out_string |> success) in + match String.split_on_char '#' s |> List.map int_of_string_opt with + | [] -> assert false + | [ Some w; Some h ] -> Ok (w, h) + | _ -> Fmt.error_msg "magick identify, invalid format" + in + fun data -> + Result.map_error (function `Msg s -> Internal (Bos s)) + @@ + let* image_file = OS.File.tmp "%s" in + let* thumb_file = OS.File.tmp "%s_thumb" in + let res = + let* () = OS.File.write image_file data in + let* () = strip_exif image_file in + let* () = make_thumbnail image_file thumb_file in + let* image = OS.File.read image_file in + let* thumbnail = OS.File.read thumb_file in + let* img_dim = read_dimension image_file in + let* thumb_dim = read_dimension thumb_file in + Ok ((image, img_dim), (thumbnail, thumb_dim)) + in + let* () = OS.File.delete image_file in + let* () = OS.File.delete thumb_file in + res + +let build ~name ~alt data = + let* name = Validate_str.image_name name in + let* alt = Validate_str.image_alt alt in + let* () = + let data_len = String.length data in + if data_len <= Config.image_max_size then Ok () + else + let s = + Fmt.str "Image is too big (%a), maximum size is %a" Fmt.bi_byte_size + data_len Fmt.bi_byte_size Config.image_max_size + in + Error (Unprocessable s) + in + let* mime = read_mime data in + let* () = + match Array.mem mime Config.supported_mime_type with + | true -> Ok () + | false -> Error (Unprocessable (Fmt.str "unsupported image type: %s" mime)) + in + let+ (data, (w, h)), (thumbnail_data, (thumb_w, thumb_h)) = magick data in + let md5 = Digestif.MD5.(to_hex (digest_string data)) in + let info = + { md5; mime; w; h; thumb_w; thumb_h; name :> string; alt :> string } + in + { info; data; thumbnail_data } diff --git a/src/image.mli b/src/image.mli new file mode 100644 index 0000000..28844c7 --- /dev/null +++ b/src/image.mli @@ -0,0 +1,5 @@ +open Err +open Types + +(* validate data, make thumbnail *) +val build : name:string -> alt:string -> string -> img result diff --git a/src/js/babillard.ml b/src/js/babillard.ml deleted file mode 100644 index 1d1a317..0000000 --- a/src/js/babillard.ml +++ /dev/null @@ -1,124 +0,0 @@ -open Brr -open Utils -open Map - -module Visibility = struct - let new_thread_div = find_by_id "new-thread" - - let thread_comment = find_by_id "comment" - - let thread_preview_div = find_by_id "thread-preview" - - let return_button = find_by_id "return-button" - - (* new-thread-button is new-thread-button-redirect if not logged in *) - let new_thread_button = find_by_id_opt "new-thread-button" - - let is_in_new_thread_mode = ref false - - let set_visible el = - log "set_visible@\n"; - El.set_class (Jstr.of_string "off") false el - - let set_invisible el = - log "set_invisible@\n"; - El.set_class (Jstr.of_string "off") true el - - let to_new_thread_mode _event = - log "change_page_mode@\n"; - is_in_new_thread_mode := true; - set_visible new_thread_div; - set_visible return_button; - set_invisible thread_preview_div; - Option.iter set_invisible new_thread_button; - El.set_has_focus true thread_comment; - Leaflet.Map.close_popup ~popup:None map - - let to_babillard_mode _event = - log "change_page_mode@\n"; - is_in_new_thread_mode := false; - set_invisible new_thread_div; - set_invisible return_button; - set_visible thread_preview_div; - Option.iter set_visible new_thread_button; - Leaflet.Map.close_popup ~popup:None map - - let () = - log "add events on return/new thread button@\n"; - let (_ : Ev.listener) = - Ev.listen Ev.click to_babillard_mode (El.as_target return_button) - in - Option.iter - (fun button -> - let (_ : Ev.listener) = - Ev.listen Ev.click to_new_thread_mode (El.as_target button) - in - () ) - new_thread_button -end - -module Marker = struct - let thread_preview_div = find_by_id "thread-preview" - - let marker_on_click thread_preview _e = - log "marker_on_click@\n"; - if not !Visibility.is_in_new_thread_mode then ( - let inner_html = El.Prop.jstr (Jstr.of_string "innerHTML") in - El.set_prop inner_html thread_preview thread_preview_div; - Pretty_post.make_pretty () ) - - let on_each_feature feature layer = - log "on_each_feature@\n"; - let feature_properties = Jv.get feature "properties" in - let thread_preview = Jv.get feature_properties "content" |> Jv.to_jstr in - Leaflet.Layer.on Leaflet.Event.Click (marker_on_click thread_preview) layer - - let handle_geojson geojson = - log "handle_geojson@\n"; - let layer = - Leaflet.Layer.create_geojson geojson [ On_each_feature on_each_feature ] - in - let _marker_layer = Leaflet.Layer.add_to map layer in - () - - let markers_handle_response response = - log "markers_handle_response@\n"; - let geo_json_list_futur = Jv.call response "json" [||] in - ignore @@ Jv.call geo_json_list_futur "then" [| Jv.repr handle_geojson |] - - let () = - log "fetch thread geojson@\n"; - let link = Jv.of_string "/markers" in - (* todo: fetch with Brr *) - let window = Jv.get Jv.global "window" in - let fetchfutur = Jv.call window "fetch" [| link |] in - ignore @@ Jv.call fetchfutur "then" [| Jv.repr markers_handle_response |] -end - -let lat_input = find_by_id "lat-input" - -let lng_input = find_by_id "lng-input" - -let button = find_by_id "submit-button" - -(* set input lat/lng when clicked*) -let on_click_set_latlng e = - log "on_click_set_latlng@\n"; - if !Visibility.is_in_new_thread_mode then ( - let latlng = Leaflet.Event.latlng e in - let popup = - Leaflet.Popup.create ~content:(Some "create thread here") - ~latlng:(Some latlng) [] - in - Leaflet.Map.open_popup popup map; - - (* TODO add a marker with special icon here *) - let lat = Leaflet.Latlng.lat latlng |> Jstr.of_float in - let lng = Leaflet.Latlng.lng latlng |> Jstr.of_float in - let value_jstr = Jstr.of_string "value" in - El.set_at value_jstr (Some lat) lat_input; - El.set_at value_jstr (Some lng) lng_input; - El.set_at (Jstr.of_string "disabled") None button ) - -(*add on_click callback to map*) -let () = Leaflet.Map.on Leaflet.Event.Click on_click_set_latlng map diff --git a/src/js/catalog.ml b/src/js/catalog.ml deleted file mode 100644 index e69de29..0000000 diff --git a/src/js/dune b/src/js/dune deleted file mode 100644 index d14c5fe..0000000 --- a/src/js/dune +++ /dev/null @@ -1,51 +0,0 @@ -(library - (name utils) - (modules utils) - (libraries brr) - (preprocess - (pps js_of_ocaml-ppx))) - -(library - (name post_form) - (modules post_form) - (libraries js_of_ocaml brr utils) - (preprocess - (pps js_of_ocaml-ppx))) - -(library - (name pretty_post) - (modules pretty_post) - (libraries js_of_ocaml brr unix utils) - (preprocess - (pps js_of_ocaml-ppx))) - -(library - (name map) - (modules map) - (libraries js_of_ocaml brr leaflet utils) - (preprocess - (pps js_of_ocaml-ppx))) - -(executable - (name catalog) - (modules catalog) - (libraries js_of_ocaml brr pretty_post) - (modes js) - (preprocess - (pps js_of_ocaml-ppx))) - -(executable - (name babillard) - (modules babillard) - (libraries js_of_ocaml brr map post_form pretty_post leaflet utils) - (modes js) - (preprocess - (pps js_of_ocaml-ppx))) - -(executable - (name thread) - (modules thread) - (libraries js_of_ocaml brr post_form pretty_post) - (modes js) - (preprocess - (pps js_of_ocaml-ppx))) diff --git a/src/js/map.ml b/src/js/map.ml deleted file mode 100644 index 31151b5..0000000 --- a/src/js/map.ml +++ /dev/null @@ -1,99 +0,0 @@ -open Utils - -let map = Leaflet.Map.create_on "map" - -let () = - let osm_layer = Leaflet.Layer.create_tile_osm None in - Leaflet.Layer.add_to map osm_layer - -let storage = Brr_io.Storage.local Brr.G.window - -(* set map's view *) -(* try to set map's view to last position viewed by using web storage *) -let () = - log "setting view@\n"; - let lat = Brr_io.Storage.get_item storage (Jstr.of_string "lat") in - let lng = Brr_io.Storage.get_item storage (Jstr.of_string "lng") in - let zoom = Brr_io.Storage.get_item storage (Jstr.of_string "zoom") in - match (lat, lng, zoom) with - | Some lat, Some lng, Some zoom -> - let lat = Jstr.to_float lat in - let lng = Jstr.to_float lng in - let zoom = - match Jstr.to_int zoom with - | None -> failwith "view storage bug" - | Some zoom -> Some zoom - in - let latlng = Leaflet.Latlng.create lat lng in - ignore @@ Leaflet.Map.set_view latlng ~zoom map - | _ -> - let latlng = Leaflet.Latlng.create 51.505 (-0.09) in - ignore @@ Leaflet.Map.set_view latlng ~zoom:(Some 13) map - -let on_moveend _event = - log "on moveend event@\n"; - let latlng = Leaflet.Map.get_center map in - (*we need to wrap coordinates so we don't drift into a parralel universe and lose track of markers :^) *) - let wrapped_latlng = Leaflet.Map.wrap_latlng latlng map in - let lat = Leaflet.Latlng.lat latlng |> Jv.of_float |> Jv.to_jstr in - let lng = Leaflet.Latlng.lng latlng |> Jv.of_float |> Jv.to_jstr in - match Brr_io.Storage.set_item storage (Jstr.of_string "lat") lat with - | (exception Jv.Error _) | Error _ -> failwith "can't set latlng storage" - | Ok () -> ( - match Brr_io.Storage.set_item storage (Jstr.of_string "lng") lng with - | (exception Jv.Error _) | Error _ -> failwith "can't set latlng storage" - | Ok () -> - let is_wrapped = not @@ Leaflet.Latlng.equals latlng wrapped_latlng in - if is_wrapped then ( - log "setView to wrapped coordinate@\n"; - (* warning: calling setView in on_moveend can cause recursion *) - Leaflet.Map.set_view wrapped_latlng ~zoom:None map ) ) - -let on_zoomend _event = - log "on zoomend event@\n"; - let zoom = Leaflet.Map.get_zoom map in - match - Brr_io.Storage.set_item storage (Jstr.of_string "zoom") (Jstr.of_int zoom) - with - | (exception Jv.Error _) | Error _ -> failwith "can't set latlng storage" - | Ok () -> () - -let () = - log "add on (move/zoom)end event@\n"; - Leaflet.Map.on Leaflet.Event.Move_end on_moveend map; - Leaflet.Map.on Leaflet.Event.Zoom_end on_zoomend map - -module Geolocalize = struct - let update_location geo = - log "update_location@\n"; - match geo with - | Error _ -> failwith "error in geolocation" - | Ok geo -> - let lat = Brr_io.Geolocation.Pos.latitude geo in - let lng = Brr_io.Geolocation.Pos.longitude geo in - let latlng = Leaflet.Latlng.create lat lng in - Leaflet.Map.set_view latlng ~zoom:(Some 13) map - - let geolocalize _ = - log "geolocalize@\n"; - let update_location geo = - log "update_location@\n"; - match geo with - | Error e -> - (* todo: popup error message for user *) - log "geolocation failure: %s@\n" - @@ Jstr.to_string - @@ Brr_io.Geolocation.Error.message e - | Ok geo -> - (* todo: add a special marker to map *) - let lat = Brr_io.Geolocation.Pos.latitude geo in - let lng = Brr_io.Geolocation.Pos.longitude geo in - let latlng = Leaflet.Latlng.create lat lng in - Leaflet.Map.set_view latlng ~zoom:(Some 17) map - in - - let l = Brr_io.Geolocation.of_navigator Brr.G.navigator in - let opts = Brr_io.Geolocation.opts ~high_accuracy:true () in - (* todo: use `Geolocation.watch` instead ? it may improve precision *) - ignore @@ Fut.await (Brr_io.Geolocation.get l ~opts) update_location -end diff --git a/src/js/newthread.ml b/src/js/newthread.ml deleted file mode 100644 index 59dfc54..0000000 --- a/src/js/newthread.ml +++ /dev/null @@ -1,3 +0,0 @@ -open Js_map - -let log = Format.printf diff --git a/src/js/post_form.ml b/src/js/post_form.ml deleted file mode 100644 index d09ded1..0000000 --- a/src/js/post_form.ml +++ /dev/null @@ -1,52 +0,0 @@ -open Brr -open Utils - -(* called by clicking post_id *) -(* insert emojid into reply form *) -let insert_quote el _event = - log "quote@\n"; - let emojid = - match El.at (Jstr.of_string "data-emojid") el with - | None -> failwith "no data-emojid on element" - | Some emojid -> Jstr.to_string emojid - in - match find_by_id_opt "comment" with - | None -> log "element `comment` not found, not logged in?@\n" - | Some textarea -> - let value = El.Prop.value in - let content = Jstr.to_string @@ El.prop value textarea in - let new_content = - if String.ends_with ~suffix:"\n" content || String.length content = 0 then - (* don't skip a line *) - Format.sprintf "%s[>%s] " content emojid - else Format.sprintf "%s@\n[>%s] " content emojid - in - El.set_prop value (Jstr.of_string new_content) textarea; - El.set_has_focus true textarea - -(* make image description field visible when a file is selected*) -let make_visible el _event = El.set_class (Jstr.of_string "off") false el - -let add_events _load = - log "add post_form events @\n"; - match find_by_id_opt "file" with - | None -> log "element `file` not found, not logged in?@\n" - | Some file_input -> - let alt_input = find_by_id "alt" in - let alt_label = find_by_id "alt-label" in - let change = Ev.Type.create (Jstr.of_string "change") in - let (_ : Ev.listener) = - Ev.listen change (make_visible alt_input) (El.as_target file_input) - in - let (_ : Ev.listener) = - Ev.listen change (make_visible alt_label) (El.as_target file_input) - in - log "add inser_quote event on post links@\n"; - add_event_to_class Ev.click "quote-link" insert_quote - -(*make events after page load*) -let () = - let (_ : Ev.listener) = - Ev.listen Ev.load add_events (Window.as_target G.window) - in - () diff --git a/src/js/pretty_post.ml b/src/js/pretty_post.ml deleted file mode 100644 index b189028..0000000 --- a/src/js/pretty_post.ml +++ /dev/null @@ -1,203 +0,0 @@ -open Brr -open Utils - -type image_size = - | Big - | Small - -let of_string = function - | "post-image" -> Some Small - | "post-image-big" -> Some Big - | _ -> None - -let to_string = function Small -> "post-image" | Big -> "post-image-big" - -(*change postImage class to make it bigger/smaller on click*) -let image_click post_image event = - log "image_click@\n"; - let class_jstr = Jstr.of_string "class" in - let current_class = - match El.at class_jstr post_image with - | None -> failwith "no class for post_image" - | Some c -> Jstr.to_string c - in - let new_class = - match of_string current_class with - | Some image_size -> ( match image_size with Big -> Small | Small -> Big ) - | None -> failwith "invalid image class name" - in - El.set_at class_jstr (Some (Jstr.of_string (to_string new_class))) post_image; - let id = - match El.at (Jstr.of_string "data-id") post_image with - | None -> failwith "no data-id on post_image" - | Some id -> Jstr.to_string id - in - let src = - match new_class with - | Small -> Format.sprintf "/img/s/%s" id - | Big -> Format.sprintf "/img/%s" id - in - El.set_at (Jstr.of_string "src") (Some (Jstr.of_string src)) post_image; - (*prevent redirect to /img/:img*) - Ev.prevent_default event; - Ev.stop_propagation event - -let render_time date_span = - log "render time@\n"; - let data_time = - match El.at (Jstr.of_string "data-time") date_span with - | None -> failwith "no attribute data-time for date element" - | Some data_time -> Jstr.to_float data_time - in - let t = Unix.localtime data_time in - let date = - Format.sprintf "%02d-%02d-%02d %02d:%02d" (1900 + t.tm_year) (1 + t.tm_mon) - t.tm_mday t.tm_hour t.tm_min - in - let inner_html = El.Prop.jstr (Jstr.of_string "innerHTML") in - El.set_prop inner_html (Jstr.of_string date) date_span - -let preview_ref = ref None - -let highlighted_ref = ref None - -let selected_ref = ref None - -let on_hashchange _event = - log "on hashchange"; - let frag = Jstr.to_string @@ Uri.fragment @@ Window.location G.window in - if frag = "" then () - else - match find_by_id_opt frag with - | None -> log "fragment not found on the page" - | Some reply -> - let () = - match !selected_ref with - | None -> () - | Some item -> El.set_class (Jstr.of_string "selected") false item - in - El.set_class (Jstr.of_string "selected") true reply; - selected_ref := Some reply - -let clone_element el = - (* TODO: how to clone with Brr? *) - let id = - match El.at (Jstr.of_string "id") el with - | None -> failwith "element as no id for cloning" - | Some id -> Jstr.to_string id - in - (* get reply_div as a Jv.t *) - let original_div = Jv.get Jv.global id in - let div = Jv.call original_div "cloneNode" [| Jv.of_bool true |] in - ignore - @@ Jv.call div "setAttribute" - [| Jv.of_string "id"; Jv.of_string "floating-reply-preview" |]; - ignore - @@ Jv.call div "setAttribute" - [| Jv.of_string "class"; Jv.of_string "post highlight" |]; - - (* append to DOM *) - (* we needs to add it to `body` and not `original_div` or it might change the display - * and do buggy things with mouse events on `original_div`*) - let document = Jv.get Jv.global "document" in - let body = Jv.get document "body" in - ignore @@ Jv.call body "append" [| div |]; - (* go back to El *) - match find_by_id_opt "floating-reply-preview" with - | None -> failwith "error cloning element" - | Some el -> el - -let on_mouse_over el _event = - log "on mouse over@\n"; - - let reply_id = - match El.at (Jstr.of_string "data-id") el with - | None -> failwith "no data-id on element" - | Some data_id -> Jstr.to_string data_id - in - - match find_by_id_opt reply_id with - | None -> failwith "error getting reply_div, this reply is not on this page" - | Some reply_div -> - (* check if it in view, if it is, just make it of class `highlight` *) - let window_height = - let window = Jv.get Jv.global "window" in - Jv.get window "innerHeight" |> Jv.to_int - in - let reply_top = El.bound_y reply_div |> int_of_float in - if reply_top < window_height - 50 && reply_top + 50 > 0 then ( - (* just highlight if reply is in viewport *) - El.set_class (Jstr.of_string "highlight") true reply_div; - highlighted_ref := Some reply_div ) - else - (* copy it to make new div `floating-reply-preview` *) - let preview_div = clone_element reply_div in - - (* place it next to the reply-link el*) - let top = - let el_top = El.bound_y el in - let h = El.bound_h preview_div in - (* clamp to viewport *) - let top = - Float.min - (el_top -. (0.5 *. h)) - (float_of_int window_height -. h -. 7.0) - in - let top = Float.max top 0.0 in - top |> int_of_float |> Format.sprintf "%dpx" |> Jstr.of_string - in - let left = - El.bound_x el +. El.bound_w el - |> int_of_float |> Format.sprintf "%dpx" |> Jstr.of_string - in - El.set_inline_style El.Style.position (Jstr.of_string "fixed") preview_div; - El.set_inline_style El.Style.z_index (Jstr.of_string "42") preview_div; - El.set_inline_style El.Style.top top preview_div; - El.set_inline_style El.Style.left left preview_div; - (* also highlight class doesn't work if we set inline style idk why wtf css *) - El.set_inline_style El.Style.background_color (Jstr.of_string "#9dd162") - preview_div; - - (* set preview_div ref for on_mouse_out *) - preview_ref := Some preview_div - -let on_mouse_out _el _event = - log "on mouse out@\n"; - (* get the `reply-preview` element, delete it if Some*) - let () = - match !highlighted_ref with - | None -> () - | Some highlighted_div -> - El.set_class (Jstr.of_string "highlight") false highlighted_div - in - match !preview_ref with - | None -> () - | Some preview_div -> El.remove preview_div - -let make_pretty _event = - log "make pretty@\n"; - - let dates = El.find_by_class (Jstr.of_string "date") in - List.iter render_time dates; - - (*add event image_click to all postImage*) - let () = add_event_to_class Ev.click "post-image" image_click in - - (*add event mouse_over/out to all reply-link *) - let () = add_event_to_class Ev.mouseover "reply-link" on_mouse_over in - let () = add_event_to_class Ev.mouseout "reply-link" on_mouse_out in - - (* add fragment listener to mark as selected the linked post *) - let (_ : Ev.listener) = - Ev.listen Ev.hashchange on_hashchange (Window.as_target G.window) - in - (* call hashchange on page load too *) - on_hashchange () - -(*make pretty after page load*) -let () = - log "add load eventlistener to make pretty@\n"; - let (_ : Ev.listener) = - Ev.listen Ev.load make_pretty (Window.as_target G.window) - in - () diff --git a/src/js/thread.ml b/src/js/thread.ml deleted file mode 100644 index e69de29..0000000 diff --git a/src/js/utils.ml b/src/js/utils.ml deleted file mode 100644 index 5b85fc3..0000000 --- a/src/js/utils.ml +++ /dev/null @@ -1,18 +0,0 @@ -open Brr - -let log = Format.printf - -let find_by_id_opt id = Document.find_el_by_id G.document (Jstr.of_string id) - -let find_by_id id = - match find_by_id_opt id with - | None -> failwith (Format.sprintf "element `%s` not found" id) - | Some el -> el - -let add_event_to_class event name handler = - let el_list = El.find_by_class (Jstr.of_string name) in - List.iter - (fun el -> - let (_ : Ev.listener) = Ev.listen event (handler el) (El.as_target el) in - () ) - el_list diff --git a/src/json_data.ml b/src/json_data.ml new file mode 100644 index 0000000..58f122c --- /dev/null +++ b/src/json_data.ml @@ -0,0 +1,364 @@ +open Data_encoding +open Types + +type nonrec 'a result = ('a, string) result + +let internal_err = + let open Err in + union + [ (let title = "No_msg" in + case ~title (Tag 0) + (obj1 (req title unit)) + (function No_msg -> Some () | _ -> None) + (fun () -> No_msg) ) + ; (let title = "Db" in + case ~title (Tag 1) + (obj1 (req title string)) + (function Db s -> Some s | _ -> None) + (fun s -> Db s) ) + ; (let title = "Db_not_found" in + case ~title (Tag 2) + (obj1 (req title string)) + (function Db_not_found s -> Some s | _ -> None) + (fun s -> Db_not_found s) ) + ; (let title = "Bos" in + case ~title (Tag 3) + (obj1 (req title string)) + (function Bos s -> Some s | _ -> None) + (fun s -> Bos s) ) + ; (let title = "Conan" in + case ~title (Tag 4) + (obj1 (req title string)) + (function Conan s -> Some s | _ -> None) + (fun s -> Conan s) ) + ] + +let err = + let open Err in + union + [ (let title = "Internal" in + case ~title (Tag 0) internal_err + (function Internal o -> Some o | _ -> None) + (fun o -> Internal o) ) + ; (let title = "Bad_form" in + case ~title (Tag 1) + (obj1 (req title unit)) + (function Bad_form -> Some () | _ -> None) + (fun () -> Bad_form) ) + ; (let title = "Bad_form_suspicious" in + case ~title (Tag 2) + (obj1 (req title unit)) + (function Bad_form_suspicious -> Some () | _ -> None) + (fun () -> Bad_form_suspicious) ) + ; (let title = "Unauthorized" in + case ~title (Tag 3) + (obj1 (req title unit)) + (function Unauthorized -> Some () | _ -> None) + (fun () -> Unauthorized) ) + ; (let title = "Unauthorized_login" in + case ~title (Tag 4) + (obj1 (req title string)) + (function Unauthorized_login s -> Some s | _ -> None) + (fun s -> Unauthorized_login s) ) + ; (let title = "Forbidden" in + case ~title (Tag 5) + (obj1 (req title unit)) + (function Forbidden -> Some () | _ -> None) + (fun () -> Forbidden) ) + ; (let title = "Not_found" in + case ~title (Tag 6) + (obj1 (req title unit)) + (function Not_found -> Some () | _ -> None) + (fun () -> Not_found) ) + ; (let title = "Not_found_thread" in + case ~title (Tag 7) + (obj1 (req title int31)) + (function Not_found_thread s -> Some s | _ -> None) + (fun s -> Not_found_thread s) ) + ; (let title = "Not_found_post" in + case ~title (Tag 8) + (obj1 (req title int31)) + (function Not_found_post s -> Some s | _ -> None) + (fun s -> Not_found_post s) ) + ; (let title = "Not_found_user" in + case ~title (Tag 9) + (obj1 (req title string)) + (function Not_found_user s -> Some s | _ -> None) + (fun s -> Not_found_user s) ) + ; (let title = "Not_found_image" in + case ~title (Tag 10) + (obj1 (req title int31)) + (function Not_found_image s -> Some s | _ -> None) + (fun s -> Not_found_image s) ) + ; (let title = "Unprocessable" in + case ~title (Tag 11) + (obj1 (req title string)) + (function Unprocessable s -> Some s | _ -> None) + (fun s -> Unprocessable s) ) + ] + +let img_info = + conv + (fun { md5; mime; w; h; thumb_w; thumb_h; name; alt } -> + (md5, mime, w, h, thumb_w, thumb_h, name, alt) ) + (fun (md5, mime, w, h, thumb_w, thumb_h, name, alt) -> + { md5; mime; w; h; thumb_w; thumb_h; name; alt } ) + (obj8 (req "md5" string) (req "mime" string) (req "w" int31) (req "h" int31) + (req "thumb_w" int31) (req "thumb_h" int31) (req "name" string) + (req "alt" string) ) + +let post = + conv_with_guard + (fun { id + ; parent_t_id + ; date + ; poster_id + ; poster_nick + ; comment + ; image_info + ; backlinks + } -> + ( id + , parent_t_id + , date + , poster_id + , poster_nick + , Comment.to_string comment + , image_info + , backlinks ) ) + (fun ( id + , parent_t_id + , date + , poster_id + , poster_nick + , comment_str + , image_info + , backlinks ) -> + let open Syntax in + let+ comment = Comment.of_string comment_str in + { id + ; parent_t_id + ; date + ; poster_id + ; poster_nick + ; comment + ; image_info + ; backlinks + } ) + (obj8 (req "id" int31) (req "parent_t_id" int31) (req "date" float) + (req "poster_id" string) (req "poster_nick" string) + (req "comment" string) + (req "image_info" (option img_info)) + (req "replies" (list int31)) ) + +let bump_status = + union + [ case ~title:"Dead" (Tag 0) + (obj1 (req "dead" empty)) + (function Dead -> Some () | _ -> None) + (fun () -> Dead) + ; case ~title:"Locked" (Tag 1) + (obj1 (req "locked" int31)) + (function Locked c -> Some c | _ -> None) + (fun c -> Locked c) + ; case ~title:"Alive" (Tag 2) + (obj1 (req "alive" int31)) + (function Alive c -> Some c | _ -> None) + (fun c -> Alive c) + ] + +let thread = + conv + (fun { op; subject; lat; lng; bump_status; reply_count } -> + (op, subject, lat, lng, bump_status, reply_count) ) + (fun (op, subject, lat, lng, bump_status, reply_count) -> + { op; subject; lat; lng; bump_status; reply_count } ) + (obj6 (req "op" post) (req "subject" string) (req "lat" float) + (req "lng" float) + (req "bump_status" bump_status) + (req "reply_count" int31) ) + +let thread_w_reply = + let open Thread_w_reply in + conv + (fun { op; subject; lat; lng; bump_status; reply_count; reply_l } -> + (op, subject, lat, lng, bump_status, reply_count, reply_l) ) + (fun (op, subject, lat, lng, bump_status, reply_count, reply_l) -> + { op; subject; lat; lng; bump_status; reply_count; reply_l } ) + (obj7 (req "op" post) (req "subject" string) (req "lat" float) + (req "lng" float) + (req "bump_status" bump_status) + (req "reply_count" int31) + (req "reply_l" (list post)) ) + +let catalog : thread list encoding = + conv (fun l -> l) (fun l -> l) (obj1 (req "catalog" (list thread))) + +let report = + conv + (fun { report_id + ; report_date + ; reported_post + ; reporter_user_id + ; reporter_nick + ; reason + } -> + ( report_id + , report_date + , reported_post + , reporter_user_id + , reporter_nick + , reason ) ) + (fun ( report_id + , report_date + , reported_post + , reporter_user_id + , reporter_nick + , reason ) -> + { report_id + ; report_date + ; reported_post + ; reporter_user_id + ; reporter_nick + ; reason + } ) + (obj6 (req "report_id" string) (req "report_date" float) + (req "reported_post" post) + (req "reporter_user_id" string) + (req "reporter_nick" string) + (req "reason" string) ) + +let reports : report list encoding = + conv (fun o -> o) (fun o -> o) (obj1 (req "reports" (list report))) + +let user = + conv + (fun { user_id; user_nick; user_is_admin; bio; avatar_info } -> + (user_id, user_nick, user_is_admin, bio, avatar_info) ) + (fun (user_id, user_nick, user_is_admin, bio, avatar_info) -> + { user_id; user_nick; user_is_admin; bio; avatar_info } ) + (obj5 (req "user_id" string) (req "user_nick" string) + (req "user_is_admin" bool) (req "bio" string) + (req "avatar_info" (option img_info)) ) + +let user_private = + let open User_private in + conv + (fun { user_id; user_nick; user_is_admin; bio; avatar_info; email } -> + (user_id, user_nick, user_is_admin, bio, avatar_info, email) ) + (fun (user_id, user_nick, user_is_admin, bio, avatar_info, email) -> + { user_id; user_nick; user_is_admin; bio; avatar_info; email } ) + (obj6 (req "user_id" string) (req "user_nick" string) + (req "user_is_admin" bool) (req "bio" string) + (req "avatar_info" (option img_info)) + (req "email" string) ) + +let geojson_marker : (float * float * post_id) encoding = + let geometry = + conv + (* !! geojson coordinates are lng first then lat *) + (fun (lat, lng) -> ((), [ lng; lat ]) ) + (fun ((), coordinates) -> + match coordinates with [ lng; lat ] -> (lat, lng) | _ -> assert false ) + (obj2 + (req "type" (constant "Point")) + (req "coordinates" (Fixed.list 2 float)) ) + in + let properties = conv (fun id -> id) (fun id -> id) (obj1 (req "id" int31)) in + conv + (fun (lat, lng, id) -> ((), (lat, lng), id)) + (fun ((), (lat, lng), id) -> (lat, lng, id)) + (obj3 + (req "type" (constant "Feature")) + (req "geometry" geometry) + (req "properties" properties) ) + +let geojson_markers : (float * float * post_id) list encoding = + conv + (fun l -> ((), l)) + (fun ((), l) -> l) + (obj2 + (req "type" (constant "FeatureCollection")) + (req "features" (list geojson_marker)) ) + +let session : session encoding = + conv + (fun { user_private; csrf_token; csrf_time_limit } -> + (user_private, csrf_token, csrf_time_limit) ) + (fun (user_private, csrf_token, csrf_time_limit) -> + { user_private; csrf_token; csrf_time_limit } ) + (obj3 + (req "user_private" (option user_private)) + (req "csrf_token" string) + (req "csrf_time_limit" float) ) + +let unit : unit encoding = + conv (fun o -> o) (fun o -> o) (obj1 (req "unit" unit)) + +let to_string enc = + let json = Data_encoding.Json.construct enc in + fun v -> Data_encoding.Json.to_string (json v) + +let of_string enc = + let destruct = Data_encoding.Json.destruct enc in + fun s -> Data_encoding.Json.from_string s |> Result.map destruct + +module Read = struct + let err = of_string err + + let session = of_string session + + let img_info = of_string img_info + + let post = of_string post + + let bump_status = of_string bump_status + + let thread = of_string thread + + let thread_w_reply = of_string thread_w_reply + + let catalog = of_string catalog + + let reports = of_string reports + + let user = of_string user + + let user_private = of_string user_private + + let geojson_marker = of_string geojson_marker + + let geojson_markers = of_string geojson_markers + + let unit = of_string unit +end + +module Write = struct + let err = to_string err + + let session = to_string session + + let img_info = to_string img_info + + let post = to_string post + + let bump_status = to_string bump_status + + let thread = to_string thread + + let thread_w_reply = to_string thread_w_reply + + let catalog = to_string catalog + + let reports = to_string reports + + let user = to_string user + + let user_private = to_string user_private + + let geojson_marker = to_string geojson_marker + + let geojson_markers = to_string geojson_markers + + let unit = to_string unit +end diff --git a/src/json_data.mli b/src/json_data.mli new file mode 100644 index 0000000..e9a4209 --- /dev/null +++ b/src/json_data.mli @@ -0,0 +1,68 @@ +(* TODO + - clean up unused + - expose pp? + - test (later) + *) +open Types + +type nonrec 'a result = ('a, string) result + +module Read : sig + val err : string -> Err.t result + + val img_info : string -> img_info result + + val post : string -> post result + + val bump_status : string -> bump_status result + + val thread : string -> thread result + + val thread_w_reply : string -> Thread_w_reply.t result + + val catalog : string -> thread list result + + val reports : string -> report list result + + val user : string -> user result + + val user_private : string -> User_private.t result + + val geojson_marker : string -> (float * float * post_id) result + + val geojson_markers : string -> (float * float * post_id) list result + + val session : string -> session result + + val unit : string -> unit result +end + +module Write : sig + val err : Err.t -> string + + val img_info : img_info -> string + + val post : post -> string + + val bump_status : bump_status -> string + + val thread : thread -> string + + val thread_w_reply : Thread_w_reply.t -> string + + val catalog : thread list -> string + + val reports : report list -> string + + val user : user -> string + + val user_private : User_private.t -> string + + val geojson_marker : float * float * post_id -> string + + val geojson_markers : (float * float * post_id) list -> string + + val session : session -> string + + val unit : unit -> string +end diff --git a/src/login.eml.html b/src/login.eml.html deleted file mode 100644 index 4337869..0000000 --- a/src/login.eml.html +++ /dev/null @@ -1,20 +0,0 @@ -let f request = - -% let url = -% match Dream.query request "redirect" with -% | None -> "/login" -% | Some r -> -% Format.sprintf "/login?redirect=%s" r -% in -<%s! Dream.form_tag ~action:url request %> -
    - - -
    What is you nickname or email?
    -
    -
    - - -
    - - diff --git a/src/moderation.ml b/src/moderation.ml new file mode 100644 index 0000000..ca67a82 --- /dev/null +++ b/src/moderation.ml @@ -0,0 +1,107 @@ +open Syntax +open Types +open Caqti_request.Infix +open Caqti_type +open Caqti_db + +(* TODO do something for multiples report on same post + - don't allow multiple report on same post from same user + - add TEST *) + +let () = + let tables = + [| (unit ->. unit) + "CREATE TABLE IF NOT EXISTS report (report_id TEXT, date FLOAT, \ + post_id INTEGER, user_id TEXT, reason TEXT, FOREIGN KEY(post_id) \ + REFERENCES post(id) ON DELETE CASCADE, FOREIGN KEY(user_id) \ + REFERENCES user(user_id) ON DELETE CASCADE)" + ; (unit ->. unit) + "CREATE TABLE IF NOT EXISTS banished (nick TEXT, email TEXT)" + |] + in + Array.iter (fun query -> Db.exec_unsafe query ()) tables + +module Q = struct + let upload_report = + Db.exec + ((t5 string float int string string ->. unit) + "INSERT INTO report VALUES (?,?,?,?,?)" ) + + let get_reports_all = + Db.collect_list + ((unit ->* t6 string float int string string string) + "SELECT r.report_id, r.date, r.post_id, r.user_id, u.nick, reason \ + FROM report r JOIN user u ON r.user_id = u.user_id" ) + + let get_reports_made_by = + Db.collect_list + ((string ->* t6 string float int string string string) + "SELECT r.report_id, r.date, r.post_id, r.user_id, u.nick, reason \ + FROM report r JOIN user u ON r.user_id = u.user_id WHERE r.user_id=?" ) + + let delete_report = + Db.exec @@ (int ->. unit) "DELETE FROM report WHERE post_id=?" + + let upload_banished = + Db.exec @@ (t2 string string ->. unit) "INSERT INTO banished VALUES (?,?)" + + let find_banished = + Db.find_opt + @@ (t2 string string ->! t2 string string) + "SELECT * FROM banished WHERE nick=? OR email=?" +end + +let get_report_aux + ( report_id + , report_date + , reported_post_id + , reporter_user_id + , reporter_nick + , reason ) = + let+ reported_post = Post.get_post reported_post_id in + { report_id + ; report_date + ; reported_post + ; reporter_user_id + ; reporter_nick + ; reason + } + +let get_reports_all () = + Db.do_transaction @@ fun () -> + let* l = Q.get_reports_all () in + list_map get_report_aux l + +let get_reports_made_by user_id = + Db.do_transaction @@ fun () -> + let* l = Q.get_reports_made_by user_id in + list_map get_report_aux l + +let make_report ~reporter_user_id ~reason reported_post_id = + let* reason = Validate_str.report reason in + (* check post exists *) + let* _post = Post.get_post reported_post_id in + let report_date = Unix.time () in + let report_id = Util.gen_uuid () in + Db.do_transaction @@ fun () -> + Q.upload_report + ( report_id + , report_date + , reported_post_id + , reporter_user_id + , (reason :> string) ) + +(* todo sql: no need to use transaction for a single query? *) +let delete_report id = Db.do_transaction @@ fun () -> Q.delete_report id + +let is_banished login = + let+ opt = Db.do_transaction @@ fun () -> Q.find_banished (login, login) in + match opt with None -> false | Some _ -> true + +(* it would be better to also invalidate banned user's session here + since Api.get_logged_user check for user existance it should be fine *) +let banish user_id = + let* user_private = User.get_user_private user_id in + let* () = User.delete_user user_id in + Db.do_transaction @@ fun () -> + Q.upload_banished (user_private.user_nick, user_private.email) diff --git a/src/permap.ml b/src/permap.ml index fd9a251..944a701 100644 --- a/src/permap.ml +++ b/src/permap.ml @@ -1,436 +1,151 @@ -open Utils open Syntax +(* TODO http cache *) +let cache_max_age = 0 + +(* TODO https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html *) +let log_err = + let open Err in + let _debug ~request e = Dream.debug (fun log -> log ~request "%a" pp e) in + let info ~request e = Dream.info (fun log -> log ~request "%a" pp e) in + let warning ~request e = Dream.warning (fun log -> log ~request "%a" pp e) in + let error ~request e = Dream.error (fun log -> log ~request "%a" pp e) in + fun request (e : Err.t) -> + if Config_serv.custom_logger then + match e with + | Internal _ -> error ~request e + | Bad_form_suspicious | Unauthorized_login _ -> warning ~request e + | _ -> info ~request e + +let status_of_err (err : Err.t) = + let open Err in + match err with + | Internal _ -> `Internal_Server_Error + | Bad_form | Bad_form_suspicious -> `Bad_Request + | Unauthorized | Unauthorized_login _ -> `Unauthorized + | Forbidden -> `Forbidden + | Not_found | Not_found_thread _ | Not_found_post _ | Not_found_user _ + | Not_found_image _ -> + `Not_Found + | Unprocessable _ -> `Bad_Request + +let render_result_img request ~headers res = + let headers = [ ("Content-Type", "image") ] @ headers in + match res with + | Ok v -> Dream.respond ~headers v + | Error e -> + log_err request e; + let e = Err.hide_internal_err_detail e in + let status = status_of_err e in + let body = Fmt.str "%a" Err.pp e in + Dream.respond ~headers ~status body + +let render_result_json request ~headers res = + match res with + | Ok v -> Dream.json ~headers v + | Error e -> + log_err request e; + let e = Err.hide_internal_err_detail e in + let status = status_of_err e in + let body = Json_data.Write.err e in + Dream.json ~headers ~status body + let asset_loader _root path _request = - match Content.read ("assets/" ^ path) with + match Assets.read path with | None -> Dream.empty `Not_Found | Some asset -> - Dream.respond ~headers:[ ("Cache-Control", "max-age=151200") ] asset - -let page name request = - match Content.read (name ^ ".md") with - | None -> Dream.empty `Not_Found - | Some page -> - let content = Omd.of_string page |> Omd.to_html in - render content request - -let about request = page "about" request - -let register_get request = render (Register.f request) request - -let register_post request = - match%lwt Dream.form request with - | `Ok [ ("email", email); ("nick", nick); ("password", password) ] -> ( - match User.register ~email ~nick ~password with - | Error e -> render e request - | Ok () -> - let res = - Result.fold ~error:Fun.id - ~ok:(fun _ -> "User created ! Welcome !") - (User.login ~login:nick ~password request) - in - render res request ) - | form -> Utils.handle_invalid_form form - -let login_get request = render (Login.f request) request - -let login_post request = - match%lwt Dream.form request with - | `Ok [ ("login", login); ("password", password) ] -> ( - match User.login ~login ~password request with - | Error e -> render e request - | Ok () -> - let url = - match Dream.query request "redirect" with - | None -> "/" - | Some redirect -> Dream.from_percent_encoded redirect - in - Dream.respond ~status:`See_Other - ~headers:[ ("Location", url) ] - "Logged in: Happy geo-posting!" ) - | form -> Utils.handle_invalid_form form - -let admin_get request = - match Dream.session "user_id" request with - | None -> - let redirect_url = - Format.sprintf "/login?redirect=%s" (Dream.to_percent_encoded "/admin") - in - Dream.respond ~status:`See_Other ~headers:[ ("Location", redirect_url) ] "" - | Some user_id -> - if not (User.is_admin user_id) then Dream.respond ~status:`Forbidden "" - else - let res = - match Babillard.get_reports () with - | Error e -> e - | Ok (posts, reports) -> - Pp_babillard.admin_page_content posts reports request - in - render res request - -let admin_post request = - Utils.logged_in_or_redirect request (fun user_id -> - if not (User.is_admin user_id) then Dream.respond ~status:`Forbidden "" - else - match%lwt Dream.form request with - | `Ok [ ("action", action); ("post_id", id) ] -> ( - (* TODO: use let* and Utils.render_result ? *) - let res = - match Babillard.get_post id with - | Error _e as e -> e - | Ok post -> ( - let evil_user_id = post.user_id in - match Babillard.moderation_action_from_string action with - | None -> Error "Invalid action" - | Some action -> ( - match action with - | Delete -> Babillard.try_delete_post ~user_id:evil_user_id id - | Banish -> User.banish evil_user_id - | Ignore -> Babillard.ignore_report id ) ) - in - match res with - | Error e -> render e request - | Ok () -> - (* TODO: ??? *) - Dream.respond ~status:`See_Other - ~headers:[ ("Location", "/admin") ] - "" ) - | form -> Utils.handle_invalid_form form ) - -let catalog request = - let catalog_content = - Result.fold ~ok:Fun.id ~error:Fun.id (Pp_babillard.catalog_content ()) - in - render (Catalog_page.f catalog_content) request - -let delete_get request = - let post_id = Dream.param request "post_id" in - let post_preview = - Result.fold ~ok:Fun.id ~error:Fun.id (Pp_babillard.view_post post_id) - in - render (Delete_page.f post_preview post_id request) request - -let delete_post request = - Utils.logged_in_or_redirect request (fun user_id -> - (* match on Dream.form needed for hidden csrf field *) - match%lwt Dream.form request with - | `Ok [] -> ( - (* TODO: use let* and Utils.render_result ? *) - let post_id = Dream.param request "post_id" in - match Babillard.try_delete_post ~user_id post_id with - | Error e -> render e request - | Ok () -> - Dream.respond ~status:`See_Other - ~headers:[ ("Location", "/") ] - "Your post was deleted!" ) - | form -> Utils.handle_invalid_form form ) - -let report_get request = - let post_id = Dream.param request "post_id" in - let post_preview = - Result.fold ~ok:Fun.id ~error:Fun.id (Pp_babillard.view_post post_id) - in - render (Report_page.f post_preview post_id request) request - -let report_post request = - Utils.logged_in_or_redirect request (fun user_id -> - match%lwt Dream.form request with - | `Ok [ ("reason", reason) ] -> - Utils.render_result request - @@ - let post_id = Dream.param request "post_id" in - let* () = Babillard.report ~user_id ~reason post_id in - Ok "The post was reported!" - | form -> Utils.handle_invalid_form form ) - -let user request = - render (Result.fold ~ok:Fun.id ~error:Fun.id (User.list ())) request - -let user_profile request = - let nick = Dream.param request "user" in - match User.get_id_from_nick nick with - | Error _e -> Dream.respond ~status:`Not_Found "User does not exists" - | Ok user_id -> render_result request @@ User.public_profile user_id - -let logout request = - let _ = Dream.invalidate_session request in - let content = "Logged out !" in - render content request - -let account_get request = - Utils.logged_in_or_redirect request (fun user_id -> - Utils.render_result request - @@ let* user = User.get_user user_id in - Ok (User_account.f user request) ) - -(*TODO ask for password *) -let account_post request = - Utils.logged_in_or_redirect request (fun user_id -> - match%lwt Dream.form request with - | `Ok [ ("delete", _) ] -> - Utils.render_result request - @@ (*TODO ask for confirmation *) - let* () = User.delete_user user_id in - let _unit_lwt = Dream.invalidate_session request in - Ok "Your account was deleted" - | `Ok [ ("email", email) ] -> - Utils.render_result request - @@ let* () = User.update_email email user_id in - Ok "Your email was updated!" - | `Ok - [ ("confirm-new-password", confirm_password) - ; ("new-password", password) - ] -> - Utils.render_result request - @@ - if password = confirm_password then - let* () = User.update_password password user_id in - Ok "Your password was updated!" - else Error "Password confirmation does not match" - | form -> Utils.handle_invalid_form form ) - -let profile_get request = - Utils.logged_in_or_redirect request (fun user_id -> - Utils.render_result request - @@ let* user = User.get_user user_id in - Ok (User_profile.f user request) ) - -let profile_post request = - Utils.logged_in_or_redirect request (fun user_id -> - match%lwt Dream.form request with - | `Ok [ ("bio", bio) ] -> ( - match User.update_bio bio user_id with - | Ok () -> - Dream.respond ~status:`See_Other - ~headers:[ ("Location", "/profile") ] - "Your bio was updated!" - | Error e -> render e request ) - | `Ok [ ("nick", nick) ] -> ( - match User.update_nick nick user_id with - | Ok () -> - Dream.respond ~status:`See_Other - ~headers:[ ("Location", "/profile") ] - "Your display nick was updated!" - | Error e -> render e request ) - | `Ok [ ("content", content); ("count", count); ("label", label) ] -> ( - match int_of_string_opt count with - | None -> render "Error: invalid count" request - | Some count -> ( - match User.update_metadata count label content user_id with - | Ok () -> - Dream.respond ~status:`See_Other - ~headers:[ ("Location", "/profile") ] - "Your display nick was updated!" - | Error e -> render e request ) ) - | `Ok _ -> Dream.respond ~status:`Bad_Request "invalid form" - | `Many_tokens _ | `Missing_token _ | `Invalid_token _ | `Wrong_session _ - | `Expired _ | `Wrong_content_type -> ( - (* TODO: why is this here ?! *) - match%lwt Dream.multipart request with - | `Ok [ ("file", file) ] -> ( - match User.upload_avatar file user_id with - | Ok () -> - Dream.respond ~status:`See_Other - ~headers:[ ("Location", "/profile") ] - "Your avatar was updated!" - | Error e -> render e request ) - | form -> Utils.handle_invalid_form form ) ) + Dream.respond + ~headers:[ ("Cache-Control", Fmt.str "max-age=%d" cache_max_age) ] + asset let get_post_image ~thumbnail request = - let id = Dream.param request "id" in - let image = - (if thumbnail then Image.get_thumbnail else Image.get_content) id + (* posts images do not change so we cache them + todo don't cache if not found? *) + let headers = + [ ("Cache-Control", Fmt.str "max-age=%d, immutable" cache_max_age) ] in - match image with - | Error e -> render e request - | Ok image_opt -> ( - match image_opt with - | None -> Dream.respond ~status:`Not_Found "Image does not exists" - | Some image -> - (* posts images do not change so we cache them *) - Dream.respond - ~headers: - [ ("Cache-Control", "max-age=3628800, immutable") - ; ("Content-Type", "image") - ] - image ) + let f = if thumbnail then Post.get_thumbnail_data else Post.get_image_data in + let res = + let* image_id = Api.url_param request Post_image_id in + f image_id + in + render_result_img request ~headers res let get_avatar_image request = - let nick = Dream.param request "user" in - match User.get_id_from_nick nick with - | Error _e -> Dream.respond ~status:`Not_Found "User does not exists" - | Ok user_id -> ( - let avatar = Image.get_user_content user_id in - match avatar with - | Ok (Some avatar) -> - Dream.respond ~headers:[ ("Content-Type", "image") ] avatar - | Ok None -> ( - match Content.read "/assets/img/default_avatar.png" with - | None -> failwith "can't find default avatar" - | Some avatar -> - Dream.respond ~headers:[ ("Content-Type", "image") ] avatar ) - | Error e -> render e request ) - -let markers request = - let markers = Pp_babillard.get_markers () in - match markers with - | Ok markers -> - Dream.respond ~headers:[ ("Content-Type", "application/json") ] markers - | Error e -> render e request - -let babillard_get request = render (Babillard_page.f request) request - -let babillard_post request = - Utils.logged_in_or_redirect request (fun user_id -> - match%lwt Dream.multipart request with - | `Ok - [ ("alt", [ (_, alt) ]) - ; ("category", categories) - ; ("comment", [ (_, comment) ]) - ; ("file", file) - ; ("lat-input", [ (_, lat) ]) - ; ("lng-input", [ (_, lng) ]) - ; ("subject", [ (_, subject) ]) - ; ("tags", [ (_, tags) ]) - ] - | `Ok - ( ("alt", [ (_, alt) ]) - :: ("comment", [ (_, comment) ]) - :: ("file", file) - :: ("lat-input", [ (_, lat) ]) - :: ("lng-input", [ (_, lng) ]) - :: ("subject", [ (_, subject) ]) - :: ("tags", [ (_, tags) ]) - :: ([] as categories) ) -> ( - let categories = List.map snd categories in - match (Float.of_string_opt lat, Float.of_string_opt lng) with - | None, _ -> render "Invalide coordinate" request - | _, None -> render "Invalide coordinate" request - | Some lat, Some lng -> ( - let op_or_reply_data = `Op_data (categories, subject, lat, lng) in - let res = - match file with - | [] -> Babillard.make_post ~comment ~tags ~op_or_reply_data user_id - | _ :: _ :: _ -> Error "More than one image" - | [ (image_name, image_content) ] -> - let image_input = (image_name, alt, image_content) in - Babillard.make_post ~comment ~image_input ~tags ~op_or_reply_data - user_id - in - match res with - | Ok thread_id -> - let adress = Format.asprintf "/thread/%s" thread_id in - Dream.respond ~status:`See_Other - ~headers:[ ("Location", adress) ] - "Your thread was posted!" - | Error e -> render e request ) ) - | form -> Utils.handle_invalid_form form ) - -let thread_feed_get request = - let thread_id = Dream.param request "thread_id" in - match Pp_babillard.feed thread_id with - | Error e -> render e request - | Ok feed -> - Dream.respond ~headers:[ ("Content-Type", "application/atom+xml") ] feed - -let thread_get request = - let thread_id = Dream.param request "thread_id" in - let thread_view = Pp_babillard.view_thread thread_id in - render - ( match thread_view with - | Error e -> e - | Ok thread_view -> Thread_page.f thread_view thread_id request ) - request - -(*form to reply to a thread *) -let reply_post request = - Utils.logged_in_or_redirect request (fun user_id -> - match%lwt Dream.multipart request with - | `Ok - [ ("alt", [ (_, alt) ]) - ; ("comment", [ (_, comment) ]) - ; ("file", file) - ; ("tags", [ (_, tags) ]) - ] -> ( - let parent_id = Dream.param request "thread_id" in - let op_or_reply_data = `Reply_data parent_id in - let res = - match file with - | [] -> Babillard.make_post ~comment ~tags ~op_or_reply_data user_id - | [ (image_name, image_content) ] -> - let image_input = (image_name, alt, image_content) in - Babillard.make_post ~comment ~image_input ~tags ~op_or_reply_data - user_id - | _ :: _ :: _ -> Error "More than one image" - in - match res with - | Ok post_id -> - let adress = Format.sprintf "/thread/%s#%s" parent_id post_id in - Dream.respond ~status:`See_Other - ~headers:[ ("Location", adress) ] - "Your reply was posted!" - | Error e -> render e request ) - | form -> Utils.handle_invalid_form form ) - -let error_template _error _debug_info response = - let status = Dream.status response in - let code = Dream.status_to_int status in - - (*TODO improve: can't use template.elm.html because it needs "request" *) - let%lwt body = Dream.body response in - let reason = - if String.equal "" body then Dream.status_to_string status else body + let res = + let* user_id = Api.url_param request User_id in + User.get_image user_id in - Dream.set_body response (Format.sprintf "%d: %s" code reason); - Lwt.return response + render_result_img request ~headers:[] res + +(* -- ~~ -- *) + +let app_start_page _request = + Html.app_start_page |> Html.page_to_string |> Dream.html let routes = - (* this is just so that they're visually aligned *) - let get_ = Dream.get in - let post = Dream.post in + (* wrap api function with render_result_json *) + let get_ path f = + let handler request = render_result_json request ~headers:[] (f request) in + Dream.get path handler + in + let post path f = + let handler request = + Lwt.bind (f request) (render_result_json request ~headers:[]) + in + Dream.post path handler + in - [ get_ "/" babillard_get - ; post "/" babillard_post - ; get_ "/about" about - ; get_ "/account" account_get - ; post "/account" account_post - ; get_ "/admin" admin_get - ; post "/admin" admin_post - ; get_ "/assets/**" (Dream.static ~loader:asset_loader "") - ; get_ "/catalog" catalog - ; get_ "/delete/:post_id" delete_get - ; post "/delete/:post_id" delete_post - ; get_ "/discuss" Discuss.render - ; get_ "/discuss/:comrade_id" Discuss.renderone - ; post "/discuss/:comrade_id" Discuss.post - ; get_ "/img/:id" (get_post_image ~thumbnail:false) - ; get_ "/img/s/:id" (get_post_image ~thumbnail:true) - ; get_ "/login" login_get - ; post "/login" login_post - ; get_ "/logout" logout - ; get_ "/markers" markers - ; get_ "/profile" profile_get - ; post "/profile" profile_post - ; get_ "/report/:post_id" report_get - ; post "/report/:post_id" report_post - ; get_ "/thread/:thread_id" thread_get - ; post "/thread/:thread_id" reply_post - ; get_ "/thread/:thread_id/feed" thread_feed_get - ; get_ "/user" user - ; get_ "/user/:user" user_profile - ; get_ "/user/:user/avatar" get_avatar_image - ] - @ - if App.open_registration then - [ get_ "/register" register_get; post "/register" register_post ] - else [] + (* TODO + - origin_referrer_check + - custom CSRF header instead of having client insert csrf in form *) + Dream.scope "/api" [ (* middlewear *) ] + Api. + [ get_ "/catalog" GET.catalog + ; get_ "/thread/:thread_id" GET.thread_w_reply + ; get_ "/post/:post_id" GET.post + ; get_ "/admin" GET.admin + ; get_ "/user/:user_id" GET.user_page + ; get_ "/session" GET.session + ; post "/register" POST.register + ; post "/login" POST.login + ; post "/admin/ignore/:post_id" POST.admin_ignore + ; post "/admin/delete/:post_id" POST.admin_delete + ; post "/admin/banish/:user_id" POST.admin_banish + ; post "/profile" POST.profile + ; post "/account" POST.account + ; post "/logout" POST.logout + ; post "/" POST.new_thread + ; post "/thread/:thread_id" POST.reply + ; post "/delete/:post_id" POST.delete + ; post "/report/:post_id" POST.report + ] + :: [ Dream.get "/assets/**" (Dream.static ~loader:asset_loader "") + ; Dream.get "/img/:image_id" (get_post_image ~thumbnail:false) + ; Dream.get "/img/s/:image_id" (get_post_image ~thumbnail:true) + ; Dream.get "/user/:user_id/avatar" get_avatar_image + ; Dream.get "/**" app_start_page + ] let () = - let logger = if App.log then Dream.logger else Fun.id in - Dream.run ~port:App.port ~error_handler:(Dream.error_template error_template) - @@ logger @@ Dream.cookie_sessions - (* this should replace memory/cookie sessions but it doesn't work :-( - @@ Dream.sql_pool Db.db_uri - @@ Dream.sql_sessions - *) + let () = + let open Config_serv in + Dream.log "config file: %a" Fpath.pp config_path; + Dream.log "default_logger: %b" default_logger; + Dream.log "custom_logger: %b" custom_logger + in + let () = + let open Config in + Dream.log "open_registration: %b" open_registration; + Dream.log "hostname: %s" hostname; + Dream.log "port: %d" port + in + () + +let () = + let logger = if Config_serv.default_logger then Dream.logger else Fun.id in + Dream.run ~port:Config.port + @@ logger + @@ Dream.sql_pool (Uri.to_string Config_serv.db_uri) + @@ Dream.sql_sessions ~lifetime:(float_of_int Config.session_lifetime) @@ Dream.router routes diff --git a/src/post.ml b/src/post.ml new file mode 100644 index 0000000..d98d178 --- /dev/null +++ b/src/post.ml @@ -0,0 +1,84 @@ +open Syntax +open Types +open Err + +let get_post id = + let* opt = Db_post.find_post id in + match opt with None -> Error (Not_found_post id) | Some v -> Ok v + +let get_thread id = + let* opt = Db_post.find_thread id in + match opt with None -> Error (Not_found_thread id) | Some v -> Ok v + +let get_thread_w_reply id = + let* opt = Db_post.find_thread_w_reply id in + match opt with None -> Error (Not_found_thread id) | Some v -> Ok v + +let get_catalog () = Db_post.get_catalog () + +(* todo id type is string here.. *) +let get_thumbnail_data id = + let* opt = Db_image.P.thumbnail_data id in + match opt with None -> Error (Not_found_image id) | Some image -> Ok image + +let get_image_data id = + let* opt = Db_image.P.data id in + match opt with None -> Error (Not_found_image id) | Some image -> Ok image + +let get_image_info id = + let* opt = Db_image.P.info id in + match opt with None -> Error (Not_found_image id) | Some image -> Ok image + +let delete ~user id = + let* post = get_post id in + if user.user_is_admin || String.equal post.poster_id user.user_id then + Db_post.delete id + else Error Forbidden + +let not_empty image_opt comment = + if Option.is_some image_opt || String.length comment <> 0 then Ok () + else Error (Unprocessable "Your post must contain an image or a comment") + +let build_image image_data = + match image_data with + | None -> Ok None + | Some (name_opt, alt, content) -> + let name = Option.value ~default:"" name_opt in + let+ image = Image.build ~name ~alt content in + Some image + +let build_comment comment = + (* todo: move validation to Comment.parse *) + let* _comment = Validate_str.comment comment in + let+ comment = + Comment.of_string comment + |> Result.map_error (fun s -> Unprocessable (Fmt.str "comment: %s" s)) + in + comment + +let make_post ~comment ~image_data ~parent_thread user = + let* () = not_empty image_data comment in + let* thread_id = + match parent_thread.bump_status with + | Dead -> Error (Unprocessable "This thread is dead, you cannot reply.") + | Locked _rank -> + Error (Unprocessable "This thread is locked, you cannot reply.") + | Alive _rank -> Ok parent_thread.op.id + in + let* comment = build_comment comment in + let* image = build_image image_data in + let+ post = Db_post.add_post ~thread_id ~user ~image ~comment in + post + +let make_thread ~comment ~image_data ~subject ~lat ~lng user = + let* () = not_empty image_data comment in + let* subject = Validate_str.subject subject in + let* () = + (* TODO latlng validation *) + let is_valid_latlng = true in + if is_valid_latlng then Ok () else Error (Unprocessable "Invalid coordinate") + in + let* comment = build_comment comment in + let* image = build_image image_data in + let+ thread = Db_post.add_thread ~subject ~lat ~lng ~user ~image ~comment in + thread diff --git a/src/post_form.eml.html b/src/post_form.eml.html deleted file mode 100644 index 78d004c..0000000 --- a/src/post_form.eml.html +++ /dev/null @@ -1,34 +0,0 @@ -let f thread_id request = -% let action = match thread_id with |None -> "/" | Some id -> Format.sprintf "/thread/%s" id in -% let checkboxes = match thread_id with |None -> Format.asprintf "%a" Pp_babillard.pp_checkboxes () | Some _id -> "" in -
    -<%s! Dream.form_tag ~action ~enctype:`Multipart_form_data request %> -% begin if Option.is_none thread_id then - - - - - -% end; - - - - - <%s! checkboxes %> - - - - -
    - - - -% begin match thread_id with -% | None -> -
    - -% | Some _id -> - -% end; - -
    diff --git a/src/pp_babillard.ml b/src/pp_babillard.ml deleted file mode 100644 index d1c35c8..0000000 --- a/src/pp_babillard.ml +++ /dev/null @@ -1,364 +0,0 @@ -open Syntax -open Babillard - -let pp_post fmt t = - let thread_data_opt, post = - match t with - | Op (data, post) -> (Some data, post) - | Post post -> (None, post) - in - let { id - ; emojid - ; parent_id = _parent_id - ; date - ; user_id - ; nick - ; comment - ; image_info - ; tags - ; replies - ; citations = _citations - } = - post - in - - let image_view fmt () = - match image_info with - | Some (_image_name, image_alt) -> - Format.fprintf fmt - {| -
    - - %s - -
    -|} - id id image_alt image_alt id - | None -> Format.fprintf fmt "" - in - - let pp_print_reply fmt reply = - Format.fprintf fmt {|>>%s|} reply - reply - in - let pp_print_replies fmt replies = - Format.fprintf fmt {|
    %a
    |} - (Format.pp_print_list ~pp_sep:Format.pp_print_space pp_print_reply) - replies - in - - let replies_view fmt () = - if Option.is_some thread_data_opt then - (* TODO put thread_posts count in thread_info ? *) - let res_nb = Q.count_thread_posts id in - match res_nb with - | Error _ -> Format.fprintf fmt "" - | Ok ((1 | 2) as nb) -> - Format.fprintf fmt {|
    %d reply
    |} (nb - 1) - | Ok nb -> - Format.fprintf fmt {|
    %d replies
    |} (nb - 1) - else pp_print_replies fmt replies - in - - let post_links_view fmt () = - if Option.is_some thread_data_opt then - Format.fprintf fmt {| - %a - |} replies_view () - else - Format.fprintf fmt - {| - - - - %a - |} - id emojid emojid replies_view () - in - - let post_info_view fmt () = - Format.fprintf fmt - {| - |} - user_id nick date id id id post_links_view () - in - - let pp_print_category fmt category = - Format.fprintf fmt {|%s|} category - in - let pp_print_tag fmt tag = - Format.fprintf fmt {|%s|} tag - in - let pp_print_tags fmt tags = - let categories, tags = - List.partition (fun tag -> List.mem tag App.categories) tags - in - let categories = List.sort String.compare categories in - let tags = List.sort String.compare tags in - let pp_sep = Format.pp_print_space in - Format.fprintf fmt {|
    %a%a
    |} - (Format.pp_print_list ~pp_sep pp_print_category) - categories - (Format.pp_print_list ~pp_sep pp_print_tag) - tags - in - let tags = List.sort String.compare tags in - let tags_view fmt () = pp_print_tags fmt tags in - - let pp_subject fmt () = - match thread_data_opt with - | None -> Format.fprintf fmt "" - | Some thread_data -> - Format.fprintf fmt - {| -
    - %s -
    - |} - thread_data.subject - in - (* put a link in if its a preview *) - let link fmt () = - if Option.is_some thread_data_opt then - Format.fprintf fmt - {||} id - in - - Format.fprintf fmt - {| -
    - %a - %a - %a - %a -
    %s
    - %a -
    -|} - id pp_subject () link () post_info_view () image_view () comment tags_view - () - -let view_post id = - let* post = get_post id in - Ok (Format.asprintf "%a" pp_post (Post post)) - -let pp_thread_preview fmt op = - let thread_data, post = op in - let thread_preview = - Format.fprintf fmt - {| -
    - %a -
    -|} - pp_post - (Op (thread_data, post)) - in - thread_preview - -let catalog_content () = - let* ids = Q.get_threads () in - let* ops = get_ops ids in - Ok - (Format.asprintf "%a" - (Format.pp_print_list ~pp_sep:Format.pp_print_space pp_thread_preview) - ops ) - -let pp_report fmt post report request = - let url = "/admin" in - let _reporter_id, reporter_nick, reason, _date, id = report in - let input_post_id fmt id = - Format.fprintf fmt - {||} id - in - let button fmt action = - let s = moderation_action_to_string action in - Format.fprintf fmt - {||} - s (String.uppercase_ascii s) - in - let form fmt action = - Format.fprintf fmt {|%s %a %a |} - (Dream.form_tag ~action:url request) - input_post_id id button action - in - - Format.fprintf fmt - {| -
    -
    -
    - %a -
    -
    - From: %s Reason: %s -
    - %a -
    - %a -
    - %a -
    -
    -
    -
    -

    -|} - pp_post (Post post) reporter_nick reason form Ignore form Delete form Banish - -let admin_page_content posts reports request = - let posts_reports = List.combine posts reports in - Format.asprintf "%a" - (Format.pp_print_list ~pp_sep:Format.pp_print_space - (fun fmt (post, report) -> pp_report fmt post report request) ) - posts_reports - -let pp_thread fmt op posts = - let thread_data, _post = op in - (*order by date *) - let posts = List.sort (fun a b -> compare a.date b.date) posts in - let posts_view fmt () = - Format.pp_print_list ~pp_sep:Format.pp_print_space - (fun fmt post -> pp_post fmt (Post post)) - fmt posts - in - Format.fprintf fmt - {| -
    -
    -

    %s

    -
    -
    - %a -
    -
    -|} - thread_data.subject posts_view () - -let view_thread thread_id = - let* op = get_op thread_id in - let* ids = Q.get_thread_posts thread_id in - let* posts = get_posts ids in - let s = - (Format.asprintf "%a" (fun fmt (op, posts) -> pp_thread fmt op posts)) - (op, posts) - in - Ok s - -let pp_marker fmt op = - let thread_data, post = op in - let content = Format.asprintf "%a" pp_thread_preview op in - (* geojson use lng lat, and not lat lng*) - let json = - `Assoc - [ ("type", `String "Feature") - ; ( "geometry" - , `Assoc - [ ("type", `String "Point") - ; ( "coordinates" - , `List [ `Float thread_data.lng; `Float thread_data.lat ] ) - ] ) - ; ( "properties" - , `Assoc - [ ("content", `String content); ("thread_id", `String post.id) ] ) - ] - in - Yojson.pretty_print fmt json - -let get_markers () = - let* ids = Q.get_threads () in - let* ops = get_ops ids in - let markers = - Format.asprintf "[%a]" - (Format.pp_print_list - ~pp_sep:(fun fmt () -> Format.fprintf fmt ",") - pp_marker ) - ops - in - Ok markers - -let pp_checkboxes fmt () = - let pp_checkbox fmt category = - Format.fprintf fmt - {| -
    - - -
    -|} - category category category category - in - Format.fprintf fmt - {| -
    - %a -
    |} - (Format.pp_print_list ~pp_sep:Format.pp_print_space pp_checkbox) - App.categories - -(* RFC-3339 date-time *) -let pp_date fmt date = - let date = Unix.gmtime date in - Format.fprintf fmt "%04d-%02d-%02dT%02d:%02d:%02dZ" (1900 + date.tm_year) - (1 + date.tm_mon) date.tm_mday date.tm_hour date.tm_min date.tm_sec - -let pp_feed_entry fmt post = - Format.fprintf fmt - {| - - - urn:uuid:%s - %a - - %s - - %s - - - |} - post.id pp_date post.date post.nick - (Dream.html_escape post.comment) - App.hostname post.parent_id post.id - -let feed thread_id = - let* thread_data, op_post = get_op thread_id in - let* ids = Q.get_thread_posts thread_id in - let* posts = get_posts ids in - let posts = List.sort (fun a b -> compare b.date a.date) posts in - let* last_update = - match posts with [] -> Error "empty thread" | op :: _l -> Ok op.date - in - - let entries fmt () = - (Format.pp_print_list ~pp_sep:Format.pp_print_space pp_feed_entry) fmt posts - in - let feed = - Format.asprintf - {| - - %s - - %a - - %s - - urn:uuid:%s - %a -|} - thread_data.subject App.hostname thread_id pp_date last_update - op_post.nick op_post.id entries () - in - Ok feed diff --git a/src/register.eml.html b/src/register.eml.html deleted file mode 100644 index fbd498c..0000000 --- a/src/register.eml.html +++ /dev/null @@ -1,16 +0,0 @@ -let f request = -<%s! Dream.form_tag ~action:"/register" request %> -
    - - -
    -
    - - -
    -
    - - -
    - - diff --git a/src/report_page.eml.html b/src/report_page.eml.html deleted file mode 100644 index dd28f87..0000000 --- a/src/report_page.eml.html +++ /dev/null @@ -1,22 +0,0 @@ -let f post_preview post_id request = - - - <%s! post_preview %> -% let url = Format.sprintf "/report/%s" post_id in -% begin match Dream.session "nick" request with -% | None -> -% let redirect = Dream.to_percent_encoded url in - Login to report a post. -% | Some _nick -> -
    -
    -
    -<%s! Dream.form_tag ~action:url request %> - - - - -
    -
    -
    -% end; diff --git a/src/syntax.ml b/src/syntax.ml index 62a0617..72132a8 100644 --- a/src/syntax.ml +++ b/src/syntax.ml @@ -1,12 +1,73 @@ -(* let bindings for early return when encountering an error *) -(* see https://ocaml.org/releases/4.13/htmlman/bindingops.html *) +let ( let* ) o f = match o with Ok v -> f v | Error _ as e -> e -let ( let* ) o f = Result.fold ~ok:f ~error:Result.error o +let ( let+ ) o f = match o with Ok v -> Ok (f v) | Error _ as e -> e -let unwrap_list f ids = - let l = List.map f ids in - let res = List.find_opt Result.is_error l in - match res with - | None -> Ok (List.map Result.get_ok l) - | Some (Ok _) -> assert false - | Some (Error _e as error) -> error +let ( let*! ) o f = match o with Ok v -> f v | Error _ as e -> Lwt.return e + +let list_iter f l = + let err = ref None in + try + List.iter + (fun v -> + match f v with + | Error _e as e -> + err := Some e; + raise Exit + | Ok () -> () ) + l; + Ok () + with Exit -> ( match !err with None -> assert false | Some v -> v ) + +let list_map f l = + let err = ref None in + try + Ok + (List.map + (fun v -> + match f v with + | Error _e as e -> + err := Some e; + raise Exit + | Ok v -> v ) + l ) + with Exit -> ( match !err with None -> assert false | Some v -> v ) + +let list_fold_left f acc l = + List.fold_left + (fun acc v -> + let* acc = acc in + f acc v ) + (Ok acc) l + +let array_iter f a = + let err = ref None in + try + for i = 0 to Array.length a - 1 do + match f (Array.unsafe_get a i) with + | Error _e as e -> + err := Some e; + raise Exit + | Ok () -> () + done; + Ok () + with Exit -> ( match !err with None -> assert false | Some v -> v ) + +let array_map f a = + let err = ref None in + try + Ok + (Array.init (Array.length a) (fun i -> + let v = Array.get a i in + match f v with + | Error _e as e -> + err := Some e; + raise Exit + | Ok v -> v ) ) + with Exit -> ( match !err with None -> assert false | Some v -> v ) + +let array_fold_left f acc l = + Array.fold_left + (fun acc v -> + let* acc = acc in + f acc v ) + (Ok acc) l diff --git a/src/template.eml.html b/src/template.eml.html deleted file mode 100644 index 1fa3d3e..0000000 --- a/src/template.eml.html +++ /dev/null @@ -1,84 +0,0 @@ -let render_unsafe ~title ~content request = - - - - <%s title %> | Permap - - - - - - -
    - -
    -
    -
    -
    -
    -
    -
    - <%s! content %> -
    -
    - -
    - - - diff --git a/src/thread_page.eml.html b/src/thread_page.eml.html deleted file mode 100644 index 94a03a0..0000000 --- a/src/thread_page.eml.html +++ /dev/null @@ -1,16 +0,0 @@ -let f thread_view thread_id request = - - <%s! thread_view %> -% let thread_url = Format.sprintf "/thread/%s" thread_id in -% begin match Dream.session "nick" request with -% | None -> -% let redirect = Dream.to_percent_encoded thread_url in -Login to reply! -% | Some _ -> -<%s! Post_form.f (Some thread_id) request %> -% end; -% let feed_url = Format.sprintf "%s/feed" thread_url in - > - - - /> diff --git a/src/types.mli b/src/types.mli new file mode 100644 index 0000000..bcffab9 --- /dev/null +++ b/src/types.mli @@ -0,0 +1,104 @@ +(* shared server and client types *) +(* TODO + - rm not shared types + - type alias for better naming in other modules? *) + +type post_id = int + +type thread_id = int + +type user_id = string + +type v_string = Validate_str.v_string + +type img_info = + { md5 : string + ; mime : string + ; w : int + ; h : int + ; thumb_w : int + ; thumb_h : int + ; name : string + ; alt : string + } + +type img = + { info : img_info + ; data : string + ; thumbnail_data : string + } + +type comment = Comment.t + +type bump_status = + | Dead + | Locked of int + | Alive of int + +module User_private : sig + type t = + { user_id : user_id + ; user_nick : string + ; user_is_admin : bool + ; bio : string + ; avatar_info : img_info option + ; email : string + } +end + +type user = + { user_id : user_id + ; user_nick : string + ; user_is_admin : bool + ; bio : string + ; avatar_info : img_info option + } + +type session = + { user_private : User_private.t option + ; csrf_token : string + ; csrf_time_limit : float + } + +type post = + { id : post_id + ; parent_t_id : thread_id + ; date : float + ; poster_id : user_id + ; poster_nick : string + ; comment : comment + ; image_info : img_info option + ; (* TODO get this out of this record? *) + backlinks : post_id list + } + +(* TODO util conversion function for Thread_w_reply.t and user_private *) +module Thread_w_reply : sig + type t = + { op : post + ; subject : string + ; lat : float + ; lng : float + ; bump_status : bump_status + ; reply_count : int + ; reply_l : post list + } +end + +type thread = + { op : post + ; subject : string + ; lat : float + ; lng : float + ; bump_status : bump_status + ; reply_count : int + } + +type report = + { report_id : string + ; report_date : float + ; reported_post : post + ; reporter_user_id : user_id + ; reporter_nick : string + ; reason : string + } diff --git a/src/user.ml b/src/user.ml index 976ab53..30e9455 100644 --- a/src/user.ml +++ b/src/user.ml @@ -1,336 +1,115 @@ open Syntax -open Caqti_request.Infix -open Caqti_type +open Types +open Err -type t = - { user_id : string - ; nick : string - ; password : string - ; email : string - ; bio : string - ; metadata : (string * string) list - } +let check_nick nick = + let* nick = Validate_str.nick nick in + let* opt = Db_user.find_user_of_nick nick in + match opt with + | None -> Ok nick + | Some _user -> Error (Unprocessable "nick already taken") -let () = - let tables = - [| (unit ->. unit) - "CREATE TABLE IF NOT EXISTS user (user_id TEXT, nick TEXT, password \ - TEXT, email TEXT, bio TEXT, PRIMARY KEY(user_id))" - ; (unit ->. unit) - "CREATE TABLE IF NOT EXISTS banished (nick TEXT, email TEXT)" - ; (unit ->. unit) - "CREATE TABLE IF NOT EXISTS user_metadata (user_id TEXT, metadata \ - TEXT, FOREIGN KEY(user_id) REFERENCES user(user_id) ON DELETE \ - CASCADE)" - |] - in - if - Array.exists Result.is_error - (Array.map (fun query -> Db.exec query ()) tables) - then Dream.error (fun log -> log "can't create user tables") - -module Q = struct - let upload_metadata = - Db.exec - @@ (tup2 string string ->. unit) "INSERT INTO user_metadata VALUES (?, ?)" - - let delete_metadata = - Db.exec @@ (string ->. unit) "DELETE FROM user_metadata WHERE user_id=?" - - let get_user_id_from_email = - Db.find @@ (string ->! string) "SELECT user_id FROM user WHERE email=?" - - let get_password = - Db.find @@ (string ->! string) "SELECT password FROM user WHERE user_id=?" - - let is_already_user = - Db.find - @@ (tup2 string string ->! int) - "SELECT EXISTS(SELECT 1 FROM user WHERE nick=? OR email=?)" - - let upload_user = - Db.exec - @@ (tup4 string string string (tup2 string string) ->. unit) - "INSERT INTO user VALUES (?, ?, ?, ?, ?)" - - let list_nicks = Db.collect_list @@ (unit ->* string) "SELECT nick FROM user" - - let get_user = - Db.find - @@ (* there is no "tup6" *) - (string ->! tup4 string string string (tup2 string string)) - "SELECT * FROM user WHERE user_id=?" - - let update_bio = - Db.exec - @@ (tup2 string string ->. unit) "UPDATE user SET bio=? WHERE user_id=?" - - let update_nick = - Db.exec - @@ (tup2 string string ->. unit) "UPDATE user SET nick=? WHERE user_id=?" - - let update_email = - Db.exec - @@ (tup2 string string ->. unit) "UPDATE user SET email=? WHERE user_id=?" - - let update_password = - Db.exec - @@ (tup2 string string ->. unit) - "UPDATE user SET password=? WHERE user_id=?" - - let get_bio = - Db.find @@ (string ->! string) "SELECT bio FROM user WHERE user_id=?" - - let get_email = - Db.find @@ (string ->! string) "SELECT email FROM user WHERE user_id=?" - - let delete_user = - Db.exec @@ (string ->. unit) "DELETE FROM user WHERE user_id=?" - - let upload_banished = - Db.exec @@ (tup2 string string ->. unit) "INSERT INTO banished VALUES (?,?)" - - let get_banished = - Db.find - @@ (tup2 string string ->! tup2 string string) - "SELECT * FROM banished WHERE nick=? OR email=?" -end - -let get_nick = - Db.find @@ (string ->! string) "SELECT nick FROM user WHERE user_id=?" - -let get_id_from_nick = - Db.find @@ (string ->! string) "SELECT user_id FROM user WHERE nick=?" - -let exist id = Result.is_ok (Q.get_user id) - -let exist_nick nick = Result.is_ok (get_id_from_nick nick) - -let exist_email email = Result.is_ok (Q.get_user_id_from_email email) - -let get_metadata = - let query = - Db.find - @@ (string ->! string) "SELECT metadata FROM user_metadata WHERE user_id=?" - in - fun nick -> - let* metadata = query nick in - let metadata : (string * string) list = Marshal.from_string metadata 0 in - Ok metadata +let check_email email = + let* email = Validate_str.email email in + match Emile.of_string (email :> string) with + | Error _ -> Error (Unprocessable "invalid email format") + | Ok _ -> ( + let* opt = Db_user.find_user_of_email email in + match opt with + | None -> Ok email + | Some _user -> Error (Unprocessable "email already taken") ) let get_user user_id = - let* user_id, nick, password, (email, bio) = Q.get_user user_id in - let* metadata = get_metadata user_id in - Ok { user_id; nick; password; email; bio; metadata } + let* opt = Db_user.find_user user_id in + match opt with None -> Error (Not_found_user user_id) | Some o -> Ok o -let is_banished login = Result.is_ok (Q.get_banished (login, login)) +let get_user_private user_id = + let* opt = Db_user.find_user_private user_id in + match opt with None -> Error (Not_found_user user_id) | Some o -> Ok o -let login ~login ~password request = - let try_password user_id = - let* good_password = Q.get_password user_id in - if Bcrypt.verify password (Bcrypt.hash_of_string good_password) then - let _unit_lwt = Dream.invalidate_session request in - let _unit_lwt = Dream.put_session "user_id" user_id request in - let* nick = get_nick user_id in - let _unit_lwt = Dream.put_session "nick" nick request in - Ok () - else if is_banished login then Error "YOU ARE BANISHED" - else Error "wrong password" +(* login can be nick or email *) +let login ~login ~password = + let f find s = + let* opt = find s in + match opt with + | None -> Ok None + | Some user -> + let* good_password = Db_user.get_password_hash user.user_id in + if Bcrypt.verify password (Bcrypt.hash_of_string good_password) then + let* opt = Db_user.find_user_private user.user_id in + match opt with + | None -> + Error + (Internal (Db_not_found (Fmt.str "user_private `%s`" user.user_id))) + | Some o -> Ok (Some o) + else Error (Unauthorized_login login) in - - let id_from_nick = get_id_from_nick login in - let id_from_email = Q.get_user_id_from_email login in - let user_id_list = - List.filter_map Result.to_option [ id_from_nick; id_from_email ] + (* assume login is nick *) + let* opt = + match Validate_str.nick login with + | Error _ -> Ok None + | Ok nick -> f Db_user.find_user_of_nick nick in - try - List.iter - (fun id -> if Result.is_ok @@ try_password id then raise Exit) - user_id_list; - Error "invalid login" - with Exit -> Ok () - -let valid_nick nick = - String.length nick < 64 - && String.length nick > 0 - && Dream.html_escape nick = nick - -let valid_password password = - String.length password < 128 && String.length password > 0 - -let valid_email email = Result.is_ok @@ Emile.of_string email + match opt with + | Some u -> Ok u + | None -> ( + (* assume login is email *) + let* email = Validate_str.email login in + let* opt = f Db_user.find_user_of_email email in + match opt with Some u -> Ok u | None -> Error (Unauthorized_login login) ) let register ~email ~nick ~password = - let valid = valid_nick nick && valid_email email && valid_password password in - - let password = Bcrypt.hash password in - let password = Bcrypt.string_of_hash password in - - if not valid then Error "Something is wrong" - else - let* nb = Q.is_already_user (nick, email) in - if nb = 0 then - let user_id = Uuidm.to_string (Uuidm.v4_gen App.random_state ()) in - let* () = Q.upload_user (user_id, nick, password, (email, "")) in - Q.upload_metadata (user_id, Marshal.to_string [] []) - else Error "nick or email already exists" - -let list () = - let* users = Q.list_nicks () in - Ok - (Format.asprintf "
      %a
    " - (Format.pp_print_list (fun fmt -> function - | s -> Format.fprintf fmt {|
  • %s
  • |} s s ) - ) - users ) - -let profile request = - match Dream.session "nick" request with - | None -> "not logged in" - | Some nick -> Format.sprintf "Hello %s !" nick - -let update_bio bio user_id = - let bio = Dream.html_escape bio in - let valid = String.length bio < 10000 in - if not valid then Error "Not biologic" else Q.update_bio (bio, user_id) - -let upload_avatar files user_id = - match files with - | [] -> Error "No file provided" - | [ (name_opt, content) ] -> - let* image = Image.make_image (name_opt, "avatar", content) in - let* () = Image.upload_avatar image user_id in - Ok () - | _files -> Error "More than one file provided" - -let is_admin user_id = - match get_nick user_id with - | Error _e -> false - | Ok nick -> List.mem nick App.admins - -let banish user_id = - let* nick = get_nick user_id in - let* email = Q.get_email user_id in - let* () = Q.delete_user user_id in - Q.upload_banished (nick, email) - -let delete_user user_id = Q.delete_user user_id - -let update_nick nick user_id = - if valid_nick nick then - if not (exist_nick nick) then Q.update_nick (nick, user_id) - else Error "nick already taken" - else Error "invalid nick" - -let update_email email user_id = - if valid_email email then - if not (exist_email email) then Q.update_email (email, user_id) - else Error "email already taken" - else Error "invalid email" - -let update_password password user_id = - if valid_password password then - let password = Bcrypt.hash password |> Bcrypt.string_of_hash in - Q.update_password (password, user_id) - else Error "invalid password" - -let update_metadata count label content user_id = - let label = Dream.html_escape label in - let content = Dream.html_escape content in - if String.length label > 200 || String.length content > 400 then - Error "label or content is too long" - else - let* metadata = get_metadata user_id in - let length = List.length metadata in - if count < 0 || count > length then Error "invalid count" - else - let n = max (count + 1) @@ length in - let metadata = Array.of_list metadata in - let metadata = - List.init n (fun i -> - if i = count then (label, content) else metadata.(i) ) - in - let metadata = - List.filter (fun (l, c) -> not (l = "" && c = "")) metadata - in - if List.length metadata >= 42 then Error "to many metadata" - else - let s = Marshal.to_string metadata [] in - let* () = Q.delete_metadata user_id in - Q.upload_metadata (user_id, s) - -let pp_metadata fmt (label, content) = - Format.fprintf fmt - {| -
    - - -
    - |} - label content - -let pp_metadata_form fmt is_last count (label, content) request = - let form_tag = Dream.form_tag ~action:"/profile" request in - let button_text = if is_last then "Add" else "Save" in - Format.fprintf fmt - {| -
    - %s - - - - -
    - |} - form_tag label content count button_text - -let pp_metadata_table fmt metadata = - Format.fprintf fmt - {| - -|} - (Format.pp_print_list ~pp_sep:Format.pp_print_space pp_metadata) - metadata - -let pp_metadata_table_form fmt metadata request = - let l = List.mapi (fun i e -> (i, e)) metadata in - Format.fprintf fmt - {| - -|} - (Format.pp_print_list ~pp_sep:Format.pp_print_space - (fun fmt (count, metadata) -> - pp_metadata_form fmt false count metadata request ) ) - l - (fun fmt (count, metadata) -> - pp_metadata_form fmt true count metadata request ) - (List.length l, ("", "")) - -let public_profile user_id = - let* user = get_user user_id in - let user_info = - Format.asprintf - {| -

    %s

    -
    -
    -
    -
    %s
    -
    -
    - Your avatar picture -
    - Speak to me ! -
    - %a -
    -
    -|} - user.nick user.bio user.nick user_id pp_metadata_table user.metadata + let* password = Validate_str.password password in + let* nick = check_nick nick in + let* email = check_email email in + let* () = + let* opt1 = Db_user.find_user_of_nick nick in + let* opt2 = Db_user.find_user_of_email email in + let loggin_not_taken = Option.is_none opt1 && Option.is_none opt2 in + if loggin_not_taken then Ok () + else Error (Unprocessable "nick or email already taken") in - Ok user_info + let password_hash = + Bcrypt.hash (password :> string) |> Bcrypt.string_of_hash + in + Db_user.add_user ~email ~nick ~password_hash + +let delete_user user_id = Db_user.delete_user user_id + +let update_bio user_id bio = + let* bio = Validate_str.bio bio in + Db_user.update_bio user_id bio + +let update_nick user_id nick = + let* nick = check_nick nick in + Db_user.update_nick user_id nick + +let update_email user_id email = + let* email = check_email email in + Db_user.update_email user_id email + +let update_password user_id password = + let* password = Validate_str.password password in + let password_hash = + Bcrypt.hash (password :> string) |> Bcrypt.string_of_hash + in + Db_user.update_password_hash user_id password_hash + +let get_image user_id = + let default_avatar_path = "/img/default_avatar.png" in + let* opt = Db_image.U.data user_id in + match opt with + | Some data -> Ok data + | None -> ( + match Assets.read default_avatar_path with + | None -> Error (Internal (Db_not_found "can not find default avatar file")) + | Some avatar -> Ok avatar ) + +(* TODO sql : rm image db functor, handle avatar image and transaction in db_user *) +let upload_avatar user_id (name_opt, alt, content) = + let name = Option.value ~default:"" name_opt in + let* image = Image.build ~name ~alt content in + Caqti_db.Db.do_transaction @@ fun () -> Db_image.U.upload user_id image + +let delete_avatar user_id = + Caqti_db.Db.do_transaction @@ fun () -> Db_image.U.delete user_id diff --git a/src/user_account.eml.html b/src/user_account.eml.html deleted file mode 100644 index 305bf0c..0000000 --- a/src/user_account.eml.html +++ /dev/null @@ -1,28 +0,0 @@ -let f (user: User.t) request = -

    <%s Format.sprintf "Account settings" %>

    -

    Change email

    - <%s! Dream.form_tag ~action:"/account" request %> -
    - - -
    - - -
    -
    -

    Change password

    - <%s! Dream.form_tag ~action:"/account" request %> -
    - - - - -
    - - -
    -
    -

    Delete account

    - <%s! Dream.form_tag ~action:"/account" request %> - - diff --git a/src/user_profile.eml.html b/src/user_profile.eml.html deleted file mode 100644 index 5086ab3..0000000 --- a/src/user_profile.eml.html +++ /dev/null @@ -1,40 +0,0 @@ -let f (user: User.t) request = -% let metadata_table = Format.asprintf "%a" (fun fmt metadata -> User.pp_metadata_table_form fmt metadata request) user.metadata in -

    Edit your profile

    -

    Check your public profile rendering.

    -

    Display nickname

    - <%s! Dream.form_tag ~action:"/profile" request %> -
    - - -
    - - -
    -
    -

    Profile metadata

    -

    Add items displayed as a table on your profile.

    -<%s! metadata_table %> -
    -
    -

    Bio

    - <%s! Dream.form_tag ~action:"/profile" request %> -
    - - -
    Who are you?
    -
    - - -
    -

    Avatar

    - Your avatar picture -
    -
    - <%s! Dream.form_tag ~action:"/profile" ~enctype:`Multipart_form_data request %> -
    - - -
    - - diff --git a/src/util.ml b/src/util.ml new file mode 100644 index 0000000..2a5c0aa --- /dev/null +++ b/src/util.ml @@ -0,0 +1,4 @@ +let gen_uuid () = + let random_state = Random.State.make_self_init () in + Random.set_state random_state; + Uuidm.to_string (Uuidm.v4_gen random_state ()) diff --git a/src/utils.ml b/src/utils.ml deleted file mode 100644 index 6dda041..0000000 --- a/src/utils.ml +++ /dev/null @@ -1,32 +0,0 @@ -let get_title content = - let open Soup in - try - let soup = content |> parse in - soup $ "h1" |> R.leaf_text - with Failure _e -> "Permap" - -let render ?title content request = - let title = - match title with None -> get_title content | Some title -> title - in - Dream.html @@ Template.render_unsafe ~title ~content request - -(* TODO: different error code ? *) -let render_result request = function Error cnt | Ok cnt -> render cnt request - -(* TODO: maybe we can remove path and find it in request ? *) -let logged_in_or_redirect request logged_in = - match Dream.session "user_id" request with - | None -> - let target = Dream.target request in - let redirect_url = - Format.sprintf "/login?redirect=%s" (Dream.to_percent_encoded target) - in - Dream.respond ~status:`See_Other ~headers:[ ("Location", redirect_url) ] "" - | Some user_id -> logged_in user_id - -let handle_invalid_form = function - | `Ok _ -> Dream.respond ~status:`Bad_Request "invalid form" - | `Many_tokens _ | `Missing_token _ | `Invalid_token _ | `Wrong_session _ - | `Expired _ | `Wrong_content_type -> - Dream.empty `Bad_Request diff --git a/src/validate_str.ml b/src/validate_str.ml new file mode 100644 index 0000000..c7ab3ec --- /dev/null +++ b/src/validate_str.ml @@ -0,0 +1,114 @@ +open Syntax + +type v_string = string + +type err = + | Too_short of int + | Too_long of int + | Contains_forbidden_char + | Not_trimed + +let pp_err fmt e = + match e with + | Too_short n -> Fmt.pf fmt "is too short, minimum lenght is %d" n + | Too_long n -> Fmt.pf fmt "is too long, maximum lenght is %d" n + | Contains_forbidden_char -> + (* TODO say which ones *) + Fmt.pf fmt "contains forbidden character" + | Not_trimed -> Fmt.pf fmt "contains leading or trailing whitespace" + +(* TODO: + - not sure on what to allow/disallow and on which input strings + - bidi issue in html *) +let is_not_restricted = + let is_not_restricted c = + match c with + | 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '_' | '-' | '.' -> true + | _ -> false + in + fun s -> String.for_all is_not_restricted s + +(* restrict_trim with min_len check that the string is not artificially padded with whitespace *) +let make ~min_len ~max_len ~restrict_char ~restrict_trim = + let check_min = + match min_len with + | None -> fun _s -> Ok () + | Some min_len -> + fun len -> if len >= min_len then Ok () else Error (Too_short min_len) + in + let check_max = + fun len -> if len <= max_len then Ok () else Error (Too_long max_len) + in + let check_char = + match restrict_char with + | false -> fun _s -> Ok () + | true -> + fun s -> + if is_not_restricted s then Ok () else Error Contains_forbidden_char + in + fun s : (v_string, err) Result.t -> + let* () = + if restrict_trim then + if String.length (String.trim s) = String.length s then Ok () + else Error Not_trimed + else Ok () + in + let len = String.length s in + let* () = check_min len in + let* () = check_max len in + let+ () = check_char s in + s + +let map_err_to_invalid_submission ~kind_str f = + fun s -> + f s + |> Result.map_error (fun e -> + let s = Fmt.str "%s %a" kind_str pp_err e in + Err.Unprocessable s ) + +open Config + +let subject = + make ~max_len:subject_max_length ~min_len:subject_min_length + ~restrict_char:false ~restrict_trim:true + |> map_err_to_invalid_submission ~kind_str:"subject" + +let comment = + make ~max_len:comment_max_length ~min_len:comment_min_length + ~restrict_char:false ~restrict_trim:true + |> map_err_to_invalid_submission ~kind_str:"comment" + +let report = + make ~max_len:report_max_length ~min_len:None ~restrict_char:false + ~restrict_trim:true + |> map_err_to_invalid_submission ~kind_str:"report" + +let nick = + make ~max_len:nick_max_length ~min_len:(Some nick_min_length) + ~restrict_char:true ~restrict_trim:true + |> map_err_to_invalid_submission ~kind_str:"nick" + +let email = + (* just to force it to be trimed *) + make ~max_len:1_000 ~min_len:None ~restrict_char:false ~restrict_trim:true + |> map_err_to_invalid_submission ~kind_str:"email" + +let bio = + make ~max_len:biography_max_length ~min_len:None ~restrict_char:false + ~restrict_trim:false + |> map_err_to_invalid_submission ~kind_str:"biography" + +let password = + make ~max_len:password_max_length ~min_len:(Some password_min_length) + ~restrict_char:false ~restrict_trim:false + |> map_err_to_invalid_submission ~kind_str:"password" + +let image_name = + make ~max_len:image_name_max_length ~min_len:None ~restrict_char:false + ~restrict_trim:true + |> map_err_to_invalid_submission ~kind_str:"image name" + +let image_alt = + make ~max_len:image_description_max_length ~min_len:None ~restrict_char:false + ~restrict_trim:true + |> map_err_to_invalid_submission ~kind_str:"image description" diff --git a/src/validate_str.mli b/src/validate_str.mli new file mode 100644 index 0000000..cf0a5eb --- /dev/null +++ b/src/validate_str.mli @@ -0,0 +1,32 @@ +(* TODO + - allow comment to be untrimed + better way to check comment is not padded with garbage than restrict_trim *) +open Err + +type v_string = private string + +type err = + | Too_short of int + | Too_long of int + | Contains_forbidden_char + | Not_trimed + +val pp_err : Format.formatter -> err -> unit + +val subject : string -> v_string result + +val comment : string -> v_string result + +val report : string -> v_string result + +val nick : string -> v_string result + +val email : string -> v_string result + +val bio : string -> v_string result + +val password : string -> v_string result + +val image_name : string -> v_string result + +val image_alt : string -> v_string result diff --git a/src/virtual/config.mli b/src/virtual/config.mli new file mode 100644 index 0000000..99350b6 --- /dev/null +++ b/src/virtual/config.mli @@ -0,0 +1,51 @@ +(* config (server & client) *) + +val source_code_url : string + +val hostname : string + +val port : int + +(* in seconds, 3600 sec = 1h *) +val csrf_lifetime : int + +val session_lifetime : int + +val open_registration : bool + +val thread_max_count : int + +val thread_alive_max_count : int + +val thread_replies_max_count : int + +(* length and size are in Byte *) +val subject_max_length : int + +val subject_min_length : int option + +val comment_max_length : int + +val comment_min_length : int option + +val report_max_length : int + +val nick_max_length : int + +val nick_min_length : int + +val biography_max_length : int + +val password_max_length : int + +val password_min_length : int + +val image_name_max_length : int + +val image_description_max_length : int + +val image_max_size : int + +val supported_mime_type : string array + +(* TODO add supported mime type *) diff --git a/src/virtual/config_serv.mli b/src/virtual/config_serv.mli new file mode 100644 index 0000000..bea4464 --- /dev/null +++ b/src/virtual/config_serv.mli @@ -0,0 +1,11 @@ +(* server config *) + +val config_path : Fpath.t + +val db_uri : Uri.t + +val default_logger : bool + +val custom_logger : bool + +val admin_l : string list diff --git a/src/virtual/dir.mli b/src/virtual/dir.mli new file mode 100644 index 0000000..41e7679 --- /dev/null +++ b/src/virtual/dir.mli @@ -0,0 +1,4 @@ +(* TODO file path instead of folder path *) +val config : Fpath.t option + +val data : Fpath.t option diff --git a/src/virtual/dune b/src/virtual/dune new file mode 100644 index 0000000..e2dcab9 --- /dev/null +++ b/src/virtual/dune @@ -0,0 +1,28 @@ +(library + (name config) + (modules config) + (virtual_modules config) + (libraries uri fpath)) + +(library + (name config_serv) + (modules config_serv) + (virtual_modules config_serv) + (libraries uri fpath)) + +(library + (name dir) + (modules dir) + (virtual_modules dir) + (libraries fpath)) + +(library + (name make_config_lib) + (modules make_config_lib) + (libraries + dir ; virtual + fmt + uri + fpath + bos + scfg)) diff --git a/src/virtual/impl/dir.ml b/src/virtual/impl/dir.ml new file mode 100644 index 0000000..1a3a8a6 --- /dev/null +++ b/src/virtual/impl/dir.ml @@ -0,0 +1,13 @@ +module App_id = struct + let qualifier = "org" + + let organization = "Permap" + + let application = "permap" +end + +module Project_dirs = Directories.Project_dirs (App_id) + +let config = Project_dirs.config_dir + +let data = Project_dirs.data_dir diff --git a/src/virtual/impl/dune b/src/virtual/impl/dune new file mode 100644 index 0000000..14445d7 --- /dev/null +++ b/src/virtual/impl/dune @@ -0,0 +1,39 @@ +(library + (name config_impl) + (modules config) + (implements config) + (libraries uri fpath)) + +(library + (name config_serv_impl) + (modules config_serv) + (implements config_serv) + (libraries uri fpath)) + +(library + (name dir_impl) + (modules dir) + (implements dir) + (libraries fpath directories)) + +(executable + (name make_config_impl) + (modules make_config_impl) + (libraries + dir_impl ; implements virtual module + make_config_lib ; virtual lib + fmt + uri + fpath + bos + scfg)) + +(rule + (with-stdout-to + config.ml + (run ./make_config_impl.exe --config))) + +(rule + (with-stdout-to + config_serv.ml + (run ./make_config_impl.exe --config-serv))) diff --git a/src/virtual/impl/make_config_impl.ml b/src/virtual/impl/make_config_impl.ml new file mode 100644 index 0000000..a7f9913 --- /dev/null +++ b/src/virtual/impl/make_config_impl.ml @@ -0,0 +1 @@ +include Make_config_lib diff --git a/src/virtual/make_config_lib.ml b/src/virtual/make_config_lib.ml new file mode 100644 index 0000000..d81b3d3 --- /dev/null +++ b/src/virtual/make_config_lib.ml @@ -0,0 +1,217 @@ +let ( let* ) o f = + match o with + | Ok v -> f v + | Error (`Msg e) -> + Fmt.epr "config error, %s" e; + exit 1 + +let unwrap param_name opt = + match opt with + | None -> Fmt.error_msg "parameter `%s` not found" param_name + | Some v -> Ok v + +let print k v = + Fmt.pr {|let %s = "%s"@.|} k v; + () + +let print_b k v = + Fmt.pr "let %s = %b@." k v; + () + +let print_int k v = + Fmt.pr "let %s = %#d@." k v; + () + +let print_int_opt k v = + let s = match v with None -> "None" | Some v -> Fmt.str "Some %#d" v in + Fmt.pr "let %s = %s@." k s; + () + +let print_l k l = + let l = List.map (Fmt.str {|"%s"|}) l in + Fmt.pr "let %s = [%s]@." k (String.concat "; " l); + () + +let print_arr k arr = + let l = Array.map (Fmt.str {|"%s"|}) arr |> Array.to_list in + Fmt.pr "let %s = [|%s|]@." k (String.concat "; " l); + () + +let print_fpath k v = + Fmt.pr {|let %s = Result.get_ok (Fpath.of_string "%s")@.|} k + (Fpath.to_string v); + () + +let print_uri k v = + Fmt.pr {|let %s = Uri.of_string "%s"@.|} k (Uri.to_string v); + () + +let config_path = + let* config_dir = + Dir.config + |> Option.to_result ~none:(`Msg "can't compute configuration directory") + in + let config_path = Fpath.(config_dir / "config.scfg") in + config_path + +let config = + let* exists = Bos.OS.File.exists config_path in + let* config = + match exists with + | false -> + Fmt.error_msg "configuration file `%a` does not exist, please create it" + Fpath.pp config_path + | true -> Scfg.Parse.from_file config_path + in + config + +let get_dir param_name = + let* dir = unwrap param_name @@ Scfg.Query.get_dir param_name config in + dir + +let mk_str param_name = + let* v = Scfg.Query.get_param 0 (get_dir param_name) in + print param_name v; + () + +let mk_bool param_name = + let* v = Scfg.Query.get_param_bool 0 (get_dir param_name) in + print_b param_name v; + () + +let mk_int ?min param_name = + let min = Option.value ~default:0 min in + let* n = Scfg.Query.get_param_int 0 (get_dir param_name) in + let* () = + match n with + | _ when n < 0 -> Fmt.error_msg "negative `%s` value" param_name + | _ when n < min -> + Fmt.error_msg "less than minimum (`%d`) `%s` value" min param_name + | _ -> Ok () + in + print_int param_name n; + () + +(* we use -1 in config file for None *) +let mk_int_opt param_name = + let* opt = + match Scfg.Query.get_dir param_name config with + | None -> Ok None + | Some dir -> ( + let* n = Scfg.Query.get_param_int 0 dir in + match n with + | -1 -> Ok None + | n when n < 0 -> Fmt.error_msg "negative `%s` value" param_name + | n -> Ok (Some n) ) + in + print_int_opt param_name opt; + () + +let mk_str_l param_name = + let dirs = Scfg.Query.get_dirs param_name config in + let l = + List.map + (fun dir -> + let* v = Scfg.Query.get_param 0 dir in + v ) + dirs + in + print_l (Fmt.str "%s_l" param_name) l; + () + +(* server config *) +let print_config_serv () = + let () = + print_fpath "config_path" config_path; + () + in + + let () = + let* data_dir = + Dir.data |> Option.to_result ~none:(`Msg "can't compute data directory") + in + (* create data_dir *) + let* () = + match Bos.OS.Dir.create data_dir with + | Ok true -> Ok () + | Ok false -> Ok () + | Error (`Msg _) -> + Fmt.error_msg "error when creating %a" Fpath.pp data_dir + in + let db_path = Fpath.(data_dir / "permap.db") in + let s = Uri.of_string @@ Fmt.str "sqlite3://%a" Fpath.pp db_path in + print_uri "db_uri" s; + () + in + + let () = mk_bool "default_logger" in + + let () = mk_bool "custom_logger" in + + (* TODO add temporary password hash + email for first connection? *) + let () = mk_str_l "admin" in + + () + +(* config (server & client) *) +let print_config () = + let () = mk_str "source_code_url" in + + let () = mk_str "hostname" in + + let () = mk_int "port" in + + let () = mk_int "csrf_lifetime" in + + let () = mk_int "session_lifetime" in + + let () = mk_bool "open_registration" in + + let () = mk_int "thread_max_count" in + + let () = mk_int "thread_alive_max_count" in + + let () = mk_int "thread_replies_max_count" in + + let () = mk_int "subject_max_length" in + + let () = mk_int_opt "subject_min_length" in + + let () = mk_int "comment_max_length" in + + let () = mk_int_opt "comment_min_length" in + + let () = mk_int "report_max_length" in + + let () = mk_int "nick_max_length" in + + let () = mk_int ~min:1 "nick_min_length" in + + let () = mk_int "biography_max_length" in + + let () = mk_int "password_max_length" in + + let () = mk_int ~min:1 "password_min_length" in + + let () = mk_int "image_name_max_length" in + + let () = mk_int "image_description_max_length" in + + let () = mk_int "image_max_size" in + print_arr "supported_mime_type" + [| "image/jpeg"; "image/png"; "image/webp"; "image/gif" |]; + + () + +let () = + let* arg = + if Array.length Sys.argv > 1 then Ok Sys.argv.(1) + else Fmt.error_msg "make config failure, no argument provided" + in + let* () = + match arg with + | "--config-serv" -> Ok (print_config_serv ()) + | "--config" -> Ok (print_config ()) + | _ -> Fmt.error_msg "make config failure, invalid argument `%s`" arg + in + () diff --git a/test/config.scfg b/test/config.scfg new file mode 100644 index 0000000..3448757 --- /dev/null +++ b/test/config.scfg @@ -0,0 +1,25 @@ +default_logger true +custom_logger true +admin admin +source_code_url "https://git.zapashcanon.fr/swrup/permap" +hostname "localhost" +port 3696 +csrf_lifetime 3600 +session_lifetime 3600 +open_registration true +thread_max_count 300 +thread_alive_max_count 250 +thread_replies_max_count 400 +subject_max_length 80 +subject_min_length -1 +comment_max_length 1_600 +comment_min_length -1 +report_max_length 1_600 +nick_max_length 64 +nick_min_length 1 +biography_max_length 6_400 +password_max_length 128 +password_min_length 1 +image_name_max_length 64 +image_description_max_length 800 +image_max_size 10_000_000 diff --git a/test/dune b/test/dune index 81cf59e..0ef8698 100644 --- a/test/dune +++ b/test/dune @@ -1,3 +1,24 @@ (test - (name main) - (modules main)) + (name test) + (modules test util assets) + (libraries + config_serv_test ; implements config_serv module + config_test ; implements config module + shared + comment + permap + ;; + alcotest + fmt + fpath + re + prelude)) + +(rule + (target assets.ml) + (deps + (source_tree img)) + (action + (with-stdout-to + %{null} + (run ocaml-crunch -m plain img -o %{target})))) diff --git a/test/img/fff.exif.png b/test/img/fff.exif.png new file mode 100644 index 0000000000000000000000000000000000000000..2be78c3a348937c30714cf0671a12602e075cf67 GIT binary patch literal 356 zcmeAS@N?(olHy`uVBq!ia0y~yU|5Hn!&&zUNC1@pbY~915=W>y9>kr_Wm>b85kHi3p^r=85sBu zgD~Uq{1qt-3=Hfgp1!W^FPQ}xSQHN_OZ;bGVBqp}aSY+Op4`FCz>vt0u;1Uohk=1X zwZt`|BqgyV)hf9tHL)a>!N|bKRM)^**T5*m(9p`n(8|8mebKWME*>fZI@#nVVW%l9*cn*JEfF0y5jg%GelU$$||NqZt?&WI=jTD>Bop pLQ;!MToOwX8B$7fb1M}?q}T{Qpz literal 0 HcmV?d00001 diff --git a/test/img/fff.gif b/test/img/fff.gif new file mode 100644 index 0000000000000000000000000000000000000000..cd50c6ff39ed5072a9c508ffd791200eaf65bb57 GIT binary patch literal 41 pcmZ?wbh9u|WMp7un8*ME|NsAwkB?_yV9){aK;jHcOd*U6)&R8S2NnPT literal 0 HcmV?d00001 diff --git a/test/img/fff.jpeg b/test/img/fff.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..f48b1c9c9eade5275e96e8f19ed96b24519ac5a5 GIT binary patch literal 694 zcmex=ovIz$!vMUve z7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51dF0O9w9-dyoA)#U6 z5s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OPfBE|D`;VW$K>lK6 zU=!0ld(M2vX6_bamA3vDUpGJfx*+&&t;uc GLK6U@x)_B3 literal 0 HcmV?d00001 diff --git a/test/main.ml b/test/main.ml deleted file mode 100644 index 29ef5f9..0000000 --- a/test/main.ml +++ /dev/null @@ -1 +0,0 @@ -let () = assert true (* TODO *) diff --git a/test/test.ml b/test/test.ml new file mode 100644 index 0000000..c373570 --- /dev/null +++ b/test/test.ml @@ -0,0 +1,419 @@ +open Alcotest +open Types +open Syntax +open Util + +module Test_comment = struct + let f () = + let open Comment in + let result = Alcotest.result string string in + let bool_result = Alcotest.result bool string in + let check_id s = + (check result) "to_string(of_string(s))=id" (Ok s) + (match of_string s with Ok v -> Ok (to_string v) | Error e -> Error e) + in + let check_eq expected s = + (check bool_result) "eq" (Ok true) + (match of_string s with Error e -> Error e | Ok v -> Ok (expected = v)) + in + let l = + [ ("cc sava?", [ Line [ Txt "cc sava?" ] ]) + ; ("cc\n\n sava?", [ Line [ Txt "cc" ]; Line []; Line [ Txt " sava?" ] ]) + ; (">cc sava?", [ Line_quote [ Txt "cc sava?" ] ]) + ; (">cc sava\n?", [ Line_quote [ Txt "cc sava" ]; Line [ Txt "?" ] ]) + ; ("cc >>13 sava?", [ Line [ Txt "cc "; Id 13; Txt " sava?" ] ]) + ; (">cc >>13 sava?", [ Line_quote [ Txt "cc "; Id 13; Txt " sava?" ] ]) + ; (">>13 cc sava?", [ Line [ Id 13; Txt " cc sava?" ] ]) + ; (">>13\n cc sava?", [ Line [ Id 13 ]; Line [ Txt " cc sava?" ] ]) + ; (">>>13\n cc sava?", [ Line_quote [ Id 13 ]; Line [ Txt " cc sava?" ] ]) + ; ( ">>>>13\n cc sava?" + , [ Line_quote [ Txt ">"; Id 13 ]; Line [ Txt " cc sava?" ] ] ) + ; (">>13 >cc sava?", [ Line [ Id 13; Txt " >cc sava?" ] ]) + ; ( ">>>>13\n cc sava?" + , [ Line_quote [ Txt ">"; Id 13 ]; Line [ Txt " cc sava?" ] ] ) + ] + in + List.iter (fun (s, _) -> check_id s) l; + List.iter (fun (s, expected) -> check_eq expected s) l; + () +end + +module Test_json_data = struct + let err () = + let err_result = Alcotest.result err string in + let check e = + (check err_result) "read(write(o))=id" (Ok e) + (Json_data.Write.err e |> Json_data.Read.err) + in + let l = + Err. + [ Internal (Db "a") + ; Internal (Conan "a") + ; Bad_form + ; Forbidden + ; Unauthorized_login "a" + ] + in + List.iter check l +end + +module Test_user = struct + let email = "user_1@test.com" + + let nick = "user_1" + + let password = "hunter2" + + let bio = "hello im user_1 ~~" + + let make () = + (check unit_result) "is ok" (Ok ()) + (let* () = User.register ~email ~nick ~password in + Ok () ); + () + + let login () = + (check unit_result) "is ok" (Ok ()) + (let* _ : User_private.t = User.login ~login:email ~password in + Ok () ); + (check unit_result) "is ok" (Ok ()) + (let* _ : User_private.t = User.login ~login:nick ~password in + Ok () ); + () + + let get_id () = + let* u = User.login ~login:email ~password in + Ok u.user_id + + let get_user () = + (check unit_result) "is ok" (Ok ()) + (let* id = get_id () in + let* _ : user = User.get_user id in + let* _ : User_private.t = User.get_user_private id in + Ok () ); + () + + let get_image () = + (check unit_result) "is ok" (Ok ()) + (let* id = get_id () in + let* _image_data : string = User.get_image id in + Ok () ); + () + + let update () = + let email_2 = "user_2@test.com" in + let nick_2 = "user_2" in + let password_2 = "xXhunter2Xx" in + (check unit_result) "is ok" (Ok ()) + (let* id = get_id () in + let* () = User.update_email id email_2 in + let* () = User.update_nick id nick_2 in + let* () = User.update_password id password_2 in + let* () = User.update_bio id bio in + let* () = + (* should not accept change to already taken value *) + assert_error (User.update_email id email_2) + in + (* revert changes *) + let* () = User.update_email id email in + let* () = User.update_nick id nick in + let* () = User.update_password id password in + Ok () ); + () + + let delete () = + (check unit_result) "is ok" (Ok ()) + (let* id = get_id () in + let* () = User.delete_avatar id in + let* () = User.delete_user id in + let* () = assert_error (User.get_user id) in + let* () = assert_error (User.login ~login:email ~password) in + Ok () ); + () +end + +module Test_post = struct + let post_count = ref 0 + + let subject = "first thread" + + let mk_comment = + fun () -> + incr post_count; + let s = Fmt.str "comment nb %d ^^ ~~!" !post_count in + s + + let assert_post_not_found id = + match Post.get_post id with + | Ok _ -> error "post found" + | Error e -> assert_true (e = Not_found_post id) + + let assert_post_image_found id = + let* _ : string = Post.get_thumbnail_data id in + let* _ : string = Post.get_image_data id in + Ok () + + let assert_post_image_not_found id = + match assert_post_image_found id with + | Ok _ -> error "post image found" + | Error e -> assert_true (e = Not_found_image id) + + let get_first_thread () = + let* catalog = Post.get_catalog () in + match catalog with [] -> error "empty catalog" | hd :: _ -> Ok hd + + let empty_catalog () = + (check unit_result) "is ok" (Ok ()) + (let* catalog = Post.get_catalog () in + assert_true (List.is_empty catalog) ); + () + + let make_thread () = + (check unit_result) "is ok" (Ok ()) + (let* user_id = Test_user.get_id () in + let* user = User.get_user user_id in + let lat, lng = (12.0, -13.0) in + let comment = mk_comment () in + let image_data = None in + let* thread = + Post.make_thread ~comment ~image_data ~subject ~lat ~lng user + in + let post_id = thread.op.id in + let date = thread.op.date in + let* expected_comment = + Comment.of_string comment + |> Result.map_error (fun s -> + Err.Unprocessable (Fmt.str "comment: %s" s) ) + in + let expected_op = + { id = post_id + ; parent_t_id = post_id + ; date + ; poster_id = user_id + ; poster_nick = Test_user.nick + ; comment = expected_comment + ; image_info = None + ; backlinks = [] + } + in + let expected_thread = + Thread_w_reply. + { op = expected_op + ; subject + ; lat + ; lng + ; bump_status = Alive 0 + ; reply_count = 0 + ; reply_l = [ expected_op ] + } + in + assert_true (expected_thread = thread) ); + () + + let make_reply () = + (check unit_result) "is ok" (Ok ()) + (let* user_id = Test_user.get_id () in + let* user = User.get_user user_id in + let* parent_thread = get_first_thread () in + let comment = mk_comment () in + let image_data = None in + let* post = Post.make_post ~comment ~image_data ~parent_thread user in + let* expected_comment = + Comment.of_string comment + |> Result.map_error (fun s -> + Err.Unprocessable (Fmt.str "comment: %s" s) ) + in + let expected_post = + { id = post.id + ; parent_t_id = parent_thread.op.id + ; date = post.date + ; poster_id = user_id + ; poster_nick = Test_user.nick + ; comment = expected_comment + ; image_info = None + ; backlinks = [] + } + in + let* () = assert_true (expected_post = post) in + let* parent_thread_updated = Post.get_thread parent_thread.op.id in + let* () = assert_true @@ (!post_count = 2) in + let* () = + assert_true + ({ parent_thread with reply_count = 1 } = parent_thread_updated) + in + Ok () ); + () + + let make_reply_with_image () = + let f img = + let mime, name, alt, data = img in + let image_data = Some (Some name, alt, data) in + let* user_id = Test_user.get_id () in + let* user = User.get_user user_id in + let* parent_thread = get_first_thread () in + let comment = mk_comment () in + let* post = Post.make_post ~comment ~image_data ~parent_thread user in + let* expected_comment = + Comment.of_string comment + |> Result.map_error (fun s -> + Err.Unprocessable (Fmt.str "comment: %s" s) ) + in + (* image read/write to file for thumbnail creation + strip exif change image data + thumbnail dimension can be <> than image dimension *) + let post_image_info = Option.get post.image_info in + let expected_image_info = + { md5 = post_image_info.md5 + ; mime + ; w = 1 + ; h = 1 + ; thumb_w = post_image_info.thumb_w + ; thumb_h = post_image_info.thumb_h + ; name + ; alt + } + in + let expected_post = + { id = post.id + ; parent_t_id = parent_thread.op.id + ; date = post.date + ; poster_id = user_id + ; poster_nick = Test_user.nick + ; comment = expected_comment + ; image_info = Some expected_image_info + ; backlinks = [] + } + in + Fmt.epr "expected --@.%s@." (Json_data.Write.post expected_post); + Fmt.epr "got --@.%s@." (Json_data.Write.post post); + let* () = assert_true (expected_post = post) in + Ok () + in + (check unit_result) "is ok" (Ok ()) + (let* () = assert_true (Array.length imgs = 4) in + let* () = array_iter f imgs in + Ok () ); + () + + let no_exif () = + let get_img id = Post.get_image_data id in + let get_img_name id = + let* info = Post.get_image_info id in + Ok info.name + in + (check unit_result) "is ok" (Ok ()) + ((* post with fff.png *) + let id1 = 3 in + (* post with fff.exif.png *) + let id2 = 6 in + let* img1 = get_img id1 in + let* img2 = get_img id2 in + let* img1_name = get_img_name id1 in + let* img2_name = get_img_name id2 in + let* () = assert_true (img1_name = "fff.png") in + let* () = assert_true (img2_name = "fff.exif.png") in + let* () = + assert_true + (let regex = Re.Posix.compile_pat "exif" in + Re.exec_opt regex img2 |> Option.is_none ) + in + let* () = assert_true (img1 = img2) in + Ok () ) + + let gets () = + (check unit_result) "is ok" (Ok ()) + (let* thread = get_first_thread () in + let* thread_w_reply = Post.get_thread_w_reply thread.op.id in + let thread' = Util.thread_w_reply_to_simple thread_w_reply in + let* () = assert_true (thread = thread') in + let count = !post_count in + let* () = assert_true @@ (count = 6) in + (* TODO remove op from thread's replies *) + (* - 1 because op does not count as a reply *) + let* () = assert_true @@ (thread.reply_count = count - 1) in + (* but not here because its in the replies list of thread *) + let* () = assert_true @@ (List.length thread_w_reply.reply_l = count) in + (* !! post id start at 1 *) + let* () = assert_error @@ Post.get_post 0 in + (* get all posts an compare them to thread replies *) + let id_l = List.init count (fun i -> i + 1) in + let* post_l = list_map Post.get_post id_l in + let* () = assert_true (thread_w_reply.reply_l = post_l) in + (* check image data *) + (* first 2 post don't have image *) + let* () = assert_post_image_not_found 1 in + let* () = assert_post_image_not_found 2 in + (* last 4 post have images *) + let* () = assert_post_image_found 3 in + let* () = assert_post_image_found 4 in + let* () = assert_post_image_found 5 in + let* () = assert_post_image_found 6 in + Ok () ); + () + + let post_delete id = + let* user_id = Test_user.get_id () in + let* user = User.get_user user_id in + let* () = Post.delete ~user id in + Ok () + + let delete_reply () = + (check unit_result) "is ok" (Ok ()) + ((* delete last post *) + let* () = post_delete !post_count in + let count = !post_count - 1 in + let* () = assert_post_not_found !post_count in + let* () = assert_post_image_not_found !post_count in + let* thread = get_first_thread () in + let* Thread_w_reply. + { op; subject; lat; lng; bump_status; reply_count; reply_l } = + Post.get_thread_w_reply thread.op.id + in + let thread' = { op; subject; lat; lng; bump_status; reply_count } in + let* () = assert_true (thread = thread') in + let id_l = List.init count (fun i -> i + 1) in + let* post_l = list_map Post.get_post id_l in + let* () = assert_true (reply_l = post_l) in + Ok () ); + () + + let delete_thread () = + (check unit_result) "is ok" (Ok ()) + ((* delete first thread *) + let* t_info = get_first_thread () in + let* () = post_delete t_info.op.id in + let* () = assert_error @@ get_first_thread () in + (* check all posts are deleted *) + let id_l = List.init !post_count (fun i -> i + 1) in + let* () = list_iter (fun id -> assert_post_not_found id) id_l in + let* () = list_iter (fun id -> assert_post_image_not_found id) id_l in + Ok () ); + () +end + +let () = + run "Tests" + [ ("Comment read & write", [ test_case "comment" `Quick Test_comment.f ]) + ; ("Json data read & write", [ test_case "err" `Quick Test_json_data.err ]) + ; (* !! order of tests on database is important *) + ( "User" + , Test_user. + [ test_case "make" `Quick make + ; test_case "login" `Quick login + ; test_case "get" `Quick get_user + ; test_case "get_image" `Quick get_image + ; test_case "update" `Quick update + ; test_case "delete" `Quick delete + ; test_case "re make user_1" `Quick make + ] ) + ; ( "Post" + , Test_post. + [ test_case "empty catalog" `Quick empty_catalog + ; test_case "make thread" `Quick make_thread + ; test_case "make reply" `Quick make_reply + ; test_case "make reply with image" `Quick make_reply_with_image + ; test_case "gets" `Quick gets + ; test_case "no exif" `Quick no_exif + ; test_case "delete reply" `Quick delete_reply + ; test_case "delete thread" `Quick delete_thread + ] ) + ] diff --git a/test/util.ml b/test/util.ml new file mode 100644 index 0000000..8be1fbb --- /dev/null +++ b/test/util.ml @@ -0,0 +1,46 @@ +let thread_w_reply_to_simple v = + let open Types in + let Thread_w_reply. + { op; subject; lat; lng; bump_status; reply_count; reply_l = _ } = + v + in + let thread = { op; subject; lat; lng; bump_status; reply_count } in + thread + +(* -- image data -- *) +let imgs = + let mk_img = + let load path = + match Assets.read path with + | None -> Fmt.failwith "test image `%s` not found" path + | Some o -> o + in + fun kind -> + let data = load (Fmt.str "/fff.%s" kind) in + let mime = + Fmt.str "image/%s" (if kind = "exif.png" then "png" else kind) + in + (mime, Fmt.str "fff.%s" kind, Fmt.str "a 1px %s test image" kind, data) + in + Array.map mk_img [| "png"; "jpeg"; "gif"; "exif.png" |] + +(* -- alcotest util -- *) + +let err = Alcotest.testable Err.pp ( = ) + +let result ok_testable = Alcotest.result ok_testable err + +let unit_result = result Alcotest.unit + +let error msg = Error (Err.Unprocessable msg) + +let assert_error res = + match res with Ok _ -> error "assert_error failure" | Error _ -> Ok () + +let assert_true b = + match b with true -> Ok () | false -> error "assert_true failure" + +let option_get opt = + match opt with + | Some v -> Ok v + | None -> error "option_get failure, option is none" diff --git a/test/virtual/dir.ml b/test/virtual/dir.ml new file mode 100644 index 0000000..e477a09 --- /dev/null +++ b/test/virtual/dir.ml @@ -0,0 +1,9 @@ +let config = + match Fpath.of_string "./" with + | Error (`Msg e) -> failwith e + | Ok v -> Some v + +let data = + match Bos.OS.Dir.current () with + | Error (`Msg e) -> failwith e + | Ok current -> Some current diff --git a/test/virtual/dune b/test/virtual/dune new file mode 100644 index 0000000..849c4a5 --- /dev/null +++ b/test/virtual/dune @@ -0,0 +1,44 @@ +(library + (name config_test) + (modules config) + (implements config) + (libraries uri fpath)) + +(library + (name config_serv_test) + (modules config_serv) + (implements config_serv) + (libraries uri fpath)) + +(library + (name dir_test) + (modules dir) + (implements dir) + (libraries fpath directories bos)) + +(executable + (name make_config_test) + (modules make_config_test) + (libraries + dir_test ; implements virtual module + make_config_lib ; virtual lib + fmt + uri + fpath + bos + scfg)) + +(rule + (copy ../config.scfg ./config.scfg)) + +; todo fix "does not exists" on clean build + +(rule + (with-stdout-to + config.ml + (run ./make_config_test.exe --config))) + +(rule + (with-stdout-to + config_serv.ml + (run ./make_config_test.exe --config-serv))) diff --git a/test/virtual/make_config_test.ml b/test/virtual/make_config_test.ml new file mode 100644 index 0000000..a7f9913 --- /dev/null +++ b/test/virtual/make_config_test.ml @@ -0,0 +1 @@ +include Make_config_lib