From 6bbf3f4d6b730dbcdc482798fa301a836bd78018 Mon Sep 17 00:00:00 2001 From: Omur Date: Tue, 1 Jul 2025 22:12:20 +0300 Subject: [PATCH] capital starter https://github.com/Checkmarx/capital --- python/capital/CONTRIBUTING.md | 29 + python/capital/Dockerfile | 10 + python/capital/LICENSE | 663 + python/capital/README.md | 220 + python/capital/alembic.ini | 36 + python/capital/app/__init__.py | 0 python/capital/app/api/__init__.py | 0 .../capital/app/api/dependencies/__init__.py | 0 .../capital/app/api/dependencies/articles.py | 59 + .../app/api/dependencies/authentication.py | 111 + .../capital/app/api/dependencies/comments.py | 47 + .../capital/app/api/dependencies/database.py | 30 + .../capital/app/api/dependencies/profiles.py | 29 + python/capital/app/api/errors/__init__.py | 0 python/capital/app/api/errors/http_error.py | 7 + .../app/api/errors/validation_error.py | 28 + python/capital/app/api/routes/__init__.py | 0 python/capital/app/api/routes/admin.py | 26 + python/capital/app/api/routes/api.py | 23 + .../app/api/routes/articles/__init__.py | 0 python/capital/app/api/routes/articles/api.py | 8 + .../api/routes/articles/articles_common.py | 104 + .../api/routes/articles/articles_resource.py | 135 + .../capital/app/api/routes/authentication.py | 68 + .../app/api/routes/authentication_old.py | 43 + python/capital/app/api/routes/comments.py | 84 + python/capital/app/api/routes/debug.py | 58 + python/capital/app/api/routes/logging.py | 26 + python/capital/app/api/routes/membership.py | 33 + python/capital/app/api/routes/profiles.py | 90 + python/capital/app/api/routes/register.py | 69 + python/capital/app/api/routes/tags.py | 15 + python/capital/app/api/routes/users.py | 74 + python/capital/app/core/__init__.py | 0 python/capital/app/core/config.py | 21 + python/capital/app/core/events.py | 25 + python/capital/app/core/logging.py | 25 + python/capital/app/core/settings/__init__.py | 0 python/capital/app/core/settings/app.py | 58 + python/capital/app/core/settings/base.py | 16 + .../capital/app/core/settings/development.py | 14 + .../capital/app/core/settings/production.py | 6 + python/capital/app/core/settings/test.py | 19 + python/capital/app/db/__init__.py | 0 python/capital/app/db/errors.py | 2 + python/capital/app/db/events.py | 25 + python/capital/app/db/migrations/env.py | 38 + .../capital/app/db/migrations/script.py.mako | 23 + .../db/migrations/versions/fdf8821871d7.py | 326 + python/capital/app/db/queries/__init__.py | 0 python/capital/app/db/queries/queries.py | 5 + python/capital/app/db/queries/queries.pyi | 127 + .../capital/app/db/queries/sql/articles.sql | 125 + .../capital/app/db/queries/sql/comments.sql | 40 + .../capital/app/db/queries/sql/profiles.sql | 38 + python/capital/app/db/queries/sql/tags.sql | 9 + python/capital/app/db/queries/sql/users.sql | 52 + python/capital/app/db/queries/tables.py | 75 + .../capital/app/db/repositories/__init__.py | 0 .../capital/app/db/repositories/articles.py | 430 + python/capital/app/db/repositories/base.py | 10 + .../capital/app/db/repositories/comments.py | 103 + .../capital/app/db/repositories/profiles.py | 74 + python/capital/app/db/repositories/tags.py | 13 + python/capital/app/db/repositories/users.py | 93 + python/capital/app/main.py | 43 + python/capital/app/models/__init__.py | 0 python/capital/app/models/common.py | 19 + python/capital/app/models/domain/__init__.py | 0 python/capital/app/models/domain/articles.py | 16 + python/capital/app/models/domain/comments.py | 8 + python/capital/app/models/domain/profiles.py | 15 + python/capital/app/models/domain/rwmodel.py | 21 + python/capital/app/models/domain/users.py | 26 + python/capital/app/models/schemas/__init__.py | 0 python/capital/app/models/schemas/articles.py | 43 + python/capital/app/models/schemas/comments.py | 16 + python/capital/app/models/schemas/debug.py | 14 + python/capital/app/models/schemas/jwt.py | 12 + python/capital/app/models/schemas/profiles.py | 7 + python/capital/app/models/schemas/rwschema.py | 6 + python/capital/app/models/schemas/tags.py | 7 + python/capital/app/models/schemas/users.py | 48 + python/capital/app/resources/__init__.py | 0 python/capital/app/resources/strings.py | 123 + python/capital/app/services/__init__.py | 0 python/capital/app/services/articles.py | 23 + python/capital/app/services/authentication.py | 20 + python/capital/app/services/comments.py | 6 + python/capital/app/services/jwt.py | 41 + python/capital/app/services/security.py | 16 + python/capital/docker-compose.yml | 33 + python/capital/frontend/LICENSE.md | 21 + python/capital/frontend/README.md | 58 + python/capital/frontend/cypress.json | 9 + python/capital/frontend/cypress/README.md | 22 + .../frontend/cypress/fixtures/example.json | 5 + .../cypress/integration/article-spec.js | 177 + .../cypress/integration/editor-spec.js | 175 + .../frontend/cypress/integration/home-spec.js | 176 + .../cypress/integration/login-spec.js | 65 + .../cypress/integration/navigation-spec.js | 190 + .../cypress/integration/profile-spec.js | 67 + .../cypress/integration/register-spec.js | 103 + .../cypress/integration/settings-spec.js | 54 + .../capital/frontend/cypress/plugins/index.js | 172 + .../capital/frontend/cypress/support/index.js | 74 + python/capital/frontend/dockerfile | 7 + python/capital/frontend/package.json | 59 + python/capital/frontend/project-logo.png | Bin 0 -> 72206 bytes python/capital/frontend/public/favicon.ico | Bin 0 -> 24838 bytes python/capital/frontend/public/index.html | 48 + .../capital/frontend/public/logo-banner.png | Bin 0 -> 55673 bytes python/capital/frontend/public/main.css | 14793 ++++++++++++++++ python/capital/frontend/src/agent.js | 378 + python/capital/frontend/src/app/history.js | 3 + python/capital/frontend/src/app/middleware.js | 31 + python/capital/frontend/src/app/store.js | 38 + python/capital/frontend/src/common/utils.js | 43 + .../frontend/src/components/AdminPage.js | 20 + python/capital/frontend/src/components/App.js | 101 + .../src/components/Article/ArticleActions.js | 39 + .../src/components/Article/ArticleMeta.js | 48 + .../frontend/src/components/Article/index.js | 82 + .../frontend/src/components/ArticleList.js | 35 + .../frontend/src/components/ArticlePreview.js | 92 + .../capital/frontend/src/components/Editor.js | 213 + .../capital/frontend/src/components/Footer.js | 16 + .../capital/frontend/src/components/Header.js | 115 + .../frontend/src/components/Home/Banner.js | 32 + .../frontend/src/components/Home/MainView.js | 119 + .../frontend/src/components/Home/index.js | 47 + .../frontend/src/components/ListErrors.js | 28 + .../frontend/src/components/ListPagination.js | 56 + .../frontend/src/components/Logging.js | 20 + .../src/components/Membership/Membership.js | 170 + .../frontend/src/components/Profile.js | 216 + .../frontend/src/features/auth/AuthScreen.js | 133 + .../src/features/auth/AuthScreen.spec.js | 183 + .../src/features/auth/SettingsScreen.js | 217 + .../src/features/auth/SettingsScreen.spec.js | 162 + .../frontend/src/features/auth/authSlice.js | 241 + .../src/features/auth/authSlice.spec.js | 454 + .../src/features/comments/CommentList.js | 131 + .../src/features/comments/CommentSection.js | 104 + .../features/comments/CommentSection.spec.js | 260 + .../src/features/comments/commentsSlice.js | 205 + .../features/comments/commentsSlice.spec.js | 466 + .../frontend/src/features/tags/TagsList.js | 23 + .../frontend/src/features/tags/TagsSidebar.js | 60 + .../src/features/tags/TagsSidebar.spec.js | 42 + .../frontend/src/features/tags/tagsSlice.js | 72 + .../src/features/tags/tagsSlice.spec.js | 67 + python/capital/frontend/src/index.js | 27 + .../capital/frontend/src/reducers/article.js | 90 + .../frontend/src/reducers/articleList.js | 135 + .../capital/frontend/src/reducers/common.js | 94 + .../capital/frontend/src/reducers/profile.js | 36 + python/capital/frontend/src/setupTests.js | 1 + python/capital/frontend/src/test/utils.js | 20 + python/capital/frontend/yarn.lock | 11517 ++++++++++++ python/capital/lists/common_endpoints.txt | 8 + python/capital/lists/common_passwords.txt | 244 + python/capital/main.py | 11 + python/capital/postman/API endpoints.PNG | Bin 0 -> 81342 bytes .../postman/capital.postman_collection.json | 2225 +++ python/capital/postman/run-api-tests.sh | 16 + python/capital/redis/Dockerfile | 4 + python/capital/redis/dump.rdb | Bin 0 -> 119 bytes python/capital/requirements.txt | 18 + python/capital/scripts/format | 8 + python/capital/scripts/lint | 11 + python/capital/scripts/test | 6 + python/capital/scripts/test-cov-html | 6 + python/capital/setup.cfg | 88 + python/capital/tests/__init__.py | 0 python/capital/tests/conftest.py | 93 + python/capital/tests/fake_asyncpg_pool.py | 46 + python/capital/tests/test_api/__init__.py | 0 .../tests/test_api/test_errors/__init__.py | 0 .../test_api/test_errors/test_422_error.py | 20 + .../tests/test_api/test_errors/test_error.py | 16 + .../tests/test_api/test_routes/__init__.py | 0 .../test_api/test_routes/test_articles.py | 575 + .../test_routes/test_authentication.py | 32 + .../test_api/test_routes/test_comments.py | 96 + .../tests/test_api/test_routes/test_login.py | 34 + .../test_api/test_routes/test_profiles.py | 181 + .../test_api/test_routes/test_registration.py | 56 + .../tests/test_api/test_routes/test_tags.py | 28 + .../tests/test_api/test_routes/test_users.py | 140 + python/capital/tests/test_db/__init__.py | 0 .../tests/test_db/test_queries/__init__.py | 0 .../tests/test_db/test_queries/test_tables.py | 19 + python/capital/tests/test_schemas/__init__.py | 0 .../tests/test_schemas/test_rw_model.py | 8 + .../capital/tests/test_services/__init__.py | 0 .../capital/tests/test_services/test_jwt.py | 51 + 198 files changed, 41477 insertions(+) create mode 100644 python/capital/CONTRIBUTING.md create mode 100644 python/capital/Dockerfile create mode 100644 python/capital/LICENSE create mode 100644 python/capital/README.md create mode 100644 python/capital/alembic.ini create mode 100644 python/capital/app/__init__.py create mode 100644 python/capital/app/api/__init__.py create mode 100644 python/capital/app/api/dependencies/__init__.py create mode 100644 python/capital/app/api/dependencies/articles.py create mode 100644 python/capital/app/api/dependencies/authentication.py create mode 100644 python/capital/app/api/dependencies/comments.py create mode 100644 python/capital/app/api/dependencies/database.py create mode 100644 python/capital/app/api/dependencies/profiles.py create mode 100644 python/capital/app/api/errors/__init__.py create mode 100644 python/capital/app/api/errors/http_error.py create mode 100644 python/capital/app/api/errors/validation_error.py create mode 100644 python/capital/app/api/routes/__init__.py create mode 100644 python/capital/app/api/routes/admin.py create mode 100644 python/capital/app/api/routes/api.py create mode 100644 python/capital/app/api/routes/articles/__init__.py create mode 100644 python/capital/app/api/routes/articles/api.py create mode 100644 python/capital/app/api/routes/articles/articles_common.py create mode 100644 python/capital/app/api/routes/articles/articles_resource.py create mode 100644 python/capital/app/api/routes/authentication.py create mode 100644 python/capital/app/api/routes/authentication_old.py create mode 100644 python/capital/app/api/routes/comments.py create mode 100644 python/capital/app/api/routes/debug.py create mode 100644 python/capital/app/api/routes/logging.py create mode 100644 python/capital/app/api/routes/membership.py create mode 100644 python/capital/app/api/routes/profiles.py create mode 100644 python/capital/app/api/routes/register.py create mode 100644 python/capital/app/api/routes/tags.py create mode 100644 python/capital/app/api/routes/users.py create mode 100644 python/capital/app/core/__init__.py create mode 100644 python/capital/app/core/config.py create mode 100644 python/capital/app/core/events.py create mode 100644 python/capital/app/core/logging.py create mode 100644 python/capital/app/core/settings/__init__.py create mode 100644 python/capital/app/core/settings/app.py create mode 100644 python/capital/app/core/settings/base.py create mode 100644 python/capital/app/core/settings/development.py create mode 100644 python/capital/app/core/settings/production.py create mode 100644 python/capital/app/core/settings/test.py create mode 100644 python/capital/app/db/__init__.py create mode 100644 python/capital/app/db/errors.py create mode 100644 python/capital/app/db/events.py create mode 100644 python/capital/app/db/migrations/env.py create mode 100644 python/capital/app/db/migrations/script.py.mako create mode 100644 python/capital/app/db/migrations/versions/fdf8821871d7.py create mode 100644 python/capital/app/db/queries/__init__.py create mode 100644 python/capital/app/db/queries/queries.py create mode 100644 python/capital/app/db/queries/queries.pyi create mode 100644 python/capital/app/db/queries/sql/articles.sql create mode 100644 python/capital/app/db/queries/sql/comments.sql create mode 100644 python/capital/app/db/queries/sql/profiles.sql create mode 100644 python/capital/app/db/queries/sql/tags.sql create mode 100644 python/capital/app/db/queries/sql/users.sql create mode 100644 python/capital/app/db/queries/tables.py create mode 100644 python/capital/app/db/repositories/__init__.py create mode 100644 python/capital/app/db/repositories/articles.py create mode 100644 python/capital/app/db/repositories/base.py create mode 100644 python/capital/app/db/repositories/comments.py create mode 100644 python/capital/app/db/repositories/profiles.py create mode 100644 python/capital/app/db/repositories/tags.py create mode 100644 python/capital/app/db/repositories/users.py create mode 100644 python/capital/app/main.py create mode 100644 python/capital/app/models/__init__.py create mode 100644 python/capital/app/models/common.py create mode 100644 python/capital/app/models/domain/__init__.py create mode 100644 python/capital/app/models/domain/articles.py create mode 100644 python/capital/app/models/domain/comments.py create mode 100644 python/capital/app/models/domain/profiles.py create mode 100644 python/capital/app/models/domain/rwmodel.py create mode 100644 python/capital/app/models/domain/users.py create mode 100644 python/capital/app/models/schemas/__init__.py create mode 100644 python/capital/app/models/schemas/articles.py create mode 100644 python/capital/app/models/schemas/comments.py create mode 100644 python/capital/app/models/schemas/debug.py create mode 100644 python/capital/app/models/schemas/jwt.py create mode 100644 python/capital/app/models/schemas/profiles.py create mode 100644 python/capital/app/models/schemas/rwschema.py create mode 100644 python/capital/app/models/schemas/tags.py create mode 100644 python/capital/app/models/schemas/users.py create mode 100644 python/capital/app/resources/__init__.py create mode 100644 python/capital/app/resources/strings.py create mode 100644 python/capital/app/services/__init__.py create mode 100644 python/capital/app/services/articles.py create mode 100644 python/capital/app/services/authentication.py create mode 100644 python/capital/app/services/comments.py create mode 100644 python/capital/app/services/jwt.py create mode 100644 python/capital/app/services/security.py create mode 100644 python/capital/docker-compose.yml create mode 100644 python/capital/frontend/LICENSE.md create mode 100644 python/capital/frontend/README.md create mode 100644 python/capital/frontend/cypress.json create mode 100644 python/capital/frontend/cypress/README.md create mode 100644 python/capital/frontend/cypress/fixtures/example.json create mode 100644 python/capital/frontend/cypress/integration/article-spec.js create mode 100644 python/capital/frontend/cypress/integration/editor-spec.js create mode 100644 python/capital/frontend/cypress/integration/home-spec.js create mode 100644 python/capital/frontend/cypress/integration/login-spec.js create mode 100644 python/capital/frontend/cypress/integration/navigation-spec.js create mode 100644 python/capital/frontend/cypress/integration/profile-spec.js create mode 100644 python/capital/frontend/cypress/integration/register-spec.js create mode 100644 python/capital/frontend/cypress/integration/settings-spec.js create mode 100644 python/capital/frontend/cypress/plugins/index.js create mode 100644 python/capital/frontend/cypress/support/index.js create mode 100644 python/capital/frontend/dockerfile create mode 100644 python/capital/frontend/package.json create mode 100644 python/capital/frontend/project-logo.png create mode 100644 python/capital/frontend/public/favicon.ico create mode 100644 python/capital/frontend/public/index.html create mode 100644 python/capital/frontend/public/logo-banner.png create mode 100644 python/capital/frontend/public/main.css create mode 100644 python/capital/frontend/src/agent.js create mode 100644 python/capital/frontend/src/app/history.js create mode 100644 python/capital/frontend/src/app/middleware.js create mode 100644 python/capital/frontend/src/app/store.js create mode 100644 python/capital/frontend/src/common/utils.js create mode 100644 python/capital/frontend/src/components/AdminPage.js create mode 100644 python/capital/frontend/src/components/App.js create mode 100644 python/capital/frontend/src/components/Article/ArticleActions.js create mode 100644 python/capital/frontend/src/components/Article/ArticleMeta.js create mode 100644 python/capital/frontend/src/components/Article/index.js create mode 100644 python/capital/frontend/src/components/ArticleList.js create mode 100644 python/capital/frontend/src/components/ArticlePreview.js create mode 100644 python/capital/frontend/src/components/Editor.js create mode 100644 python/capital/frontend/src/components/Footer.js create mode 100644 python/capital/frontend/src/components/Header.js create mode 100644 python/capital/frontend/src/components/Home/Banner.js create mode 100644 python/capital/frontend/src/components/Home/MainView.js create mode 100644 python/capital/frontend/src/components/Home/index.js create mode 100644 python/capital/frontend/src/components/ListErrors.js create mode 100644 python/capital/frontend/src/components/ListPagination.js create mode 100644 python/capital/frontend/src/components/Logging.js create mode 100644 python/capital/frontend/src/components/Membership/Membership.js create mode 100644 python/capital/frontend/src/components/Profile.js create mode 100644 python/capital/frontend/src/features/auth/AuthScreen.js create mode 100644 python/capital/frontend/src/features/auth/AuthScreen.spec.js create mode 100644 python/capital/frontend/src/features/auth/SettingsScreen.js create mode 100644 python/capital/frontend/src/features/auth/SettingsScreen.spec.js create mode 100644 python/capital/frontend/src/features/auth/authSlice.js create mode 100644 python/capital/frontend/src/features/auth/authSlice.spec.js create mode 100644 python/capital/frontend/src/features/comments/CommentList.js create mode 100644 python/capital/frontend/src/features/comments/CommentSection.js create mode 100644 python/capital/frontend/src/features/comments/CommentSection.spec.js create mode 100644 python/capital/frontend/src/features/comments/commentsSlice.js create mode 100644 python/capital/frontend/src/features/comments/commentsSlice.spec.js create mode 100644 python/capital/frontend/src/features/tags/TagsList.js create mode 100644 python/capital/frontend/src/features/tags/TagsSidebar.js create mode 100644 python/capital/frontend/src/features/tags/TagsSidebar.spec.js create mode 100644 python/capital/frontend/src/features/tags/tagsSlice.js create mode 100644 python/capital/frontend/src/features/tags/tagsSlice.spec.js create mode 100644 python/capital/frontend/src/index.js create mode 100644 python/capital/frontend/src/reducers/article.js create mode 100644 python/capital/frontend/src/reducers/articleList.js create mode 100644 python/capital/frontend/src/reducers/common.js create mode 100644 python/capital/frontend/src/reducers/profile.js create mode 100644 python/capital/frontend/src/setupTests.js create mode 100644 python/capital/frontend/src/test/utils.js create mode 100644 python/capital/frontend/yarn.lock create mode 100644 python/capital/lists/common_endpoints.txt create mode 100644 python/capital/lists/common_passwords.txt create mode 100644 python/capital/main.py create mode 100644 python/capital/postman/API endpoints.PNG create mode 100644 python/capital/postman/capital.postman_collection.json create mode 100644 python/capital/postman/run-api-tests.sh create mode 100644 python/capital/redis/Dockerfile create mode 100644 python/capital/redis/dump.rdb create mode 100644 python/capital/requirements.txt create mode 100644 python/capital/scripts/format create mode 100644 python/capital/scripts/lint create mode 100644 python/capital/scripts/test create mode 100644 python/capital/scripts/test-cov-html create mode 100644 python/capital/setup.cfg create mode 100644 python/capital/tests/__init__.py create mode 100644 python/capital/tests/conftest.py create mode 100644 python/capital/tests/fake_asyncpg_pool.py create mode 100644 python/capital/tests/test_api/__init__.py create mode 100644 python/capital/tests/test_api/test_errors/__init__.py create mode 100644 python/capital/tests/test_api/test_errors/test_422_error.py create mode 100644 python/capital/tests/test_api/test_errors/test_error.py create mode 100644 python/capital/tests/test_api/test_routes/__init__.py create mode 100644 python/capital/tests/test_api/test_routes/test_articles.py create mode 100644 python/capital/tests/test_api/test_routes/test_authentication.py create mode 100644 python/capital/tests/test_api/test_routes/test_comments.py create mode 100644 python/capital/tests/test_api/test_routes/test_login.py create mode 100644 python/capital/tests/test_api/test_routes/test_profiles.py create mode 100644 python/capital/tests/test_api/test_routes/test_registration.py create mode 100644 python/capital/tests/test_api/test_routes/test_tags.py create mode 100644 python/capital/tests/test_api/test_routes/test_users.py create mode 100644 python/capital/tests/test_db/__init__.py create mode 100644 python/capital/tests/test_db/test_queries/__init__.py create mode 100644 python/capital/tests/test_db/test_queries/test_tables.py create mode 100644 python/capital/tests/test_schemas/__init__.py create mode 100644 python/capital/tests/test_schemas/test_rw_model.py create mode 100644 python/capital/tests/test_services/__init__.py create mode 100644 python/capital/tests/test_services/test_jwt.py diff --git a/python/capital/CONTRIBUTING.md b/python/capital/CONTRIBUTING.md new file mode 100644 index 000000000..5380a120c --- /dev/null +++ b/python/capital/CONTRIBUTING.md @@ -0,0 +1,29 @@ +# How to contribute to c{api}tal? + +c{api}tal is an open source project. As such, all contributions and suggestions are welcome. +You can contribute in many ways: giving ideas, answering questions, reporting bugs, proposing enhancements, +improving the documentation, fixing bugs, etc... +Many thanks in advance for every contribution. + +In order to facilitate healthy, constructive behavior in an open and inclusive community, +we all respect and abide by our code of conduct. + +## How to work on an open Issue? + +The list of open issues can be found at: `https://github.com/Checkmarx/capital/issues` +If you would like to work on any of the open Issues: +- Make sure it is not already assigned to someone else. +The assignee (if any) can be found on the top of the right column of the Issue page. +- You can self-assign it by commenting on the Issue page with one of the keywords: `#take` or `#self-assign`. +- Work on your self-assigned issue and eventually create a Pull Request. + + +## How to create a Pull Request? + +1. Fork the repository +2. Clone your fork +3. Create a new branch to hold your development changes: +4. Develop the features in your branch +5. Once you're happy with your code, add your changes and make a commit to record your changes locally +6. Push the changes +7. Click on `"Pull request"` to send it to the project maintainers for review diff --git a/python/capital/Dockerfile b/python/capital/Dockerfile new file mode 100644 index 000000000..3d075c1f3 --- /dev/null +++ b/python/capital/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.9.10-slim +COPY app /capital/app +COPY requirements.txt /capital +COPY alembic.ini /capital +COPY main.py /capital +WORKDIR /capital +RUN apt update -y +RUN apt install -y procps +RUN pip install -r requirements.txt +CMD ["python3", "main.py"] diff --git a/python/capital/LICENSE b/python/capital/LICENSE new file mode 100644 index 000000000..7e666f157 --- /dev/null +++ b/python/capital/LICENSE @@ -0,0 +1,663 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + c{api}tal is a vulnerable by design application that contains 10 API challenges which map to the OWASP top 10 API risks. + It is built with Python (FastAPI) and JS (React). + c{api}tal can also be used for conducting your own API Security CTF event. + Copyright (C) 2022 Checkmarx + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/python/capital/README.md b/python/capital/README.md new file mode 100644 index 000000000..3786c004f --- /dev/null +++ b/python/capital/README.md @@ -0,0 +1,220 @@ +

+ +

+ +[![API Security Top 10](https://img.shields.io/badge/API%20Security-top%2010-blue)](https://github.com/OWASP/API-Security) +[![Vulnerable](https://img.shields.io/badge/Vulnerable-by%20design-blue)](https://github.com/Checkmarx/capital#quickstart) +[![license](https://img.shields.io/badge/license-AGPL%20V3-blue)](https://opensource.org/licenses/AGPL-3.0) + +Quick facts +---------- + + - **Name**: 'c{api}tal' + - **Type**: Vulnerable API Security application + - **Purpose**: Educational + - **License**: GNU AFFERO GENERAL PUBLIC LICENSE + - **Language**: Python, JS + - **Author**: Checkmarx Research team + +Description +---------- +The Checkmarx research team created c{api}tal to provide users with an active playground in which they hone their API Security skills.
The c{api}tal application contains 10 API challenges which map to the OWASP top 10 API risks.
It is built with Python (FastAPI) and JS (React). + +c{api}tal can also be used for conducting your own API Security CTF event. + +Features: +---------- +Contains 10 challenges based on the OWASP top 10 API risks + +* Built on FastAPI (backend) and React (frontend) +* UI - Blogging website (i.e medium) +* OpenAPI3 API JSON specification file that can be imported as a POSTMAN collection +* JWT token based authentication (lifetime can be adjusted in app) + + +c{api}tal is a blogging application which allow users to register, create and delete posts, +create and delete comments, follow other users, and more. + +

+ +

+ +# Quickstart + +Run the full application using docker-compose: + + docker-compose up -d + + +The backend will be running on http://localhost:8000/
+The frontend will be running on http://localhost:4100/
+Check out the API endpoints specification page at http://localhost:8000/docs
+ +Generate API requests to http://localhost:8000/api (via POSTMAN/Burp for example)
+Import the API collection JSON file to POSTMAN and start generating API requests:
+[click here to download the c{api}tal API json collection file](https://www.capital-ctf.com/files/de1ad03a48959f38c7f131f81f95d42e/capital.postman_collection.json) + +

+ +

+ +To run the web application in debug: +---------- + +First, run ``PostgreSQL``, set environment variables and create database: + + export POSTGRES_DB=rwdb POSTGRES_PORT=5432 POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres + docker run --name pgdb --rm -p 5432:5432 -e POSTGRES_USER="$POSTGRES_USER" -e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" -e POSTGRES_DB="$POSTGRES_DB" postgres + export POSTGRES_HOST=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' pgdb) + createdb --host=$POSTGRES_HOST --port=$POSTGRES_PORT --username=$POSTGRES_USER $POSTGRES_DB + +[Option 1] Run locally + +Then run the following commands to bootstrap your environment: + + git clone https://github.com/Checkmarx/capital + cd capital + pip install -r requirements.txt + + +Then create ``.env`` file in project root and set environment variables for application: + + export POSTGRES_DB=rwdb POSTGRES_PORT=5432 POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres + export POSTGRES_HOST=localhost + export DATABASE_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DB + touch .env + echo APP_ENV=dev + echo DATABASE_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DB >> .env + echo SECRET_KEY=$(openssl rand -hex 32) >> .env + +Then run the backend server: + + python3 + .py + +[Option 2] Run backend using docker +Run the backend using docker build: + + docker build . -t capital + docker run -p 8000:8000 -e DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:5432/rwdb --rm --name backend-capital capital + + +Run tests +--------- + +Tests for this project are defined in the ``tests/`` folder. + +Set up environment variable ``DATABASE_URL`` or set up ``database_url`` in ``app/core/settings/test.py`` + +This project uses `pytest +`_ to define tests because it allows you to use the ``assert`` keyword with good formatting for failed assertations. + + +To run all the tests of a project, simply run the ``pytest`` command: :: + + $ pytest + ================================================= test session starts ================================================== + platform linux -- Python 3.8.3, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 + rootdir: /home/some-user/user-projects/fastapi-realworld-example-app, inifile: setup.cfg, testpaths: tests + plugins: env-0.6.2, cov-2.9.0, asyncio-0.12.0 + collected 90 items + + tests/test_api/test_errors/test_422_error.py . [ 1%] + tests/test_api/test_errors/test_error.py . [ 2%] + tests/test_api/test_routes/test_articles.py ................................. [ 38%] + tests/test_api/test_routes/test_authentication.py .. [ 41%] + tests/test_api/test_routes/test_comments.py .... [ 45%] + tests/test_api/test_routes/test_login.py ... [ 48%] + tests/test_api/test_routes/test_profiles.py ............ [ 62%] + tests/test_api/test_routes/test_registration.py ... [ 65%] + tests/test_api/test_routes/test_tags.py .. [ 67%] + tests/test_api/test_routes/test_users.py .................... [ 90%] + tests/test_db/test_queries/test_tables.py ... [ 93%] + tests/test_schemas/test_rw_model.py . [ 94%] + tests/test_services/test_jwt.py ..... [100%] + + ============================================ 90 passed in 70.50s (0:01:10) ============================================= + $ + +If you want to run a specific test, you can do this with `this +`_ pytest feature: :: + + $ pytest tests/test_api/test_routes/test_users.py::test_user_can_not_take_already_used_credentials + +Web routes +---------- + +All routes are available on ``/docs`` or ``/redoc`` paths with Swagger or ReDoc. + + +Project structure +----------------- + +Files related to application are in the ``app`` or ``tests`` directories. +Application parts are: + + app + ├── api - web related stuff. + │   ├── dependencies - dependencies for routes definition. + │   ├── errors - definition of error handlers. + │   └── routes - web routes. + ├── core - application configuration, startup events, logging. + ├── db - db related stuff. + │   ├── migrations - manually written alembic migrations. + │   └── repositories - all crud stuff. + ├── models - pydantic models for this application. + │   ├── domain - main models that are used almost everywhere. + │   └── schemas - schemas for using in web routes. + ├── resources - strings that are used in web responses. + ├── services - logic that is not just crud related. + ├── credentials - list of common strings for Brute Force. + ├── postman - api json file for postman. + ├── redis - redis docker file and conf file. + ├── scripts + ├── tests + └── main.py - FastAPI application creation and configuration. + +Presented At +---------- + +[Blackhat Europe 2022 Arsenal](https://www.youtube.com/watch?v=OP4X8Sc8hMs) + +[AppSec village at DefCon30](https://www.appsecvillage.com/events/dc-2022/c%7Bapi%7Dtal-api-security-ctf) + +Write-ups & Referrences +---------- +[c{api}tal CTF event sum-up blog](https://checkmarx.com/blog/how-we-created-an-api-security-ctf) + +A great write-up by Maor Tal:
+[Part 1](https://medium.com/@maor_59001/defcon-30-appsec-villiage-ctf-writeup-part-1-1730de791f50)
+[Part 2](https://medium.com/@maor_59001/defcon-30-c-api-tal-ctf-writeup-part-2-ef99a0fc8d28) + + +Stickers from DefCon30:
+ + + + + +Development and Bugs +---------- +Found an issue, or have a great idea? Let us know: + +* E-mail - capital@checkmarx.com + +Contributions are appreciated and can be done via GitHub. + +See CONTRIBUTING.md for more information about how to submit them. + +Thanks +---------- + +This project was created at Checkmarx by [Ravid Mazon](https://www.linkedin.com/in/ravid-mazon) with the help of these great contributors: +[Liad Levy](https://www.linkedin.com/in/liad-levy-93b235211/), +[Yaniv Nizry](https://www.linkedin.com/in/yaniv-n-8b4a76193/), +[Guy Lyuboshits](https://www.linkedin.com/in/guy-lyuboshits-075175165/) + +The application was built base on ``real-world-app`` , we used these awesome repos:
+Backend - FastAPI (Python)
+Frontend - React (JS)
+Thanks again for contributing to the open-source community!
diff --git a/python/capital/alembic.ini b/python/capital/alembic.ini new file mode 100644 index 000000000..2c43e60bc --- /dev/null +++ b/python/capital/alembic.ini @@ -0,0 +1,36 @@ +[alembic] +script_location = ./app/db/migrations + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/python/capital/app/__init__.py b/python/capital/app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/capital/app/api/__init__.py b/python/capital/app/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/capital/app/api/dependencies/__init__.py b/python/capital/app/api/dependencies/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/capital/app/api/dependencies/articles.py b/python/capital/app/api/dependencies/articles.py new file mode 100644 index 000000000..460806c4e --- /dev/null +++ b/python/capital/app/api/dependencies/articles.py @@ -0,0 +1,59 @@ +from typing import Optional + +from fastapi import Depends, HTTPException, Path, Query +from starlette import status + +from app.api.dependencies.authentication import get_current_user_authorizer +from app.api.dependencies.database import get_repository +from app.db.errors import EntityDoesNotExist +from app.db.repositories.articles import ArticlesRepository +from app.models.domain.articles import Article +from app.models.domain.users import User +from app.models.schemas.articles import ( + DEFAULT_ARTICLES_LIMIT, + DEFAULT_ARTICLES_OFFSET, + ArticlesFilters, +) +from app.resources import strings +from app.services.articles import check_user_can_modify_article + + +def get_articles_filters( + tag: Optional[str] = None, + author: Optional[str] = None, + favorited: Optional[str] = None, + limit: int = Query(DEFAULT_ARTICLES_LIMIT, ge=1), + offset: int = Query(DEFAULT_ARTICLES_OFFSET, ge=0), +) -> ArticlesFilters: + return ArticlesFilters( + tag=tag, + author=author, + favorited=favorited, + limit=limit, + offset=offset, + ) + + +async def get_article_by_slug_from_path( + slug: str = Path(..., min_length=1), + user: Optional[User] = Depends(get_current_user_authorizer(required=False)), + articles_repo: ArticlesRepository = Depends(get_repository(ArticlesRepository)), +) -> Article: + try: + return await articles_repo.get_article_by_slug(slug=slug, requested_user=user) + except EntityDoesNotExist: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=strings.ARTICLE_DOES_NOT_EXIST_ERROR, + ) + + +def check_article_modification_permissions( + current_article: Article = Depends(get_article_by_slug_from_path), + user: User = Depends(get_current_user_authorizer()), +) -> None: + if not check_user_can_modify_article(current_article, user): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=strings.USER_IS_NOT_AUTHOR_OF_ARTICLE, + ) diff --git a/python/capital/app/api/dependencies/authentication.py b/python/capital/app/api/dependencies/authentication.py new file mode 100644 index 000000000..dbc8d48c8 --- /dev/null +++ b/python/capital/app/api/dependencies/authentication.py @@ -0,0 +1,111 @@ +# noqa:WPS201 +from typing import Callable, Optional + +from fastapi import Depends, HTTPException, Security +from fastapi.security import APIKeyHeader +from starlette import requests, status +from starlette.exceptions import HTTPException as StarletteHTTPException + +from app.api.dependencies.database import get_repository +from app.core.config import get_app_settings +from app.core.settings.app import AppSettings +from app.db.errors import EntityDoesNotExist +from app.db.repositories.users import UsersRepository +from app.models.domain.users import User +from app.resources import strings +from app.services import jwt + +HEADER_KEY = "Authorization" + + +class RWAPIKeyHeader(APIKeyHeader): + async def __call__( # noqa: WPS610 + self, + request: requests.Request, + ) -> Optional[str]: + try: + return await super().__call__(request) + except StarletteHTTPException as original_auth_exc: + raise HTTPException( + status_code=original_auth_exc.status_code, + detail=strings.AUTHENTICATION_REQUIRED, + ) + + +def get_current_user_authorizer(*, required: bool = True) -> Callable: # type: ignore + return _get_current_user if required else _get_current_user_optional + + +def _get_authorization_header_retriever( + *, + required: bool = True, +) -> Callable: # type: ignore + return _get_authorization_header if required else _get_authorization_header_optional + + +def _get_authorization_header( + api_key: str = Security(RWAPIKeyHeader(name=HEADER_KEY)), + settings: AppSettings = Depends(get_app_settings), +) -> str: + try: + token_prefix, token = api_key.split(" ") + except ValueError: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=strings.WRONG_TOKEN_PREFIX, + ) + if token_prefix != settings.jwt_token_prefix: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=strings.WRONG_TOKEN_PREFIX, + ) + + return token + + +def _get_authorization_header_optional( + authorization: Optional[str] = Security( + RWAPIKeyHeader(name=HEADER_KEY, auto_error=False), + ), + settings: AppSettings = Depends(get_app_settings), +) -> str: + if authorization: + return _get_authorization_header(authorization, settings) + + return "" + + +async def _get_current_user( + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + token: str = Depends(_get_authorization_header_retriever()), + settings: AppSettings = Depends(get_app_settings), +) -> User: + try: + username = jwt.get_username_from_token( + token, + str(settings.secret_key.get_secret_value()), + ) + except ValueError: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=strings.MALFORMED_PAYLOAD, + ) + + try: + return await users_repo.get_user_by_username(username=username) + except EntityDoesNotExist: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=strings.MALFORMED_PAYLOAD, + ) + + +async def _get_current_user_optional( + repo: UsersRepository = Depends(get_repository(UsersRepository)), + token: str = Depends(_get_authorization_header_retriever(required=False)), + settings: AppSettings = Depends(get_app_settings), +) -> Optional[User]: + if token: + return await _get_current_user(repo, token, settings) + + return None diff --git a/python/capital/app/api/dependencies/comments.py b/python/capital/app/api/dependencies/comments.py new file mode 100644 index 000000000..c00a07789 --- /dev/null +++ b/python/capital/app/api/dependencies/comments.py @@ -0,0 +1,47 @@ +from typing import Optional + +from fastapi import Depends, HTTPException, Path +from starlette import status + +from app.api.dependencies import articles, authentication, database +from app.db.errors import EntityDoesNotExist +from app.db.repositories.comments import CommentsRepository +from app.models.domain.articles import Article +from app.models.domain.comments import Comment +from app.models.domain.users import User +from app.resources import strings +from app.services.comments import check_user_can_modify_comment + + +async def get_comment_by_id_from_path( + comment_id: int = Path(..., ge=1), + article: Article = Depends(articles.get_article_by_slug_from_path), + user: Optional[User] = Depends( + authentication.get_current_user_authorizer(required=False), + ), + comments_repo: CommentsRepository = Depends( + database.get_repository(CommentsRepository), + ), +) -> Comment: + try: + return await comments_repo.get_comment_by_id( + comment_id=comment_id, + article=article, + user=user, + ) + except EntityDoesNotExist: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=strings.COMMENT_DOES_NOT_EXIST, + ) + + +def check_comment_modification_permissions( + comment: Comment = Depends(get_comment_by_id_from_path), + user: User = Depends(authentication.get_current_user_authorizer()), +) -> None: + if not check_user_can_modify_comment(comment, user): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=strings.USER_IS_NOT_AUTHOR_OF_ARTICLE, + ) diff --git a/python/capital/app/api/dependencies/database.py b/python/capital/app/api/dependencies/database.py new file mode 100644 index 000000000..cc91306bf --- /dev/null +++ b/python/capital/app/api/dependencies/database.py @@ -0,0 +1,30 @@ +from typing import AsyncGenerator, Callable, Type + +from asyncpg.connection import Connection +from asyncpg.pool import Pool +from fastapi import Depends +from starlette.requests import Request + +from app.db.repositories.base import BaseRepository + + +def _get_db_pool(request: Request) -> Pool: + return request.app.state.pool + + +async def _get_connection_from_pool( + pool: Pool = Depends(_get_db_pool), +) -> AsyncGenerator[Connection, None]: + async with pool.acquire() as conn: + yield conn + + +def get_repository( + repo_type: Type[BaseRepository], +) -> Callable[[Connection], BaseRepository]: + def _get_repo( + conn: Connection = Depends(_get_connection_from_pool), + ) -> BaseRepository: + return repo_type(conn) + + return _get_repo diff --git a/python/capital/app/api/dependencies/profiles.py b/python/capital/app/api/dependencies/profiles.py new file mode 100644 index 000000000..db8f9b078 --- /dev/null +++ b/python/capital/app/api/dependencies/profiles.py @@ -0,0 +1,29 @@ +from typing import Optional + +from fastapi import Depends, HTTPException, Path +from starlette.status import HTTP_404_NOT_FOUND + +from app.api.dependencies.authentication import get_current_user_authorizer +from app.api.dependencies.database import get_repository +from app.db.errors import EntityDoesNotExist +from app.db.repositories.profiles import ProfilesRepository +from app.models.domain.profiles import Profile +from app.models.domain.users import User +from app.resources import strings + + +async def get_profile_by_username_from_path( + username: str = Path(..., min_length=1), + user: Optional[User] = Depends(get_current_user_authorizer(required=False)), + profiles_repo: ProfilesRepository = Depends(get_repository(ProfilesRepository)), +) -> Profile: + try: + return await profiles_repo.get_profile_by_username( + username=username, + requested_user=user, + ) + except EntityDoesNotExist: + raise HTTPException( + status_code=HTTP_404_NOT_FOUND, + detail=strings.USER_DOES_NOT_EXIST_ERROR, + ) diff --git a/python/capital/app/api/errors/__init__.py b/python/capital/app/api/errors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/capital/app/api/errors/http_error.py b/python/capital/app/api/errors/http_error.py new file mode 100644 index 000000000..c50322935 --- /dev/null +++ b/python/capital/app/api/errors/http_error.py @@ -0,0 +1,7 @@ +from fastapi import HTTPException +from starlette.requests import Request +from starlette.responses import JSONResponse + + +async def http_error_handler(_: Request, exc: HTTPException) -> JSONResponse: + return JSONResponse({"errors": [exc.detail]}, status_code=exc.status_code) diff --git a/python/capital/app/api/errors/validation_error.py b/python/capital/app/api/errors/validation_error.py new file mode 100644 index 000000000..a85730c8f --- /dev/null +++ b/python/capital/app/api/errors/validation_error.py @@ -0,0 +1,28 @@ +from typing import Union + +from fastapi.exceptions import RequestValidationError +from fastapi.openapi.constants import REF_PREFIX +from fastapi.openapi.utils import validation_error_response_definition +from pydantic import ValidationError +from starlette.requests import Request +from starlette.responses import JSONResponse +from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY + + +async def http422_error_handler( + _: Request, + exc: Union[RequestValidationError, ValidationError], +) -> JSONResponse: + return JSONResponse( + {"errors": exc.errors()}, + status_code=HTTP_422_UNPROCESSABLE_ENTITY, + ) + + +validation_error_response_definition["properties"] = { + "errors": { + "title": "Errors", + "type": "array", + "items": {"$ref": "{0}ValidationError".format(REF_PREFIX)}, + }, +} diff --git a/python/capital/app/api/routes/__init__.py b/python/capital/app/api/routes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/capital/app/api/routes/admin.py b/python/capital/app/api/routes/admin.py new file mode 100644 index 000000000..8139f72cd --- /dev/null +++ b/python/capital/app/api/routes/admin.py @@ -0,0 +1,26 @@ +from fastapi import APIRouter, Depends + +from app.api.dependencies.authentication import get_current_user_authorizer +from app.core.config import get_app_settings +from app.core.settings.app import AppSettings +from app.models.domain.users import User +from app.models.schemas.users import OnlyCTFResponse +from app.resources import strings +from app.services import jwt + +router = APIRouter() + + +@router.get("", response_model=OnlyCTFResponse, name="admin:get-admin-page", include_in_schema=False) +async def retrieve_admin_page( + user: User = Depends(get_current_user_authorizer()), + settings: AppSettings = Depends(get_app_settings), +) -> OnlyCTFResponse: + token = jwt.create_access_token_for_user( + user, + str(settings.secret_key.get_secret_value()), + ) + return OnlyCTFResponse( + flag=strings.BrokenFunctionLevelAuthorization(), + description=strings.DescriptionBrokenFunctionLevelAuthorization, + ) diff --git a/python/capital/app/api/routes/api.py b/python/capital/app/api/routes/api.py new file mode 100644 index 000000000..c9daab974 --- /dev/null +++ b/python/capital/app/api/routes/api.py @@ -0,0 +1,23 @@ +from fastapi import APIRouter + +from app.api.routes import authentication, comments, profiles, tags, users, admin, authentication_old, debug, register,\ + membership, logging +from app.api.routes.articles import api as articles + +router = APIRouter() +router.include_router(register.router, tags=["authentication"], prefix="/users") +router.include_router(authentication.router, tags=["authentication"], prefix="/v2/users") +router.include_router(authentication_old.router, tags=["authentication"], prefix="/v1/users") +router.include_router(users.router, tags=["users"], prefix="/user") +router.include_router(membership.router, tags=["membership"], prefix="/membership") +router.include_router(admin.router, tags=["admin"], prefix="/admin") +router.include_router(logging.router, tags=["logging"], prefix="/logging") +router.include_router(profiles.router, tags=["profiles"], prefix="/profiles") +router.include_router(articles.router, tags=["articles"]) +router.include_router(debug.router, tags=["debug"], prefix="/debug") +router.include_router( + comments.router, + tags=["comments"], + prefix="/articles/{slug}/comments", +) +router.include_router(tags.router, tags=["tags"], prefix="/tags") diff --git a/python/capital/app/api/routes/articles/__init__.py b/python/capital/app/api/routes/articles/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/capital/app/api/routes/articles/api.py b/python/capital/app/api/routes/articles/api.py new file mode 100644 index 000000000..4a32eee17 --- /dev/null +++ b/python/capital/app/api/routes/articles/api.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +from app.api.routes.articles import articles_common, articles_resource + +router = APIRouter() + +router.include_router(articles_common.router, prefix="/articles") +router.include_router(articles_resource.router, prefix="/articles") diff --git a/python/capital/app/api/routes/articles/articles_common.py b/python/capital/app/api/routes/articles/articles_common.py new file mode 100644 index 000000000..85d434f00 --- /dev/null +++ b/python/capital/app/api/routes/articles/articles_common.py @@ -0,0 +1,104 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from starlette import status + +from app.api.dependencies.articles import get_article_by_slug_from_path +from app.api.dependencies.authentication import get_current_user_authorizer +from app.api.dependencies.database import get_repository +from app.db.repositories.articles import ArticlesRepository +from app.models.domain.articles import Article +from app.models.domain.users import User +from app.models.schemas.articles import ( + DEFAULT_ARTICLES_LIMIT, + DEFAULT_ARTICLES_OFFSET, + ArticleForResponse, + ArticleInResponse, + ListOfArticlesInResponse, +) +from app.resources import strings + +router = APIRouter() + + +@router.get( + "/feed", + response_model=ListOfArticlesInResponse, + name="articles:get-user-feed-articles", +) +async def get_articles_for_user_feed( + limit: int = Query(DEFAULT_ARTICLES_LIMIT, ge=1), + offset: int = Query(DEFAULT_ARTICLES_OFFSET, ge=0), + user: User = Depends(get_current_user_authorizer()), + articles_repo: ArticlesRepository = Depends(get_repository(ArticlesRepository)), +) -> ListOfArticlesInResponse: + articles = await articles_repo.get_articles_for_user_feed( + user=user, + limit=limit, + offset=offset, + ) + articles_for_response = [ + ArticleForResponse(**article.dict()) for article in articles + ] + return ListOfArticlesInResponse( + articles=articles_for_response, + articles_count=await articles_repo.get_articles_for_user_feed_count(user=user), + ) + + +@router.post( + "/{slug}/favorite", + response_model=ArticleInResponse, + name="articles:mark-article-favorite", +) +async def mark_article_as_favorite( + article: Article = Depends(get_article_by_slug_from_path), + user: User = Depends(get_current_user_authorizer()), + articles_repo: ArticlesRepository = Depends(get_repository(ArticlesRepository)), +) -> ArticleInResponse: + if not article.favorited: + await articles_repo.add_article_into_favorites(article=article, user=user) + + return ArticleInResponse( + article=ArticleForResponse.from_orm( + article.copy( + update={ + "favorited": True, + "favorites_count": article.favorites_count + 1, + }, + ), + ), + ) + + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=strings.ARTICLE_IS_ALREADY_FAVORITED, + ) + + +@router.delete( + "/{slug}/favorite", + response_model=ArticleInResponse, + name="articles:unmark-article-favorite", +) +async def remove_article_from_favorites( + article: Article = Depends(get_article_by_slug_from_path), + user: User = Depends(get_current_user_authorizer()), + articles_repo: ArticlesRepository = Depends(get_repository(ArticlesRepository)), +) -> ArticleInResponse: + if article.favorited: + await articles_repo.remove_article_from_favorites(article=article, user=user) + + return ArticleInResponse( + article=ArticleForResponse.from_orm( + article.copy( + update={ + "favorited": False, + "favorites_count": article.favorites_count - 1, + }, + ), + ), + ) + + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=strings.ARTICLE_IS_NOT_FAVORITED, + ) diff --git a/python/capital/app/api/routes/articles/articles_resource.py b/python/capital/app/api/routes/articles/articles_resource.py new file mode 100644 index 000000000..cdbba503c --- /dev/null +++ b/python/capital/app/api/routes/articles/articles_resource.py @@ -0,0 +1,135 @@ +from typing import Optional + +from fastapi import APIRouter, Body, Depends, HTTPException, Response +from starlette import status + +from app.api.dependencies.articles import ( + check_article_modification_permissions, + get_article_by_slug_from_path, + get_articles_filters, +) +from app.api.dependencies.authentication import get_current_user_authorizer +from app.api.dependencies.database import get_repository +from app.db.repositories.articles import ArticlesRepository +from app.models.domain.articles import Article +from app.models.domain.users import User +from app.models.schemas.articles import ( + ArticleForResponse, + ArticleInCreate, + ArticleInResponse, + ArticleInUpdate, + ArticlesFilters, + ListOfArticlesInResponse, +) +from app.resources import strings +from app.services.articles import check_article_exists, get_slug_for_article + +from fastapi.responses import JSONResponse +from fastapi.encoders import jsonable_encoder +from app.models.domain.users import User + +router = APIRouter() + + +@router.get("", response_model=ListOfArticlesInResponse, name="articles:list-articles") +async def list_articles( + articles_filters: ArticlesFilters = Depends(get_articles_filters), + user: Optional[User] = Depends(get_current_user_authorizer(required=False)), + articles_repo: ArticlesRepository = Depends(get_repository(ArticlesRepository)), +) -> ListOfArticlesInResponse: + articles = await articles_repo.filter_articles( + tag=articles_filters.tag, + author=articles_filters.author, + favorited=articles_filters.favorited, + limit=articles_filters.limit, + offset=articles_filters.offset, + requested_user=user, + ) + articles_for_response = [ + ArticleForResponse.from_orm(article) for article in articles + ] + return ListOfArticlesInResponse( + articles=articles_for_response, + articles_count=await articles_repo.get_articles_count( + tag=articles_filters.tag, + author=articles_filters.author, + favorited=articles_filters.favorited),#len(articles) + ) + + +@router.post( + "", + status_code=status.HTTP_201_CREATED, + response_model=ArticleInResponse, + name="articles:create-article", +) +async def create_new_article( + article_create: ArticleInCreate = Body(..., embed=True, alias="article"), + user: User = Depends(get_current_user_authorizer()), + articles_repo: ArticlesRepository = Depends(get_repository(ArticlesRepository)), +) -> ArticleInResponse: + slug = get_slug_for_article(article_create.title) + if await check_article_exists(articles_repo, slug): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=strings.ARTICLE_ALREADY_EXISTS, + ) + if not article_create.title or not article_create.title.strip(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=strings.ARTICLE_TITLE_IS_NULL, + ) + + article = await articles_repo.create_article( + slug=slug, + title=article_create.title, + description=article_create.description, + body=article_create.body, + author=user, + tags=article_create.tags, + ) + return ArticleInResponse(article=ArticleForResponse.from_orm(article)) + + +@router.get("/{slug}", response_model=ArticleInResponse, name="articles:get-article") +async def retrieve_article_by_slug( + article: Article = Depends(get_article_by_slug_from_path), +) -> ArticleInResponse: + return ArticleInResponse(article=ArticleForResponse.from_orm(article)) + + +@router.put( + "/{slug}", + response_model=ArticleInResponse, + name="articles:update-article", + dependencies=[Depends(check_article_modification_permissions)], +) +async def update_article_by_slug( + article_update: ArticleInUpdate = Body(..., embed=True, alias="article"), + current_article: Article = Depends(get_article_by_slug_from_path), + articles_repo: ArticlesRepository = Depends(get_repository(ArticlesRepository)), +) -> ArticleInResponse: + slug = get_slug_for_article(article_update.title) if article_update.title else None + article = await articles_repo.update_article( + article=current_article, + slug=slug, + **article_update.dict(), + ) + return ArticleInResponse(article=ArticleForResponse.from_orm(article)) + + +@router.delete( + "/{slug}", + status_code=status.HTTP_204_NO_CONTENT, + name="articles:delete-article", +) +async def delete_article_by_slug( + article: Article = Depends(get_article_by_slug_from_path), + articles_repo: ArticlesRepository = Depends(get_repository(ArticlesRepository)), + user: User = Depends(get_current_user_authorizer()), +) -> JSONResponse: + await articles_repo.delete_article(article=article) + json_compatible_item_data = jsonable_encoder({"message": "Your article has been deleted"}) + if article.author.username != user.username: + json_compatible_item_data = jsonable_encoder({"message": strings.BOLA(), "description": strings.DescriptionBOLA}) + return JSONResponse(content=json_compatible_item_data) diff --git a/python/capital/app/api/routes/authentication.py b/python/capital/app/api/routes/authentication.py new file mode 100644 index 000000000..7a695542d --- /dev/null +++ b/python/capital/app/api/routes/authentication.py @@ -0,0 +1,68 @@ +from typing import Union + +from fastapi import APIRouter, Body, Depends, HTTPException +from starlette.status import HTTP_201_CREATED, HTTP_400_BAD_REQUEST + +from app.api.dependencies.database import get_repository +from app.core.config import get_app_settings +from app.core.settings.app import AppSettings +from app.db.errors import EntityDoesNotExist +from app.db.repositories.users import UsersRepository +from app.models.schemas.users import ( + UserInLogin, + UserInResponse, + UserWithToken, OnlyCTFResponse, OnlyCTFResponseWithSecret, CTFResponse +) +from app.resources import strings +from app.services import jwt + +router = APIRouter() + + +@router.post("/login", response_model=Union[CTFResponse, UserInResponse] , name="auth:login") +async def login( + user_login: UserInLogin = Body(..., embed=True, alias="user"), + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + settings: AppSettings = Depends(get_app_settings), +) -> Union[CTFResponse, UserInResponse]: + wrong_login_error = HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=strings.INCORRECT_LOGIN_INPUT, + ) + + try: + user = await users_repo.get_user_by_email(email=user_login.email) + except EntityDoesNotExist as existence_error: + raise wrong_login_error from existence_error + + if not user.check_password(user_login.password): + raise wrong_login_error + + token = jwt.create_access_token_for_user( + user, + str(settings.secret_key.get_secret_value()), + ) + if user_login.email == "Pikachu@checkmarx.com": + return CTFResponse( + flag=strings.BrokenUserAuthentication(), + description=strings.DescriptionBrokenUserAuthentication, + user=UserWithToken( + username=user.username, + email=user.email, + image=user.image, + token=token, + bio="THIS IS A TOP SECRET: This application is not logging and monitoring user's activities... You might wanna check the logging endpoint in the application.." + ), + ) + + else: + return UserInResponse( + user=UserWithToken( + username=user.username, + email=user.email, + bio=user.bio, + image=user.image, + token=token, + admin=user.admin + ), + ) diff --git a/python/capital/app/api/routes/authentication_old.py b/python/capital/app/api/routes/authentication_old.py new file mode 100644 index 000000000..390d38480 --- /dev/null +++ b/python/capital/app/api/routes/authentication_old.py @@ -0,0 +1,43 @@ +from fastapi import APIRouter, Body, Depends, HTTPException +from starlette.status import HTTP_400_BAD_REQUEST + +from app.resources.strings import ImproperAssetsManagement, DescriptionImproperAssetsManagement +from app.api.dependencies.database import get_repository +from app.db.errors import EntityDoesNotExist +from app.db.repositories.users import UsersRepository +from app.models.schemas.users import ( + UserInLogin, + UserWithToken, CTFResponse, +) +from app.resources import strings + +router = APIRouter() + + +@router.post("/login", response_model=CTFResponse, name="auth:login", include_in_schema=False) +async def login( + user_login: UserInLogin = Body(..., embed=True, alias="user"), + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), +) -> CTFResponse: + wrong_login_error = HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=strings.INCORRECT_LOGIN_INPUT, + ) + + try: + user = await users_repo.get_user_by_email(email=user_login.email) + except EntityDoesNotExist as existence_error: + raise wrong_login_error from existence_error + + return CTFResponse( + flag=ImproperAssetsManagement(), + description=DescriptionImproperAssetsManagement, + user=UserWithToken( + username=user.username, + email=user.email, + image=user.image, + token="Keep it up!", + bio=user.bio, + ), + ) + diff --git a/python/capital/app/api/routes/comments.py b/python/capital/app/api/routes/comments.py new file mode 100644 index 000000000..c843e0910 --- /dev/null +++ b/python/capital/app/api/routes/comments.py @@ -0,0 +1,84 @@ +from typing import Optional + +from fastapi import APIRouter, Body, Depends, Response, HTTPException +from starlette import status +from app.resources import strings +from app.api.dependencies.articles import get_article_by_slug_from_path +from app.api.dependencies.authentication import get_current_user_authorizer +from app.api.dependencies.comments import ( + check_comment_modification_permissions, + get_comment_by_id_from_path, +) +from app.api.dependencies.database import get_repository +from app.db.repositories.comments import CommentsRepository +from app.models.domain.articles import Article +from app.models.domain.comments import Comment +from app.models.domain.users import User +from app.models.schemas.comments import ( + CommentInCreate, + CommentInResponse, + ListOfCommentsInResponse, +) +from fastapi.responses import JSONResponse +from fastapi.encoders import jsonable_encoder + +router = APIRouter() + + +@router.get( + "", + response_model=ListOfCommentsInResponse, + name="comments:get-comments-for-article", +) +async def list_comments_for_article( + article: Article = Depends(get_article_by_slug_from_path), + user: Optional[User] = Depends(get_current_user_authorizer(required=False)), + comments_repo: CommentsRepository = Depends(get_repository(CommentsRepository)), +) -> ListOfCommentsInResponse: + comments = await comments_repo.get_comments_for_article(article=article, user=user) + return ListOfCommentsInResponse(comments=comments) + + +@router.post( + "", + status_code=status.HTTP_201_CREATED, + response_model=CommentInResponse, + name="comments:create-comment-for-article", +) +async def create_comment_for_article( + comment_create: CommentInCreate = Body(..., embed=True, alias="comment"), + article: Article = Depends(get_article_by_slug_from_path), + user: User = Depends(get_current_user_authorizer()), + comments_repo: CommentsRepository = Depends(get_repository(CommentsRepository)), +) -> CommentInResponse: + if not comment_create.body or not comment_create.body.strip(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=strings.COMMENT_IS_NULL, + ) + comment = await comments_repo.create_comment_for_article( + body=comment_create.body, + article=article, + user=user, + ) + return CommentInResponse(comment=comment) + + +@router.delete( + "/{comment_id}", + status_code=status.HTTP_204_NO_CONTENT, + name="comments:delete-comment-from-article", +) +async def delete_comment_from_article( + comment: Comment = Depends(get_comment_by_id_from_path), + comments_repo: CommentsRepository = Depends(get_repository(CommentsRepository)), + user: User = Depends(get_current_user_authorizer()), + ) -> JSONResponse: + await comments_repo.delete_comment(comment=comment) + json_compatible_item_data = jsonable_encoder({"message": "Your comment has been deleted"}) + if comment.author.username != user.username: + json_compatible_item_data = jsonable_encoder({"message": strings.BOLA(), "description": strings.DescriptionBOLA}) + return JSONResponse(content=json_compatible_item_data) + + + diff --git a/python/capital/app/api/routes/debug.py b/python/capital/app/api/routes/debug.py new file mode 100644 index 000000000..6666b3a1a --- /dev/null +++ b/python/capital/app/api/routes/debug.py @@ -0,0 +1,58 @@ +import subprocess + +from fastapi import APIRouter, Body, Depends, HTTPException +from starlette import status +from starlette.status import HTTP_400_BAD_REQUEST + +from app.api.dependencies.authentication import get_current_user_authorizer + +from app.models.domain.users import User +from app.models.schemas.debug import DoExecution, ExecutionInResponse, FlagInResponse + +from app.resources.strings import Injection, DescriptionInjection + +router = APIRouter() + + +def execute(cmd): + if cmd.startswith("uptime"): + p = subprocess.Popen(cmd, shell=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=True) + err = p.stderr.read() + if "rm" in cmd: + return 0, "Don't delete anything!" + if "||" in cmd: + return 0, "injection block" + if cmd.replace(" ", "") == "uptime" or cmd.replace(" ", "") == "uptime;": + return 1, p.stdout.read().decode() + if cmd != "uptime" and len(err) == 0: + return 2, p.stdout.read().decode() + else: + return 0, "Error" + return 0, {"whitelist": {"commands": ['uptime']}} + +@router.post( + "", + status_code=status.HTTP_200_OK, + name="debug", +) +async def create_comment_for_article( + execution: DoExecution = Body(..., embed=True, alias="body"), + user: User = Depends(get_current_user_authorizer()), +): + code , stdout = execute(execution.command) + if code == 0: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=stdout, + ) + if code == 1: + return ExecutionInResponse(stdout=stdout) + if code == 2: + return FlagInResponse( + flag=Injection(),description=DescriptionInjection,stdout=stdout + ) + diff --git a/python/capital/app/api/routes/logging.py b/python/capital/app/api/routes/logging.py new file mode 100644 index 000000000..6b48f5f55 --- /dev/null +++ b/python/capital/app/api/routes/logging.py @@ -0,0 +1,26 @@ +from fastapi import APIRouter, Depends + +from app.api.dependencies.authentication import get_current_user_authorizer +from app.core.config import get_app_settings +from app.core.settings.app import AppSettings +from app.models.domain.users import User +from app.models.schemas.users import OnlyCTFResponse +from app.resources import strings +from app.services import jwt + +router = APIRouter() + + +@router.get("", response_model=OnlyCTFResponse, name="logging:get-logging-page", include_in_schema=False) +async def retrieve_logging_page( + user: User = Depends(get_current_user_authorizer()), + settings: AppSettings = Depends(get_app_settings), +) -> OnlyCTFResponse: + token = jwt.create_access_token_for_user( + user, + str(settings.secret_key.get_secret_value()), + ) + return OnlyCTFResponse( + flag=strings.InsufficientLogging(), + description=strings.DescriptionInsufficientLogging, + ) diff --git a/python/capital/app/api/routes/membership.py b/python/capital/app/api/routes/membership.py new file mode 100644 index 000000000..875642ee5 --- /dev/null +++ b/python/capital/app/api/routes/membership.py @@ -0,0 +1,33 @@ +from fastapi import APIRouter, Depends +from pydantic import BaseModel + +from app.api.dependencies.authentication import get_current_user_authorizer + +from app.models.domain.users import User +from app.resources.strings import ExcessiveDataExposure, DescriptionExcessiveDataExposure + +router = APIRouter() + + +class Member(BaseModel): + number: str + cvc: str + expiry: str + name: str + + +@router.post("", name="Membership:Subscribe") +async def membership( + member: Member, + user: User = Depends(get_current_user_authorizer()), + +): + if ( + member.number == '4426111122223333' and + member.cvc == '555' and + member.name == 'Team Rocket' and + member.expiry == '0922' + ): + return "{}\n\n{}".format(ExcessiveDataExposure(), DescriptionExcessiveDataExposure) + else: + return "Card declined, try again!" diff --git a/python/capital/app/api/routes/profiles.py b/python/capital/app/api/routes/profiles.py new file mode 100644 index 000000000..9154e0579 --- /dev/null +++ b/python/capital/app/api/routes/profiles.py @@ -0,0 +1,90 @@ +from fastapi import APIRouter, Depends, HTTPException +from starlette.status import HTTP_400_BAD_REQUEST + +from app.api.dependencies.authentication import get_current_user_authorizer +from app.api.dependencies.database import get_repository +from app.api.dependencies.profiles import get_profile_by_username_from_path +from app.db.repositories.profiles import ProfilesRepository +from app.models.domain.profiles import Profile +from app.models.domain.users import User +from app.models.schemas.profiles import ProfileInResponse +from app.resources import strings + +router = APIRouter() + + +@router.get( + "/{username}", + response_model=ProfileInResponse, + name="profiles:get-profile", +) +async def retrieve_profile_by_username( + profile: Profile = Depends(get_profile_by_username_from_path), + user: User = Depends(get_current_user_authorizer()), +) -> ProfileInResponse: + if profile.username == "TeamR$cket": + profile.card_number = "4426111122223333" + profile.card_name = "Team Rocket" + profile.card_cvc = "555" + profile.card_expiry = "0922" + return ProfileInResponse(profile=profile) + + +@router.post( + "/{username}/follow", + response_model=ProfileInResponse, + name="profiles:follow-user", +) +async def follow_for_user( + profile: Profile = Depends(get_profile_by_username_from_path), + user: User = Depends(get_current_user_authorizer()), + profiles_repo: ProfilesRepository = Depends(get_repository(ProfilesRepository)), +) -> ProfileInResponse: + if user.username == profile.username: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=strings.UNABLE_TO_FOLLOW_YOURSELF, + ) + + if profile.following: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=strings.USER_IS_ALREADY_FOLLOWED, + ) + + await profiles_repo.add_user_into_followers( + target_user=profile, + requested_user=user, + ) + + return ProfileInResponse(profile=profile.copy(update={"following": True})) + + +@router.delete( + "/{username}/follow", + response_model=ProfileInResponse, + name="profiles:unsubscribe-from-user", +) +async def unsubscribe_from_user( + profile: Profile = Depends(get_profile_by_username_from_path), + user: User = Depends(get_current_user_authorizer()), + profiles_repo: ProfilesRepository = Depends(get_repository(ProfilesRepository)), +) -> ProfileInResponse: + if user.username == profile.username: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=strings.UNABLE_TO_UNSUBSCRIBE_FROM_YOURSELF, + ) + + if not profile.following: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=strings.USER_IS_NOT_FOLLOWED, + ) + + await profiles_repo.remove_user_from_followers( + target_user=profile, + requested_user=user, + ) + + return ProfileInResponse(profile=profile.copy(update={"following": False})) diff --git a/python/capital/app/api/routes/register.py b/python/capital/app/api/routes/register.py new file mode 100644 index 000000000..785f8d5a2 --- /dev/null +++ b/python/capital/app/api/routes/register.py @@ -0,0 +1,69 @@ +from fastapi import APIRouter, Body, Depends, HTTPException +from starlette.status import HTTP_201_CREATED, HTTP_400_BAD_REQUEST + +from app.api.dependencies.database import get_repository +from app.core.config import get_app_settings +from app.core.settings.app import AppSettings +from app.db.repositories.users import UsersRepository +from app.models.schemas.users import ( + UserInCreate, + UserInResponse, + UserWithToken, +) +from app.resources import strings +from app.services import jwt +from app.services.authentication import check_email_is_taken, check_username_is_taken + +router = APIRouter() + + +@router.post( + "", + status_code=HTTP_201_CREATED, + response_model=UserInResponse, + name="auth:register", +) +async def register( + user_create: UserInCreate = Body(..., embed=True, alias="user"), + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + settings: AppSettings = Depends(get_app_settings), +) -> UserInResponse: + if not user_create.username or not user_create.username.strip(): + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=strings.USERNAME_IS_NULL, + ) + + if not user_create.password: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=strings.PASSWORD_IS_NULL, + ) + + if await check_username_is_taken(users_repo, user_create.username): + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=strings.USERNAME_TAKEN, + ) + + if await check_email_is_taken(users_repo, user_create.email): + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=strings.EMAIL_TAKEN, + ) + + user = await users_repo.create_user(**user_create.dict()) + + token = jwt.create_access_token_for_user( + user, + str(settings.secret_key.get_secret_value()), + ) + return UserInResponse( + user=UserWithToken( + username=user.username, + email=user.email, + bio=user.bio, + image=user.image, + token=token, + ), + ) diff --git a/python/capital/app/api/routes/tags.py b/python/capital/app/api/routes/tags.py new file mode 100644 index 000000000..4706187b4 --- /dev/null +++ b/python/capital/app/api/routes/tags.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter, Depends + +from app.api.dependencies.database import get_repository +from app.db.repositories.tags import TagsRepository +from app.models.schemas.tags import TagsInList + +router = APIRouter() + + +@router.get("", response_model=TagsInList, name="tags:get-all") +async def get_all_tags( + tags_repo: TagsRepository = Depends(get_repository(TagsRepository)), +) -> TagsInList: + tags = await tags_repo.get_all_tags() + return TagsInList(tags=tags) diff --git a/python/capital/app/api/routes/users.py b/python/capital/app/api/routes/users.py new file mode 100644 index 000000000..cc86b5413 --- /dev/null +++ b/python/capital/app/api/routes/users.py @@ -0,0 +1,74 @@ +from fastapi import APIRouter, Body, Depends, HTTPException +from starlette.status import HTTP_400_BAD_REQUEST + +from app.api.dependencies.authentication import get_current_user_authorizer +from app.api.dependencies.database import get_repository +from app.core.config import get_app_settings +from app.core.settings.app import AppSettings +from app.db.repositories.users import UsersRepository +from app.models.domain.users import User +from app.models.schemas.users import UserInResponse, UserInUpdate, UserWithToken +from app.resources import strings +from app.services import jwt +from app.services.authentication import check_email_is_taken, check_username_is_taken + +router = APIRouter() + + +@router.get("", response_model=UserInResponse, name="users:get-current-user") +async def retrieve_current_user( + user: User = Depends(get_current_user_authorizer()), + settings: AppSettings = Depends(get_app_settings), +) -> UserInResponse: + token = jwt.create_access_token_for_user( + user, + str(settings.secret_key.get_secret_value()), + ) + return UserInResponse( + user=UserWithToken( + username=user.username, + email=user.email, + bio=user.bio, + image=user.image, + token=token, + admin=user.admin, + ), + ) + + +@router.put("", response_model=UserInResponse, name="users:update-current-user") +async def update_current_user( + user_update: UserInUpdate = Body(..., embed=True, alias="user"), + current_user: User = Depends(get_current_user_authorizer()), + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + settings: AppSettings = Depends(get_app_settings), +) -> UserInResponse: + if user_update.username and user_update.username != current_user.username: + if await check_username_is_taken(users_repo, user_update.username): + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=strings.USERNAME_TAKEN, + ) + + if user_update.email and user_update.email != current_user.email: + if await check_email_is_taken(users_repo, user_update.email): + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=strings.EMAIL_TAKEN, + ) + user = await users_repo.update_user(user=current_user, **user_update.dict()) + + token = jwt.create_access_token_for_user( + user, + str(settings.secret_key.get_secret_value()), + ) + return UserInResponse( + user=UserWithToken( + username=user.username, + email=user.email, + bio=user.bio, + image=user.image, + token=token, + admin=user.admin + ), + ) diff --git a/python/capital/app/core/__init__.py b/python/capital/app/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/capital/app/core/config.py b/python/capital/app/core/config.py new file mode 100644 index 000000000..87f58a852 --- /dev/null +++ b/python/capital/app/core/config.py @@ -0,0 +1,21 @@ +from functools import lru_cache +from typing import Dict, Type + +from app.core.settings.app import AppSettings +from app.core.settings.base import AppEnvTypes, BaseAppSettings +from app.core.settings.development import DevAppSettings +from app.core.settings.production import ProdAppSettings +from app.core.settings.test import TestAppSettings + +environments: Dict[AppEnvTypes, Type[AppSettings]] = { + AppEnvTypes.dev: DevAppSettings, + AppEnvTypes.prod: ProdAppSettings, + AppEnvTypes.test: TestAppSettings, +} + + +@lru_cache +def get_app_settings() -> AppSettings: + app_env = BaseAppSettings().app_env + config = environments[app_env] + return config() diff --git a/python/capital/app/core/events.py b/python/capital/app/core/events.py new file mode 100644 index 000000000..3e82ee3e7 --- /dev/null +++ b/python/capital/app/core/events.py @@ -0,0 +1,25 @@ +from typing import Callable + +from fastapi import FastAPI +from loguru import logger + +from app.core.settings.app import AppSettings +from app.db.events import close_db_connection, connect_to_db + + +def create_start_app_handler( + app: FastAPI, + settings: AppSettings, +) -> Callable: # type: ignore + async def start_app() -> None: + await connect_to_db(app, settings) + + return start_app + + +def create_stop_app_handler(app: FastAPI) -> Callable: # type: ignore + @logger.catch + async def stop_app() -> None: + await close_db_connection(app) + + return stop_app diff --git a/python/capital/app/core/logging.py b/python/capital/app/core/logging.py new file mode 100644 index 000000000..10ceda76c --- /dev/null +++ b/python/capital/app/core/logging.py @@ -0,0 +1,25 @@ +import logging +from types import FrameType +from typing import cast + +from loguru import logger + + +class InterceptHandler(logging.Handler): + def emit(self, record: logging.LogRecord) -> None: # pragma: no cover + # Get corresponding Loguru level if it exists + try: + level = logger.level(record.levelname).name + except ValueError: + level = str(record.levelno) + + # Find caller from where originated the logged message + frame, depth = logging.currentframe(), 2 + while frame.f_code.co_filename == logging.__file__: # noqa: WPS609 + frame = cast(FrameType, frame.f_back) + depth += 1 + + logger.opt(depth=depth, exception=record.exc_info).log( + level, + record.getMessage(), + ) diff --git a/python/capital/app/core/settings/__init__.py b/python/capital/app/core/settings/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/capital/app/core/settings/app.py b/python/capital/app/core/settings/app.py new file mode 100644 index 000000000..1e8141337 --- /dev/null +++ b/python/capital/app/core/settings/app.py @@ -0,0 +1,58 @@ +import logging +import sys +from typing import Any, Dict, List, Tuple + +from loguru import logger +from pydantic import PostgresDsn, SecretStr + +from app.core.logging import InterceptHandler +from app.core.settings.base import BaseAppSettings +from secrets import token_hex + + +class AppSettings(BaseAppSettings): + debug: bool = False + docs_url: str = "/docs" + openapi_prefix: str = "" + openapi_url: str = "/openapi.json" + redoc_url: str = "/redoc" + title: str = "FastAPI example application" + version: str = "0.0.0" + + database_url: PostgresDsn + max_connection_count: int = 10 + min_connection_count: int = 10 + + secret_key: SecretStr = SecretStr(token_hex(16)) + + api_prefix: str = "/api" + + jwt_token_prefix: str = "Token" + + allowed_hosts: List[str] = ["*"] + + logging_level: int = logging.INFO + loggers: Tuple[str, str] = ("uvicorn.asgi", "uvicorn.access") + + class Config: + validate_assignment = True + + @property + def fastapi_kwargs(self) -> Dict[str, Any]: + return { + "debug": self.debug, + "docs_url": self.docs_url, + "openapi_prefix": self.openapi_prefix, + "openapi_url": self.openapi_url, + "redoc_url": self.redoc_url, + "title": self.title, + "version": self.version, + } + + def configure_logging(self) -> None: + logging.getLogger().handlers = [InterceptHandler()] + for logger_name in self.loggers: + logging_logger = logging.getLogger(logger_name) + logging_logger.handlers = [InterceptHandler(level=self.logging_level)] + + logger.configure(handlers=[{"sink": sys.stderr, "level": self.logging_level}]) diff --git a/python/capital/app/core/settings/base.py b/python/capital/app/core/settings/base.py new file mode 100644 index 000000000..0397cbb98 --- /dev/null +++ b/python/capital/app/core/settings/base.py @@ -0,0 +1,16 @@ +from enum import Enum + +from pydantic import BaseSettings + + +class AppEnvTypes(Enum): + prod: str = "prod" + dev: str = "dev" + test: str = "test" + + +class BaseAppSettings(BaseSettings): + app_env: AppEnvTypes = AppEnvTypes.prod + + class Config: + env_file = ".env" diff --git a/python/capital/app/core/settings/development.py b/python/capital/app/core/settings/development.py new file mode 100644 index 000000000..041a77d27 --- /dev/null +++ b/python/capital/app/core/settings/development.py @@ -0,0 +1,14 @@ +import logging + +from app.core.settings.app import AppSettings + + +class DevAppSettings(AppSettings): + debug: bool = True + + title: str = "Dev FastAPI example application" + + logging_level: int = logging.DEBUG + + class Config(AppSettings.Config): + env_file = ".env" diff --git a/python/capital/app/core/settings/production.py b/python/capital/app/core/settings/production.py new file mode 100644 index 000000000..f2d3eab64 --- /dev/null +++ b/python/capital/app/core/settings/production.py @@ -0,0 +1,6 @@ +from app.core.settings.app import AppSettings + + +class ProdAppSettings(AppSettings): + class Config(AppSettings.Config): + env_file = "prod.env" diff --git a/python/capital/app/core/settings/test.py b/python/capital/app/core/settings/test.py new file mode 100644 index 000000000..bea3c53b8 --- /dev/null +++ b/python/capital/app/core/settings/test.py @@ -0,0 +1,19 @@ +import logging + +from pydantic import PostgresDsn, SecretStr + +from app.core.settings.app import AppSettings + + +class TestAppSettings(AppSettings): + debug: bool = True + + title: str = "Test FastAPI example application" + + secret_key: SecretStr = SecretStr("test_secret") + + database_url: PostgresDsn + max_connection_count: int = 5 + min_connection_count: int = 5 + + logging_level: int = logging.DEBUG diff --git a/python/capital/app/db/__init__.py b/python/capital/app/db/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/capital/app/db/errors.py b/python/capital/app/db/errors.py new file mode 100644 index 000000000..bb3ef6690 --- /dev/null +++ b/python/capital/app/db/errors.py @@ -0,0 +1,2 @@ +class EntityDoesNotExist(Exception): + """Raised when entity was not found in database.""" diff --git a/python/capital/app/db/events.py b/python/capital/app/db/events.py new file mode 100644 index 000000000..5fa7f73b0 --- /dev/null +++ b/python/capital/app/db/events.py @@ -0,0 +1,25 @@ +import asyncpg +from fastapi import FastAPI +from loguru import logger + +from app.core.settings.app import AppSettings + + +async def connect_to_db(app: FastAPI, settings: AppSettings) -> None: + logger.info("Connecting to PostgreSQL") + + app.state.pool = await asyncpg.create_pool( + str(settings.database_url), + min_size=settings.min_connection_count, + max_size=settings.max_connection_count, + ) + + logger.info("Connection established") + + +async def close_db_connection(app: FastAPI) -> None: + logger.info("Closing connection to database") + + await app.state.pool.close() + + logger.info("Connection closed") diff --git a/python/capital/app/db/migrations/env.py b/python/capital/app/db/migrations/env.py new file mode 100644 index 000000000..f3129f35f --- /dev/null +++ b/python/capital/app/db/migrations/env.py @@ -0,0 +1,38 @@ +import pathlib +import sys +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import engine_from_config, pool + +sys.path.append(str(pathlib.Path(__file__).resolve().parents[3])) + +from app.core.config import get_app_settings # isort:skip + +SETTINGS = get_app_settings() +DATABASE_URL = SETTINGS.database_url + +config = context.config + +fileConfig(config.config_file_name) # type: ignore + +target_metadata = None + +config.set_main_option("sqlalchemy.url", str(DATABASE_URL)) + + +def run_migrations_online() -> None: + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +run_migrations_online() diff --git a/python/capital/app/db/migrations/script.py.mako b/python/capital/app/db/migrations/script.py.mako new file mode 100644 index 000000000..3217cf0fb --- /dev/null +++ b/python/capital/app/db/migrations/script.py.mako @@ -0,0 +1,23 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/python/capital/app/db/migrations/versions/fdf8821871d7.py b/python/capital/app/db/migrations/versions/fdf8821871d7.py new file mode 100644 index 000000000..8005ef7a4 --- /dev/null +++ b/python/capital/app/db/migrations/versions/fdf8821871d7.py @@ -0,0 +1,326 @@ +"""main tables + +Revision ID: fdf8821871d7 +Revises: +Create Date: 2019-09-22 01:36:44.791880 + +""" +from typing import Tuple + +import sqlalchemy as sa +from alembic import op +from sqlalchemy import func +from app.services import security + +revision = "fdf8821871d7" +down_revision = None +branch_labels = None +depends_on = None + +def create_updated_at_trigger() -> None: + op.execute( + """ + CREATE FUNCTION update_updated_at_column() + RETURNS TRIGGER AS + $$ + BEGIN + NEW.updated_at = now(); + RETURN NEW; + END; + $$ language 'plpgsql'; + """ + ) + + +def timestamps() -> Tuple[sa.Column, sa.Column]: + return ( + sa.Column( + "created_at", + sa.TIMESTAMP(timezone=True), + nullable=False, + server_default=func.now(), + ), + sa.Column( + "updated_at", + sa.TIMESTAMP(timezone=True), + nullable=False, + server_default=func.now(), + onupdate=func.current_timestamp(), + ), + ) + + +def create_users_table() -> None: + op.create_table( + "users", + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("username", sa.Text, unique=True, nullable=False, index=True), + sa.Column("email", sa.Text, unique=True, nullable=False, index=True), + sa.Column("salt", sa.Text, nullable=False), + sa.Column("hashed_password", sa.Text), + sa.Column("bio", sa.Text, nullable=False, server_default=""), + sa.Column("image", sa.Text), + sa.Column("admin", sa.Boolean, nullable=False, default=False), + *timestamps(), + ) + op.execute( + """ + CREATE TRIGGER update_user_modtime + BEFORE UPDATE + ON users + FOR EACH ROW + EXECUTE PROCEDURE update_updated_at_column(); + """ + ) + + +def create_followers_to_followings_table() -> None: + op.create_table( + "followers_to_followings", + sa.Column( + "follower_id", + sa.Integer, + sa.ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column( + "following_id", + sa.Integer, + sa.ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + ), + ) + op.create_primary_key( + "pk_followers_to_followings", + "followers_to_followings", + ["follower_id", "following_id"], + ) + + +def create_articles_table() -> None: + op.create_table( + "articles", + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("slug", sa.Text, unique=True, nullable=False, index=True), + sa.Column("title", sa.Text, nullable=False), + sa.Column("description", sa.Text, nullable=False), + sa.Column("body", sa.Text, nullable=False), + sa.Column( + "author_id", sa.Integer, sa.ForeignKey("users.id", ondelete="SET NULL") + ), + *timestamps(), + ) + op.execute( + """ + CREATE TRIGGER update_article_modtime + BEFORE UPDATE + ON articles + FOR EACH ROW + EXECUTE PROCEDURE update_updated_at_column(); + """ + ) + + +def create_tags_table() -> None: + op.create_table("tags", sa.Column("tag", sa.Text, primary_key=True)) + + +def create_articles_to_tags_table() -> None: + op.create_table( + "articles_to_tags", + sa.Column( + "article_id", + sa.Integer, + sa.ForeignKey("articles.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column( + "tag", + sa.Text, + sa.ForeignKey("tags.tag", ondelete="CASCADE"), + nullable=False, + ), + ) + op.create_primary_key( + "pk_articles_to_tags", "articles_to_tags", ["article_id", "tag"] + ) + + +def create_favorites_table() -> None: + op.create_table( + "favorites", + sa.Column( + "user_id", + sa.Integer, + sa.ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column( + "article_id", + sa.Integer, + sa.ForeignKey("articles.id", ondelete="CASCADE"), + nullable=False, + ), + ) + op.create_primary_key("pk_favorites", "favorites", ["user_id", "article_id"]) + + +def create_commentaries_table() -> None: + op.create_table( + "commentaries", + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("body", sa.Text, nullable=False), + sa.Column( + "author_id", + sa.Integer, + sa.ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column( + "article_id", + sa.Integer, + sa.ForeignKey("articles.id", ondelete="CASCADE"), + nullable=False, + ), + *timestamps(), + ) + op.execute( + """ + CREATE TRIGGER update_comment_modtime + BEFORE UPDATE + ON commentaries + FOR EACH ROW + EXECUTE PROCEDURE update_updated_at_column(); + """ + ) + + +def add_tags(tags, article_id): + connection = op.get_bind() + for tag in tags: + exists = False + res = connection.execute( + f""" + SELECT * from tags + WHERE tag = '{tag}' + + """) + for row in res: + if row['tag']: + exists = True + if not exists: + op.execute( + f""" + INSERT INTO tags(tag) + VALUES('{tag}') + """) + op.execute( + f""" + INSERT INTO articles_to_tags(article_id, tag) + VALUES('{article_id}', '{tag}') + """) + + +def create_new_user(username, email, password, admin=False, image="") -> None: + salt = security.generate_salt() + hashed_password = security.get_password_hash(salt + password) + + op.execute( + f""" + INSERT INTO users(username, email, salt, hashed_password, admin, image) + VALUES('{username}','{email}', '{salt}', '{hashed_password}','{admin}', '{image}') + """) + + +def create_new_article(slug, title, description, body, author_id, tags=[]) -> None: + connection = op.get_bind() + res = connection.execute( + f""" + INSERT INTO articles (slug, title, description, body, author_id) + VALUES ('{slug}', '{title}', '{description}', '{body}', '{author_id}') + RETURNING id + """) + for row in res: + if tags: + add_tags(tags, row['id']) + + +def create_new_comment(body, author_id, article_id) -> None: + op.execute( + f""" + INSERT INTO commentaries (body, author_id, article_id) + VALUES ('{body}', '{author_id}', '{article_id}') + """) + + +def upgrade() -> None: + create_updated_at_trigger() + create_users_table() + create_followers_to_followings_table() + create_articles_table() + create_tags_table() + create_articles_to_tags_table() + create_favorites_table() + create_commentaries_table() + create_new_user(username="Pikachu", email="Pikachu@checkmarx.com", password="snorlax", image="https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/30e23551-c869-487d-a6d4-4aa73c102731/d553a9m-94352d40-78c7-433f-b867-801e0a04f563.jpg/v1/fill/w_900,h_900,q_75,strp/pikachu_wallpaper_by_moustachegirl05_d553a9m-fullview.jpg?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNhNWYwZDQxNWVhMGQyNmUwIiwiaXNzIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCIsIm9iaiI6W1t7ImhlaWdodCI6Ijw9OTAwIiwicGF0aCI6IlwvZlwvMzBlMjM1NTEtYzg2OS00ODdkLWE2ZDQtNGFhNzNjMTAyNzMxXC9kNTUzYTltLTk0MzUyZDQwLTc4YzctNDMzZi1iODY3LTgwMWUwYTA0ZjU2My5qcGciLCJ3aWR0aCI6Ijw9OTAwIn1dXSwiYXVkIjpbInVybjpzZXJ2aWNlOmltYWdlLm9wZXJhdGlvbnMiXX0.MLpVI785Bl0H8UKlTJeqil4l2Dr2lERfgkYCrotQ4Yg") + create_new_user(username="Bob_the_dev", email="bob_dev@checkmarx.com", password="IamDev", image="https://res.cloudinary.com/practicaldev/image/fetch/s--h93cj2BI--/c_fill,f_auto,fl_progressive,h_320,q_auto,w_320/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/336281/766eff39-964e-4acc-a390-bc0e2bc9d459.jpg") + create_new_article(slug="Dev_updates_1", title="Dev updates #1", + description="First update after launch", + body="1. Updating the typings in ts\n2. Integrating the new Redis db for caching\n3. Updating the main docker image version\n4. Changing the API functions to async IO", + author_id="2", + tags = ["dev", "updates"]) + create_new_article(slug="Dev_updates_2", title="Dev updates #2", + description="Improvments and bug fixes", + body="1. Fixed the UI bug after uploading an article\n2. Updated redis versions\n3. Improvments in the enviorment for speed\n4. Updated dependencies\n5. Removed the notification feature", + author_id="2", + tags=["dev", "updates"]) + create_new_user(username="Hodor", email="holdthedoor@checkmarx.com", password="NoSecIssues", image="https://pyxis.nymag.com/v1/imgs/9bc/c6e/b9ba697b64de36b21e4d2dfb1755b20bbb-23-got-ep-5-002.rsquare.w700.jpg") + create_new_article(slug="Dev_updates_3", title="Dev updates #3", + description="Security push", + body="1. Updated 6 packages with high sevierity vulnerabilities\n2. Fixed the stored XSS via the tag input", + author_id="3", + tags=["security", "dev", "updates"]) + create_new_article(slug="Dev_updates_4", title="Dev updates #4", + description="Un secured endpoints", + body="Unfortunately, we didnt have time to fix all issues..\nThere are few endpoints which are open to the world while they should have been restricted\nIm afaraid that some of them might be very sensitive ones like devops or administrative endpoints", + author_id="3", + tags=["security", "dev", "updates"]) + create_new_article(slug="I am Pikachu!", title="I am Pikachu!", description="I am the only Pikachu here, you cant have it!", + body="There is only one Pikachu! you can be Balbazur if you want.. contact me at Pikachu@checkmarx.com", author_id="1", + tags=["pokemon"]) + create_new_article(slug="My favourite pokemon!", title="My favourites pokemon!", + description="You will never guess what are my favourite pokemons!", + body="flygon\nluxray\ngarchomp\ngyarados\nabsol\nninetales\ntorterra\nkomala\nlurantis\ncharizard\ngengar\narcanine\nbulbasaur\ndragonite\nBlaziken\nsnorlax\nMudkip\nJigglypuff\nninetals\nsquirtle", + author_id="1", + tags=["pokemon"]) + create_new_user(username="Ash Ketchum", email="Ash Ketchum@checkmarx.com", password="Gotta Catch ’Em All", image="https://i.stack.imgur.com/3N48C.png?s=256&g=1") + create_new_user(username="Blastoise", email="Blastoise@checkmarx.com", password="powerfulwater", image="https://www.serebii.net/dungeonrescueteamdx/pokemon/009.png") + create_new_user(username="Dragonite", email="Dragonite@checkmarx.com", password="firebomb", image="https://static.wikia.nocookie.net/pkmnshuffle/images/a/a6/Dragonite.png/revision/latest?cb=20170407191605") + create_new_user(username="Gengar", email="Gengar@checkmarx.com", password="ghostly", image="https://i.pinimg.com/736x/54/2c/7f/542c7f7e89f0deb1186bbf9242ebc3ae.jpg") + create_new_article(slug="Gotta Catch ’Em All!", title="Gotta Catch ’Em All!", + description="My Pokemon Team is faster than light. Surrender now or you’re in for a fight!", + body="Maybe you think I’m a little too brash. But the Master is here! And my name is Ash", + author_id="4", + tags=["pokemon"]) + create_new_article(slug="THIS IS MY AWESOME POST!", title="THIS IS MY AWESOME POST!", + description="Whoever comment first will get 1,000,000$ from Pikachu!", + body="Cmon! Lets see who will be first to comment!", + author_id="5", + tags=["pokemon", "prize"]) + create_new_comment(body="Im the first! Im the first!", author_id="5", article_id="8") + create_new_comment(body="Oh no.. I never have luck with that, I wish I could be the first comment", author_id="2", article_id="8") + + create_new_user(username="TeamR$cket", email="TeamR$cket@checkmarx.com", password="iamsorich", image="data:image/png;base64,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") + create_new_article(slug="TeamR$cket", title="TeamR$cket", description="Money Money", + body="We have so much money, we will win everyone!", author_id="8", + tags=["pokemon", "bitcoin"]) + +def downgrade() -> None: + op.drop_table("commentaries") + op.drop_table("favorites") + op.drop_table("articles_to_tags") + op.drop_table("tags") + op.drop_table("articles") + op.drop_table("followers_to_followings") + op.drop_table("users") + op.execute("DROP FUNCTION update_updated_at_column") diff --git a/python/capital/app/db/queries/__init__.py b/python/capital/app/db/queries/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/capital/app/db/queries/queries.py b/python/capital/app/db/queries/queries.py new file mode 100644 index 000000000..a190595c9 --- /dev/null +++ b/python/capital/app/db/queries/queries.py @@ -0,0 +1,5 @@ +import pathlib + +import aiosql + +queries = aiosql.from_path(pathlib.Path(__file__).parent / "sql", "asyncpg") diff --git a/python/capital/app/db/queries/queries.pyi b/python/capital/app/db/queries/queries.pyi new file mode 100644 index 000000000..d36509958 --- /dev/null +++ b/python/capital/app/db/queries/queries.pyi @@ -0,0 +1,127 @@ +"""Typings for queries generated by aiosql""" + +from typing import Dict, Optional, Sequence + +from asyncpg import Connection, Record + +class TagsQueriesMixin: + async def get_all_tags(self, conn: Connection) -> Record: ... + async def create_new_tags( + self, conn: Connection, tags: Sequence[Dict[str, str]] + ) -> None: ... + +class UsersQueriesMixin: + async def get_user_by_email(self, conn: Connection, *, email: str) -> Record: ... + async def get_user_by_username( + self, conn: Connection, *, username: str + ) -> Record: ... + async def create_new_user( + self, + conn: Connection, + *, + username: str, + email: str, + salt: str, + hashed_password: str, + admin: bool, + ) -> Record: ... + async def update_user_by_username( + self, + conn: Connection, + *, + username: str, + new_username: str, + new_email: str, + new_salt: str, + new_password: str, + new_bio: Optional[str], + new_image: Optional[str], + new_admin: Optional[bool], + ) -> Record: ... + +class ProfilesQueriesMixin: + async def is_user_following_for_another( + self, conn: Connection, *, follower_username: str, following_username: str + ) -> Record: ... + async def subscribe_user_to_another( + self, conn: Connection, *, follower_username: str, following_username: str + ) -> None: ... + async def unsubscribe_user_from_another( + self, conn: Connection, *, follower_username: str, following_username: str + ) -> None: ... + +class CommentsQueriesMixin: + async def get_comments_for_article_by_slug( + self, conn: Connection, *, slug: str + ) -> Record: ... + async def get_comment_by_id_and_slug( + self, conn: Connection, *, comment_id: int, article_slug: str + ) -> Record: ... + async def create_new_comment( + self, conn: Connection, *, body: str, article_slug: str, author_username: str + ) -> Record: ... + async def delete_comment_by_id( + self, conn: Connection, *, comment_id: int, author_username: str + ) -> None: ... + +class ArticlesQueriesMixin: + async def add_article_to_favorites( + self, conn: Connection, *, username: str, slug: str + ) -> None: ... + async def remove_article_from_favorites( + self, conn: Connection, *, username: str, slug: str + ) -> None: ... + async def is_article_in_favorites( + self, conn: Connection, *, username: str, slug: str + ) -> Record: ... + async def get_favorites_count_for_article( + self, conn: Connection, *, slug: str + ) -> Record: ... + async def get_tags_for_article_by_slug( + self, conn: Connection, *, slug: str + ) -> Record: ... + async def get_article_by_slug(self, conn: Connection, *, slug: str) -> Record: ... + async def create_new_article( + self, + conn: Connection, + *, + slug: str, + title: str, + description: str, + body: str, + author_username: str + ) -> Record: ... + async def add_tags_to_article( + self, conn: Connection, tags_slugs: Sequence[Dict[str, str]] + ) -> None: ... + async def update_article( + self, + conn: Connection, + *, + slug: str, + author_username: str, + new_slug: str, + new_title: str, + new_body: str, + new_description: str + ) -> Record: ... + async def delete_article( + self, conn: Connection, *, slug: str, author_username: str + ) -> None: ... + async def get_articles_for_feed( + self, conn: Connection, *, follower_username: str, limit: int, offset: int + ) -> Record: ... + async def get_articles_for_feed_count( + self, conn: Connection, *, username: str + ) -> Record: ... + + +class Queries( + TagsQueriesMixin, + UsersQueriesMixin, + ProfilesQueriesMixin, + CommentsQueriesMixin, + ArticlesQueriesMixin, +): ... + +queries: Queries diff --git a/python/capital/app/db/queries/sql/articles.sql b/python/capital/app/db/queries/sql/articles.sql new file mode 100644 index 000000000..650912cf6 --- /dev/null +++ b/python/capital/app/db/queries/sql/articles.sql @@ -0,0 +1,125 @@ +-- name: add-article-to-favorites! +INSERT INTO favorites (user_id, article_id) +VALUES ((SELECT id FROM users WHERE username = :username), + (SELECT id FROM articles WHERE slug = :slug)) +ON CONFLICT DO NOTHING; + + +-- name: remove-article-from-favorites! +DELETE +FROM favorites +WHERE user_id = (SELECT id FROM users WHERE username = :username) + AND article_id = (SELECT id FROM articles WHERE slug = :slug); + + +-- name: is-article-in-favorites^ +SELECT CASE WHEN count(user_id) > 0 THEN TRUE ELSE FALSE END AS favorited +FROM favorites +WHERE user_id = (SELECT id FROM users WHERE username = :username) + AND article_id = (SELECT id FROM articles WHERE slug = :slug); + + +-- name: get-favorites-count-for-article^ +SELECT count(*) as favorites_count +FROM favorites +WHERE article_id = (SELECT id FROM articles WHERE slug = :slug); + + +-- name: get-tags-for-article-by-slug +SELECT t.tag +FROM tags t + INNER JOIN articles_to_tags att ON + t.tag = att.tag + AND + att.article_id = (SELECT id FROM articles WHERE slug = :slug); + + +-- name: get-article-by-slug^ +SELECT id, + slug, + title, + description, + body, + created_at, + updated_at, + (SELECT username FROM users WHERE id = author_id) AS author_username +FROM articles +WHERE slug = :slug +LIMIT 1; + + +-- name: create-new-article None: + super().__init__("${0}".format(count)) + + +class TypedTable(Table): + __table__ = "" + + def __init__( + self, + name: Optional[str] = None, + schema: Optional[str] = None, + alias: Optional[str] = None, + query_cls: Optional[Query] = None, + ) -> None: + if name is None: + if self.__table__: + name = self.__table__ + else: + name = self.__class__.__name__ + + super().__init__(name, schema, alias, query_cls) + + +class Users(TypedTable): + __table__ = "users" + + id: int + username: str + + +class Articles(TypedTable): + __table__ = "articles" + + id: int + slug: str + title: str + description: str + body: str + author_id: int + created_at: datetime + updated_at: datetime + + +class Tags(TypedTable): + __table__ = "tags" + + tag: str + + +class ArticlesToTags(TypedTable): + __table__ = "articles_to_tags" + + article_id: int + tag: str + + +class Favorites(TypedTable): + __table__ = "favorites" + + article_id: int + user_id: int + + +users = Users() +articles = Articles() +tags = Tags() +articles_to_tags = ArticlesToTags() +favorites = Favorites() diff --git a/python/capital/app/db/repositories/__init__.py b/python/capital/app/db/repositories/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/capital/app/db/repositories/articles.py b/python/capital/app/db/repositories/articles.py new file mode 100644 index 000000000..2ababe7cd --- /dev/null +++ b/python/capital/app/db/repositories/articles.py @@ -0,0 +1,430 @@ +from typing import List, Optional, Sequence, Union + +from asyncpg import Connection, Record +from pypika import Query, functions + +from app.db.errors import EntityDoesNotExist +from app.db.queries.queries import queries +from app.db.queries.tables import ( + Parameter, + articles, + articles_to_tags, + favorites, + tags as tags_table, + users, +) +from app.db.repositories.base import BaseRepository +from app.db.repositories.profiles import ProfilesRepository +from app.db.repositories.tags import TagsRepository +from app.models.domain.articles import Article +from app.models.domain.profiles import Profile +from app.models.domain.users import User +from app.resources.strings import LackOf, DescriptionLackOf + +AUTHOR_USERNAME_ALIAS = "author_username" +SLUG_ALIAS = "slug" + +CAMEL_OR_SNAKE_CASE_TO_WORDS = r"^[a-z\d_\-]+|[A-Z\d_\-][^A-Z\d_\-]*" + + +class ArticlesRepository(BaseRepository): # noqa: WPS214 + def __init__(self, conn: Connection) -> None: + super().__init__(conn) + self._profiles_repo = ProfilesRepository(conn) + self._tags_repo = TagsRepository(conn) + + async def create_article( # noqa: WPS211 + self, + *, + slug: str, + title: str, + description: str, + body: str, + author: User, + tags: Optional[Sequence[str]] = None, + ) -> Article: + async with self.connection.transaction(): + article_row = await queries.create_new_article( + self.connection, + slug=slug, + title=title, + description=description, + body=body, + author_username=author.username, + ) + + if tags: + await self._tags_repo.create_tags_that_dont_exist(tags=tags) + await self._link_article_with_tags(slug=slug, tags=tags) + + return await self._get_article_from_db_record( + article_row=article_row, + slug=slug, + author_username=article_row[AUTHOR_USERNAME_ALIAS], + requested_user=author, + ) + + async def update_article( # noqa: WPS211 + self, + *, + article: Article, + slug: Optional[str] = None, + title: Optional[str] = None, + body: Optional[str] = None, + description: Optional[str] = None, + ) -> Article: + updated_article = article.copy(deep=True) + updated_article.slug = slug or updated_article.slug + updated_article.title = title or article.title + updated_article.body = body or article.body + updated_article.description = description or article.description + + async with self.connection.transaction(): + updated_article.updated_at = await queries.update_article( + self.connection, + slug=article.slug, + author_username=article.author.username, + new_slug=updated_article.slug, + new_title=updated_article.title, + new_body=updated_article.body, + new_description=updated_article.description, + ) + + return updated_article + + async def delete_article(self, *, article: Article) -> None: + async with self.connection.transaction(): + await queries.delete_article( + self.connection, + slug=article.slug, + author_username=article.author.username, + ) + + async def filter_articles( # noqa: WPS211 + self, + *, + tag: Optional[str] = None, + author: Optional[str] = None, + favorited: Optional[str] = None, + limit: int = 10, + offset: int = 0, + requested_user: Optional[User] = None, + ) -> List[Article]: + query_params: List[Union[str, int]] = [] + query_params_count = 0 + + # fmt: off + query = Query.from_( + articles, + ).select( + articles.id, + articles.slug, + articles.title, + articles.description, + articles.body, + articles.created_at, + articles.updated_at, + Query.from_( + users, + ).where( + users.id == articles.author_id, + ).select( + users.username, + ).as_( + AUTHOR_USERNAME_ALIAS, + ), + ) + # fmt: on + + if tag: + query_params.append(tag) + query_params_count += 1 + + # fmt: off + query = query.join( + articles_to_tags, + ).on( + (articles.id == articles_to_tags.article_id) & ( + articles_to_tags.tag == Query.from_( + tags_table, + ).where( + tags_table.tag == Parameter(query_params_count), + ).select( + tags_table.tag, + ) + ), + ) + # fmt: on + + if author: + query_params.append(author) + query_params_count += 1 + + # fmt: off + query = query.join( + users, + ).on( + (articles.author_id == users.id) & ( + users.id == Query.from_( + users, + ).where( + users.username == Parameter(query_params_count), + ).select( + users.id, + ) + ), + ) + # fmt: on + + if favorited: + query_params.append(favorited) + query_params_count += 1 + + # fmt: off + query = query.join( + favorites, + ).on( + (articles.id == favorites.article_id) & ( + favorites.user_id == Query.from_( + users, + ).where( + users.username == Parameter(query_params_count), + ).select( + users.id, + ) + ), + ) + # fmt: on + + query = query.limit(Parameter(query_params_count + 1)).offset( + Parameter(query_params_count + 2), + ) + query_params.extend([limit, offset]) + + if limit >= 100000: + return [Article(**{"slug": "DoS", "title": LackOf(), "description": DescriptionLackOf, "body": DescriptionLackOf, "tags": ["DoS"], "author": Profile(**{"username": "DoS"}), "favorited": True, "favorites_count": 0})] + + articles_rows = await self.connection.fetch(query.get_sql(), *query_params) + + return [ + await self._get_article_from_db_record( + article_row=article_row, + slug=article_row[SLUG_ALIAS], + author_username=article_row[AUTHOR_USERNAME_ALIAS], + requested_user=requested_user, + ) + for article_row in articles_rows + ] + + async def get_articles_count( # noqa: WPS211 + self, + *, + tag: Optional[str] = None, + author: Optional[str] = None, + favorited: Optional[str] = None, + limit: int = 10, + offset: int = 0, + requested_user: Optional[User] = None, + ) -> int: + query_params: List[Union[str, int]] = [] + query_params_count = 0 + + # fmt: off + query = Query.from_( + articles, + ).select( + functions.Count('*') + ) + # fmt: on + + if tag: + query_params.append(tag) + query_params_count += 1 + + # fmt: off + query = query.join( + articles_to_tags, + ).on( + (articles.id == articles_to_tags.article_id) & ( + articles_to_tags.tag == Query.from_( + tags_table, + ).where( + tags_table.tag == Parameter(query_params_count), + ).select( + tags_table.tag, + ) + ), + ) + # fmt: on + + if author: + query_params.append(author) + query_params_count += 1 + + # fmt: off + query = query.join( + users, + ).on( + (articles.author_id == users.id) & ( + users.id == Query.from_( + users, + ).where( + users.username == Parameter(query_params_count), + ).select( + users.id, + ) + ), + ) + # fmt: on + + if favorited: + query_params.append(favorited) + query_params_count += 1 + + # fmt: off + query = query.join( + favorites, + ).on( + (articles.id == favorites.article_id) & ( + favorites.user_id == Query.from_( + users, + ).where( + users.username == Parameter(query_params_count), + ).select( + users.id, + ) + ), + ) + + articles_count = await self.connection.fetch(query.get_sql(), *query_params) + return articles_count[0][0] + + async def get_articles_for_user_feed_count( + self, + user: User + ) -> int: + articles_num = await queries.get_articles_for_feed_count( + self.connection, + username=user.username, + ) + + return articles_num[0][0] + + async def get_articles_for_user_feed( + self, + *, + user: User, + limit: int = 10, + offset: int = 0, + ) -> List[Article]: + articles_rows = await queries.get_articles_for_feed( + self.connection, + follower_username=user.username, + limit=limit, + offset=offset, + ) + + return [ + await self._get_article_from_db_record( + article_row=article_row, + slug=article_row[SLUG_ALIAS], + author_username=article_row[AUTHOR_USERNAME_ALIAS], + requested_user=user, + ) + for article_row in articles_rows + ] + + async def get_article_by_slug( + self, + *, + slug: str, + requested_user: Optional[User] = None, + ) -> Article: + article_row = await queries.get_article_by_slug(self.connection, slug=slug) + if article_row: + return await self._get_article_from_db_record( + article_row=article_row, + slug=article_row[SLUG_ALIAS], + author_username=article_row[AUTHOR_USERNAME_ALIAS], + requested_user=requested_user, + ) + + raise EntityDoesNotExist("article with slug {0} does not exist".format(slug)) + + async def get_tags_for_article_by_slug(self, *, slug: str) -> List[str]: + tag_rows = await queries.get_tags_for_article_by_slug( + self.connection, + slug=slug, + ) + return [row["tag"] for row in tag_rows] + + async def get_favorites_count_for_article_by_slug(self, *, slug: str) -> int: + return ( + await queries.get_favorites_count_for_article(self.connection, slug=slug) + )["favorites_count"] + + async def is_article_favorited_by_user(self, *, slug: str, user: User) -> bool: + return ( + await queries.is_article_in_favorites( + self.connection, + username=user.username, + slug=slug, + ) + )["favorited"] + + async def add_article_into_favorites(self, *, article: Article, user: User) -> None: + await queries.add_article_to_favorites( + self.connection, + username=user.username, + slug=article.slug, + ) + + async def remove_article_from_favorites( + self, + *, + article: Article, + user: User, + ) -> None: + await queries.remove_article_from_favorites( + self.connection, + username=user.username, + slug=article.slug, + ) + + async def _get_article_from_db_record( + self, + *, + article_row: Record, + slug: str, + author_username: str, + requested_user: Optional[User], + ) -> Article: + return Article( + id_=article_row["id"], + slug=slug, + title=article_row["title"], + description=article_row["description"], + body=article_row["body"], + author=await self._profiles_repo.get_profile_by_username( + username=author_username, + requested_user=requested_user, + ), + tags=await self.get_tags_for_article_by_slug(slug=slug), + favorites_count=await self.get_favorites_count_for_article_by_slug( + slug=slug, + ), + favorited=await self.is_article_favorited_by_user( + slug=slug, + user=requested_user, + ) + if requested_user + else False, + created_at=article_row["created_at"], + updated_at=article_row["updated_at"], + ) + + async def _link_article_with_tags(self, *, slug: str, tags: Sequence[str]) -> None: + await queries.add_tags_to_article( + self.connection, + [{SLUG_ALIAS: slug, "tag": tag} for tag in tags], + ) diff --git a/python/capital/app/db/repositories/base.py b/python/capital/app/db/repositories/base.py new file mode 100644 index 000000000..8f8a5c38b --- /dev/null +++ b/python/capital/app/db/repositories/base.py @@ -0,0 +1,10 @@ +from asyncpg.connection import Connection + + +class BaseRepository: + def __init__(self, conn: Connection) -> None: + self._conn = conn + + @property + def connection(self) -> Connection: + return self._conn diff --git a/python/capital/app/db/repositories/comments.py b/python/capital/app/db/repositories/comments.py new file mode 100644 index 000000000..7c21911af --- /dev/null +++ b/python/capital/app/db/repositories/comments.py @@ -0,0 +1,103 @@ +from typing import List, Optional + +from asyncpg import Connection, Record + +from app.db.errors import EntityDoesNotExist +from app.db.queries.queries import queries +from app.db.repositories.base import BaseRepository +from app.db.repositories.profiles import ProfilesRepository +from app.models.domain.articles import Article +from app.models.domain.comments import Comment +from app.models.domain.users import User + + +class CommentsRepository(BaseRepository): + def __init__(self, conn: Connection) -> None: + super().__init__(conn) + self._profiles_repo = ProfilesRepository(conn) + + async def get_comment_by_id( + self, + *, + comment_id: int, + article: Article, + user: Optional[User] = None, + ) -> Comment: + comment_row = await queries.get_comment_by_id_and_slug( + self.connection, + comment_id=comment_id, + article_slug=article.slug, + ) + if comment_row: + return await self._get_comment_from_db_record( + comment_row=comment_row, + author_username=comment_row["author_username"], + requested_user=user, + ) + + raise EntityDoesNotExist( + "comment with id {0} does not exist".format(comment_id), + ) + + async def get_comments_for_article( + self, + *, + article: Article, + user: Optional[User] = None, + ) -> List[Comment]: + comments_rows = await queries.get_comments_for_article_by_slug( + self.connection, + slug=article.slug, + ) + return [ + await self._get_comment_from_db_record( + comment_row=comment_row, + author_username=comment_row["author_username"], + requested_user=user, + ) + for comment_row in comments_rows + ] + + async def create_comment_for_article( + self, + *, + body: str, + article: Article, + user: User, + ) -> Comment: + comment_row = await queries.create_new_comment( + self.connection, + body=body, + article_slug=article.slug, + author_username=user.username, + ) + return await self._get_comment_from_db_record( + comment_row=comment_row, + author_username=comment_row["author_username"], + requested_user=user, + ) + + async def delete_comment(self, *, comment: Comment) -> None: + await queries.delete_comment_by_id( + self.connection, + comment_id=comment.id_, + author_username=comment.author.username, + ) + + async def _get_comment_from_db_record( + self, + *, + comment_row: Record, + author_username: str, + requested_user: Optional[User], + ) -> Comment: + return Comment( + id_=comment_row["id"], + body=comment_row["body"], + author=await self._profiles_repo.get_profile_by_username( + username=author_username, + requested_user=requested_user, + ), + created_at=comment_row["created_at"], + updated_at=comment_row["updated_at"], + ) diff --git a/python/capital/app/db/repositories/profiles.py b/python/capital/app/db/repositories/profiles.py new file mode 100644 index 000000000..99c6a9554 --- /dev/null +++ b/python/capital/app/db/repositories/profiles.py @@ -0,0 +1,74 @@ +from typing import Optional, Union + +from asyncpg import Connection + +from app.db.queries.queries import queries +from app.db.repositories.base import BaseRepository +from app.db.repositories.users import UsersRepository +from app.models.domain.profiles import Profile +from app.models.domain.users import User + +UserLike = Union[User, Profile] + + +class ProfilesRepository(BaseRepository): + def __init__(self, conn: Connection): + super().__init__(conn) + self._users_repo = UsersRepository(conn) + + async def get_profile_by_username( + self, + *, + username: str, + requested_user: Optional[UserLike], + ) -> Profile: + user = await self._users_repo.get_user_by_username(username=username) + + profile = Profile(username=user.username, bio=user.bio, image=user.image, admin=user.admin) + if requested_user: + profile.following = await self.is_user_following_for_another_user( + target_user=user, + requested_user=requested_user, + ) + + return profile + + async def is_user_following_for_another_user( + self, + *, + target_user: UserLike, + requested_user: UserLike, + ) -> bool: + return ( + await queries.is_user_following_for_another( + self.connection, + follower_username=requested_user.username, + following_username=target_user.username, + ) + )["is_following"] + + async def add_user_into_followers( + self, + *, + target_user: UserLike, + requested_user: UserLike, + ) -> None: + async with self.connection.transaction(): + await queries.subscribe_user_to_another( + self.connection, + follower_username=requested_user.username, + following_username=target_user.username, + ) + + async def remove_user_from_followers( + self, + *, + target_user: UserLike, + requested_user: UserLike, + ) -> None: + async with self.connection.transaction(): + await queries.unsubscribe_user_from_another( + self.connection, + follower_username=requested_user.username, + following_username=target_user.username, + ) diff --git a/python/capital/app/db/repositories/tags.py b/python/capital/app/db/repositories/tags.py new file mode 100644 index 000000000..573499227 --- /dev/null +++ b/python/capital/app/db/repositories/tags.py @@ -0,0 +1,13 @@ +from typing import List, Sequence + +from app.db.queries.queries import queries +from app.db.repositories.base import BaseRepository + + +class TagsRepository(BaseRepository): + async def get_all_tags(self) -> List[str]: + tags_row = await queries.get_all_tags(self.connection) + return [tag[0] for tag in tags_row] + + async def create_tags_that_dont_exist(self, *, tags: Sequence[str]) -> None: + await queries.create_new_tags(self.connection, [{"tag": tag} for tag in tags]) diff --git a/python/capital/app/db/repositories/users.py b/python/capital/app/db/repositories/users.py new file mode 100644 index 000000000..e348dec1a --- /dev/null +++ b/python/capital/app/db/repositories/users.py @@ -0,0 +1,93 @@ +from typing import Optional + +from app.db.errors import EntityDoesNotExist +from app.db.queries.queries import queries +from app.db.repositories.base import BaseRepository +from app.models.domain.users import User, UserInDB +from app.resources.strings import MassAssignment, DescriptionMassAssignment + + +class UsersRepository(BaseRepository): + async def get_user_by_email(self, *, email: str) -> UserInDB: + user_row = await queries.get_user_by_email(self.connection, email=email) + if user_row: + return UserInDB(**user_row) + + raise EntityDoesNotExist("user with email {0} does not exist".format(email)) + + async def get_user_by_username(self, *, username: str) -> UserInDB: + user_row = await queries.get_user_by_username( + self.connection, + username=username, + ) + if user_row: + return UserInDB(**user_row) + + raise EntityDoesNotExist( + "user with username {0} does not exist".format(username), + ) + + async def create_user( + self, + *, + username: str, + email: str, + password: str, + ) -> UserInDB: + user = UserInDB(username=username, email=email) + user.change_password(password) + + async with self.connection.transaction(): + user_row = await queries.create_new_user( + self.connection, + username=user.username, + email=user.email, + salt=user.salt, + hashed_password=user.hashed_password, + admin=user.admin + ) + + return user.copy(update=dict(user_row)) + + async def update_user( # noqa: WPS211 + self, + *, + user: User, + username: Optional[str] = None, + email: Optional[str] = None, + password: Optional[str] = None, + bio: Optional[str] = None, + image: Optional[str] = None, + admin: Optional[bool] = None + ) -> UserInDB: + user_in_db = await self.get_user_by_username(username=user.username) + + user_in_db.username = username or user_in_db.username + user_in_db.email = email or user_in_db.email + user_in_db.bio = bio or user_in_db.bio + user_in_db.image = image or user_in_db.image + if admin == True: + user_in_db.bio = MassAssignment() + "\n\n" + "Description: " + DescriptionMassAssignment + if user_in_db.admin == None: + admin = False + if admin != None: + user_in_db.admin = admin + else: + user_in_db.admin = admin or user_in_db.admin + if password: + user_in_db.change_password(password) + + async with self.connection.transaction(): + user_in_db.updated_at = await queries.update_user_by_username( + self.connection, + username=user.username, + new_username=user_in_db.username, + new_email=user_in_db.email, + new_salt=user_in_db.salt, + new_password=user_in_db.hashed_password, + new_bio=user_in_db.bio, + new_image=user_in_db.image, + new_admin=user_in_db.admin, + ) + + return user_in_db diff --git a/python/capital/app/main.py b/python/capital/app/main.py new file mode 100644 index 000000000..6259b4612 --- /dev/null +++ b/python/capital/app/main.py @@ -0,0 +1,43 @@ +import uvicorn +from fastapi import FastAPI +from fastapi.exceptions import RequestValidationError +from starlette.exceptions import HTTPException +from starlette.middleware.cors import CORSMiddleware + +from app.api.errors.http_error import http_error_handler +from app.api.errors.validation_error import http422_error_handler +from app.api.routes.api import router as api_router +from app.core.config import get_app_settings +from app.core.events import create_start_app_handler, create_stop_app_handler + + +def get_application() -> FastAPI: + settings = get_app_settings() + + settings.configure_logging() + + application = FastAPI(**settings.fastapi_kwargs) + + application.add_middleware( + CORSMiddleware, + allow_origins=settings.allowed_hosts, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + application.add_event_handler( + "startup", + create_start_app_handler(application, settings), + ) + application.add_event_handler( + "shutdown", + create_stop_app_handler(application), + ) + + application.add_exception_handler(HTTPException, http_error_handler) + application.add_exception_handler(RequestValidationError, http422_error_handler) + + application.include_router(api_router, prefix=settings.api_prefix) + + return application diff --git a/python/capital/app/models/__init__.py b/python/capital/app/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/capital/app/models/common.py b/python/capital/app/models/common.py new file mode 100644 index 000000000..fdc515bef --- /dev/null +++ b/python/capital/app/models/common.py @@ -0,0 +1,19 @@ +import datetime + +from pydantic import BaseModel, Field, validator + + +class DateTimeModelMixin(BaseModel): + created_at: datetime.datetime = None # type: ignore + updated_at: datetime.datetime = None # type: ignore + + @validator("created_at", "updated_at", pre=True) + def default_datetime( + cls, # noqa: N805 + value: datetime.datetime, # noqa: WPS110 + ) -> datetime.datetime: + return value or datetime.datetime.now() + + +class IDModelMixin(BaseModel): + id_: int = Field(0, alias="id") diff --git a/python/capital/app/models/domain/__init__.py b/python/capital/app/models/domain/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/capital/app/models/domain/articles.py b/python/capital/app/models/domain/articles.py new file mode 100644 index 000000000..0c032bdb1 --- /dev/null +++ b/python/capital/app/models/domain/articles.py @@ -0,0 +1,16 @@ +from typing import List + +from app.models.common import DateTimeModelMixin, IDModelMixin +from app.models.domain.profiles import Profile +from app.models.domain.rwmodel import RWModel + + +class Article(IDModelMixin, DateTimeModelMixin, RWModel): + slug: str + title: str + description: str + body: str + tags: List[str] + author: Profile + favorited: bool + favorites_count: int diff --git a/python/capital/app/models/domain/comments.py b/python/capital/app/models/domain/comments.py new file mode 100644 index 000000000..ce5fc6af4 --- /dev/null +++ b/python/capital/app/models/domain/comments.py @@ -0,0 +1,8 @@ +from app.models.common import DateTimeModelMixin, IDModelMixin +from app.models.domain.profiles import Profile +from app.models.domain.rwmodel import RWModel + + +class Comment(IDModelMixin, DateTimeModelMixin, RWModel): + body: str + author: Profile diff --git a/python/capital/app/models/domain/profiles.py b/python/capital/app/models/domain/profiles.py new file mode 100644 index 000000000..26d5dd490 --- /dev/null +++ b/python/capital/app/models/domain/profiles.py @@ -0,0 +1,15 @@ +from typing import Optional + +from app.models.domain.rwmodel import RWModel + + +class Profile(RWModel): + admin: bool = False + username: str + bio: str = "" + image: Optional[str] = None + following: bool = False + card_name: Optional[str] + card_number: Optional[str] + card_cvc: Optional[str] + card_expiry: Optional[str] diff --git a/python/capital/app/models/domain/rwmodel.py b/python/capital/app/models/domain/rwmodel.py new file mode 100644 index 000000000..1c34f3b0b --- /dev/null +++ b/python/capital/app/models/domain/rwmodel.py @@ -0,0 +1,21 @@ +import datetime + +from pydantic import BaseConfig, BaseModel + + +def convert_datetime_to_realworld(dt: datetime.datetime) -> str: + return dt.replace(tzinfo=datetime.timezone.utc).isoformat().replace("+00:00", "Z") + + +def convert_field_to_camel_case(string: str) -> str: + return "".join( + word if index == 0 else word.capitalize() + for index, word in enumerate(string.split("_")) + ) + + +class RWModel(BaseModel): + class Config(BaseConfig): + allow_population_by_field_name = True + json_encoders = {datetime.datetime: convert_datetime_to_realworld} + alias_generator = convert_field_to_camel_case diff --git a/python/capital/app/models/domain/users.py b/python/capital/app/models/domain/users.py new file mode 100644 index 000000000..6f6214e65 --- /dev/null +++ b/python/capital/app/models/domain/users.py @@ -0,0 +1,26 @@ +from typing import Optional + +from app.models.common import DateTimeModelMixin, IDModelMixin +from app.models.domain.rwmodel import RWModel +from app.services import security + + +class User(RWModel): + username: str + email: str + bio: str = "" + image: Optional[str] = None + admin: bool = False + + + +class UserInDB(IDModelMixin, DateTimeModelMixin, User): + salt: str = "" + hashed_password: str = "" + + def check_password(self, password: str) -> bool: + return security.verify_password(self.salt + password, self.hashed_password) + + def change_password(self, password: str) -> None: + self.salt = security.generate_salt() + self.hashed_password = security.get_password_hash(self.salt + password) diff --git a/python/capital/app/models/schemas/__init__.py b/python/capital/app/models/schemas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/capital/app/models/schemas/articles.py b/python/capital/app/models/schemas/articles.py new file mode 100644 index 000000000..79a6f49c1 --- /dev/null +++ b/python/capital/app/models/schemas/articles.py @@ -0,0 +1,43 @@ +from typing import List, Optional + +from pydantic import BaseModel, Field + +from app.models.domain.articles import Article +from app.models.schemas.rwschema import RWSchema + +DEFAULT_ARTICLES_LIMIT = 10 +DEFAULT_ARTICLES_OFFSET = 0 + + +class ArticleForResponse(RWSchema, Article): + tags: List[str] = Field(..., alias="tagList") + + +class ArticleInResponse(RWSchema): + article: ArticleForResponse + + +class ArticleInCreate(RWSchema): + title: str + description: str + body: str + tags: List[str] = Field([], alias="tagList") + + +class ArticleInUpdate(RWSchema): + title: Optional[str] = None + description: Optional[str] = None + body: Optional[str] = None + + +class ListOfArticlesInResponse(RWSchema): + articles: List[ArticleForResponse] + articles_count: int + + +class ArticlesFilters(BaseModel): + tag: Optional[str] = None + author: Optional[str] = None + favorited: Optional[str] = None + limit: int = Field(DEFAULT_ARTICLES_LIMIT, ge=1) + offset: int = Field(DEFAULT_ARTICLES_OFFSET, ge=0) diff --git a/python/capital/app/models/schemas/comments.py b/python/capital/app/models/schemas/comments.py new file mode 100644 index 000000000..e23069702 --- /dev/null +++ b/python/capital/app/models/schemas/comments.py @@ -0,0 +1,16 @@ +from typing import List + +from app.models.domain.comments import Comment +from app.models.schemas.rwschema import RWSchema + + +class ListOfCommentsInResponse(RWSchema): + comments: List[Comment] + + +class CommentInResponse(RWSchema): + comment: Comment + + +class CommentInCreate(RWSchema): + body: str diff --git a/python/capital/app/models/schemas/debug.py b/python/capital/app/models/schemas/debug.py new file mode 100644 index 000000000..f5264a902 --- /dev/null +++ b/python/capital/app/models/schemas/debug.py @@ -0,0 +1,14 @@ +from typing import Optional + +from app.models.schemas.rwschema import RWSchema + + +class ExecutionInResponse(RWSchema): + stdout: str + +class FlagInResponse(ExecutionInResponse): + flag: str + description: str + +class DoExecution(RWSchema): + command: str diff --git a/python/capital/app/models/schemas/jwt.py b/python/capital/app/models/schemas/jwt.py new file mode 100644 index 000000000..56d1fa30d --- /dev/null +++ b/python/capital/app/models/schemas/jwt.py @@ -0,0 +1,12 @@ +from datetime import datetime + +from pydantic import BaseModel + + +class JWTMeta(BaseModel): + exp: datetime + sub: str + + +class JWTUser(BaseModel): + username: str diff --git a/python/capital/app/models/schemas/profiles.py b/python/capital/app/models/schemas/profiles.py new file mode 100644 index 000000000..5662dfcf6 --- /dev/null +++ b/python/capital/app/models/schemas/profiles.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel + +from app.models.domain.profiles import Profile + + +class ProfileInResponse(BaseModel): + profile: Profile diff --git a/python/capital/app/models/schemas/rwschema.py b/python/capital/app/models/schemas/rwschema.py new file mode 100644 index 000000000..018ad4be2 --- /dev/null +++ b/python/capital/app/models/schemas/rwschema.py @@ -0,0 +1,6 @@ +from app.models.domain.rwmodel import RWModel + + +class RWSchema(RWModel): + class Config(RWModel.Config): + orm_mode = True diff --git a/python/capital/app/models/schemas/tags.py b/python/capital/app/models/schemas/tags.py new file mode 100644 index 000000000..e9655fb58 --- /dev/null +++ b/python/capital/app/models/schemas/tags.py @@ -0,0 +1,7 @@ +from typing import List + +from pydantic import BaseModel + + +class TagsInList(BaseModel): + tags: List[str] diff --git a/python/capital/app/models/schemas/users.py b/python/capital/app/models/schemas/users.py new file mode 100644 index 000000000..b526c3c88 --- /dev/null +++ b/python/capital/app/models/schemas/users.py @@ -0,0 +1,48 @@ +from typing import Optional + +from pydantic import BaseModel, EmailStr, HttpUrl + +from app.models.domain.users import User +from app.models.schemas.rwschema import RWSchema + + +class UserInLogin(RWSchema): + email: EmailStr + password: str + + +class UserInCreate(UserInLogin): + username: str + + +class UserInUpdate(BaseModel): + username: Optional[str] = None + email: Optional[EmailStr] = None + password: Optional[str] = None + bio: Optional[str] = None + image: Optional[HttpUrl] = None + admin: Optional[bool] = None + + +class UserWithToken(User): + token: str + + +class UserInResponse(RWSchema): + user: UserWithToken + + +class CTFResponse(UserInResponse): + flag: str + description: str + + +class OnlyCTFResponse(BaseModel): + flag: str + description: str + + +class OnlyCTFResponseWithSecret(BaseModel): + flag: str + description: str + secret: str diff --git a/python/capital/app/resources/__init__.py b/python/capital/app/resources/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/capital/app/resources/strings.py b/python/capital/app/resources/strings.py new file mode 100644 index 000000000..c15fa3c25 --- /dev/null +++ b/python/capital/app/resources/strings.py @@ -0,0 +1,123 @@ +# API messages + +USER_DOES_NOT_EXIST_ERROR = "user does not exist" +ARTICLE_DOES_NOT_EXIST_ERROR = "article does not exist" +ARTICLE_ALREADY_EXISTS = "article already exists" +ARTICLE_TITLE_IS_NULL = "no title is provided" +USERNAME_IS_NULL = "no username is provided" +PASSWORD_IS_NULL = "no password is provided" +COMMENT_IS_NULL = "no comment is provided" +USER_IS_NOT_AUTHOR_OF_ARTICLE = "you are not an author of this article" + +INCORRECT_LOGIN_INPUT = "incorrect email or password" +USERNAME_TAKEN = "user with this username already exists" +EMAIL_TAKEN = "user with this email already exists" + +UNABLE_TO_FOLLOW_YOURSELF = "user can not follow him self" +UNABLE_TO_UNSUBSCRIBE_FROM_YOURSELF = "user can not unsubscribe from him self" +USER_IS_NOT_FOLLOWED = "you don't follow this user" +USER_IS_ALREADY_FOLLOWED = "you follow this user already" + +WRONG_TOKEN_PREFIX = "unsupported authorization type" # noqa: S105 +MALFORMED_PAYLOAD = "could not validate credentials" + +ARTICLE_IS_ALREADY_FAVORITED = "you are already marked this articles as favorite" +ARTICLE_IS_NOT_FAVORITED = "article is not favorited" + +COMMENT_DOES_NOT_EXIST = "comment does not exist" + +AUTHENTICATION_REQUIRED = "authentication required" + + +# Flags + +def BrokenFunctionLevelAuthorization(): + return "flag{BFL4_I_aM_Th3_aDm1n_H3r3!}" + + +def BrokenUserAuthentication(): + return "flag{br0k3n_uS3r_4uthEnt1cAt1oN}" + + +def BOLA(): + return "flag{B0lA!!!!!}" + + +def ImproperAssetsManagement(): + return "flag{Impr0peR_Ass3ts_ManAg3m3nt}" + + +def Injection(): + return "flag{1nject10n_Ap1}" + + +def ExcessiveDataExposure(): + return "flag{3xc3ss1v3_daTa_Xp0sur3}" + + +def LackOf(): + return "flag{L4cK_0f_R3s0urc3S_&_r4t3_L1m1t1ng}" + + +def MassAssignment(): + return "flag{M4sS_AsS1gnm3nt}" + + +def InsufficientLogging(): + return "flag{InsUfF1C3nT_L0gG1nG}" + + +def SecMiss(): # not in used in app (external flag) + return "flag{5eC_M1sc0nF1g}" + + +# Description +DescriptionInsufficientLogging = "Insufficient logging and monitoring, coupled with missing or ineffective " \ + "integration with incident response, " \ + "allows attackers to further attack systems, maintain persistence, pivot to more " \ + "systems to tamper with, extract, or destroy data. " \ + "Most breach studies demonstrate the time to detect a breach is over 200 days, " \ + "typically detected by external parties rather than internal processes or monitoring." +DescriptionImproperAssetsManagement = "Old API versions are usually unpatched and are an easy way to compromise " \ + "systems without having to fight " \ + "state-of-the-art security mechanisms, which might be in place to protect the " \ + "most recent API versions. " +DescriptionInjection = "Attackers will feed the API with " \ + "malicious data through whatever " \ + "injection vectors are available " \ + "(e.g., direct input, parameters, " \ + "integrated services, etc.), " \ + "expecting it to be sent to an " \ + "interpreter" +DescriptionExcessiveDataExposure = "Exploitation of Excessive Data " \ + "Exposure is simple, and is usually " \ + "performed by sniffing the traffic " \ + "to analyze the API responses, " \ + "looking for sensitive data " \ + "exposure that should not be " \ + "returned to the user." +DescriptionBOLA = "APIs tend to expose endpoints that handle object identifiers, " \ + "creating a wide attack surface Level Access Control issue. Object " \ + "level authorization checks should be considered in every function " \ + "that accesses a data source using an input from the user." +DescriptionBrokenFunctionLevelAuthorization = "Complex access control policies with different hierarchies, " \ + "groups, and roles, and an unclear separation between " \ + "administrative and regular functions, tend to lead to authorization " \ + "flaws. By exploiting these issues, attackers gain access to other " \ + "users’ resources and/or administrative functions." +DescriptionBrokenUserAuthentication = "Authentication mechanisms are often implemented incorrectly, " \ + "allowing attackers to compromise authentication tokens or to " \ + "exploit implementation flaws to assume other user's identities " \ + "temporarily or permanently. Compromising system's ability to " \ + "identify the client/user, compromises API security overall." +DescriptionLackOf = "Quite often, APIs do not impose any restrictions on the size or " \ + "number of resources that can be requested by the client/user. Not " \ + "only can this impact the API server performance, leading to " \ + "Denial of Service (DoS), but also leaves the door open to " \ + "authentication flaws such as brute force." +DescriptionMassAssignment = "Binding client provided data (e.g., JSON) to data models, without " \ + "proper properties filtering based on a whitelist, usually lead to " \ + "Mass Assignment. Either guessing objects properties, exploring " \ + "other API endpoints, reading the documentation, or providing " \ + "additional object properties in request payloads, allows attackers " \ + "to modify object properties they are not supposed to." diff --git a/python/capital/app/services/__init__.py b/python/capital/app/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/capital/app/services/articles.py b/python/capital/app/services/articles.py new file mode 100644 index 000000000..02dff6916 --- /dev/null +++ b/python/capital/app/services/articles.py @@ -0,0 +1,23 @@ +from slugify import slugify + +from app.db.errors import EntityDoesNotExist +from app.db.repositories.articles import ArticlesRepository +from app.models.domain.articles import Article +from app.models.domain.users import User + + +async def check_article_exists(articles_repo: ArticlesRepository, slug: str) -> bool: + try: + await articles_repo.get_article_by_slug(slug=slug) + except EntityDoesNotExist: + return False + + return True + + +def get_slug_for_article(title: str) -> str: + return slugify(title) + + +def check_user_can_modify_article(article: Article, user: User) -> bool: + return article.author.username == user.username diff --git a/python/capital/app/services/authentication.py b/python/capital/app/services/authentication.py new file mode 100644 index 000000000..84539a6dc --- /dev/null +++ b/python/capital/app/services/authentication.py @@ -0,0 +1,20 @@ +from app.db.errors import EntityDoesNotExist +from app.db.repositories.users import UsersRepository + + +async def check_username_is_taken(repo: UsersRepository, username: str) -> bool: + try: + await repo.get_user_by_username(username=username) + except EntityDoesNotExist: + return False + + return True + + +async def check_email_is_taken(repo: UsersRepository, email: str) -> bool: + try: + await repo.get_user_by_email(email=email) + except EntityDoesNotExist: + return False + + return True diff --git a/python/capital/app/services/comments.py b/python/capital/app/services/comments.py new file mode 100644 index 000000000..33f220e68 --- /dev/null +++ b/python/capital/app/services/comments.py @@ -0,0 +1,6 @@ +from app.models.domain.comments import Comment +from app.models.domain.users import User + + +def check_user_can_modify_comment(comment: Comment, user: User) -> bool: + return comment.author.username == user.username diff --git a/python/capital/app/services/jwt.py b/python/capital/app/services/jwt.py new file mode 100644 index 000000000..355ecea35 --- /dev/null +++ b/python/capital/app/services/jwt.py @@ -0,0 +1,41 @@ +from datetime import datetime, timedelta +from typing import Dict + +import jwt +from pydantic import ValidationError + +from app.models.domain.users import User +from app.models.schemas.jwt import JWTMeta, JWTUser + +JWT_SUBJECT = "access" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # one week + + +def create_jwt_token( + *, + jwt_content: Dict[str, str], + secret_key: str, + expires_delta: timedelta, +) -> str: + to_encode = jwt_content.copy() + expire = datetime.utcnow() + expires_delta + to_encode.update(JWTMeta(exp=expire, sub=JWT_SUBJECT).dict()) + return jwt.encode(to_encode, secret_key, algorithm=ALGORITHM) + + +def create_access_token_for_user(user: User, secret_key: str) -> str: + return create_jwt_token( + jwt_content=JWTUser(username=user.username).dict(), + secret_key=secret_key, + expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES), + ) + + +def get_username_from_token(token: str, secret_key: str) -> str: + try: + return JWTUser(**jwt.decode(token, secret_key, algorithms=[ALGORITHM])).username + except jwt.PyJWTError as decode_error: + raise ValueError("unable to decode JWT token") from decode_error + except ValidationError as validation_error: + raise ValueError("malformed payload in token") from validation_error diff --git a/python/capital/app/services/security.py b/python/capital/app/services/security.py new file mode 100644 index 000000000..08c523b3d --- /dev/null +++ b/python/capital/app/services/security.py @@ -0,0 +1,16 @@ +import bcrypt +from passlib.context import CryptContext + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def generate_salt() -> str: + return bcrypt.gensalt().decode() + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) diff --git a/python/capital/docker-compose.yml b/python/capital/docker-compose.yml new file mode 100644 index 000000000..baadab8b0 --- /dev/null +++ b/python/capital/docker-compose.yml @@ -0,0 +1,33 @@ +version: '3' +services: + backend: + image: capitalapp/backend:latest + restart: on-failure + ports: + - "8000:8000" + environment: + DATABASE_URL: "postgresql://postgres:postgres@db/postgres" + APP_ENV: "prod" + depends_on: + db: + condition: service_healthy + db: + image: postgres:11.5-alpine + expose: + - "5432" + environment: + DATABASE_URL: "postgresql://postgres:postgres@localhost/postgres" + healthcheck: + test: ["CMD", "psql", "--user=postgres"] + interval: 10s + timeout: 10s + retries: 20 + start_period: 30s + redis: + image: capitalapp/redis:latest + ports: + - "6379:6379" + frontend: + image: capitalapp/frontend:latest + ports: + - "4100:4100" diff --git a/python/capital/frontend/LICENSE.md b/python/capital/frontend/LICENSE.md new file mode 100644 index 000000000..006edc881 --- /dev/null +++ b/python/capital/frontend/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 GoThinkster + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/python/capital/frontend/README.md b/python/capital/frontend/README.md new file mode 100644 index 000000000..b36ffce5b --- /dev/null +++ b/python/capital/frontend/README.md @@ -0,0 +1,58 @@ +

+ +

+ + +## Getting started + +To get the frontend running locally: + +- Clone this repo +- `npm install` to install all req'd dependencies +- `npm start` to start the local server + +Local web server will use port 4100 instead of standard React's port 3000 to prevent conflicts with some backends like Node or Rails. You can configure port in scripts section of `package.json`: we use [cross-env](https://github.com/kentcdodds/cross-env) to set environment variable PORT for React scripts, this is Windows-compatible way of setting environment variables. + +Alternatively, you can add `.env` file in the root folder of project to set environment variables (use PORT to change webserver's port). This file will be ignored by git, so it is suitable for API keys and other sensitive stuff. Refer to [dotenv](https://github.com/motdotla/dotenv) and [React](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-development-environment-variables-in-env) documentation for more details. Also, please remove setting variable via script section of `package.json` - `dotenv` never override variables if they are already set. + +### Making requests to the backend API + +Configure the REACT_APP_BACKEND_URL environment variable pointing to the backend local server's URL: + +`export REACT_APP_BACKEND_URL="http://localhost:8000/api"` + + +## Functionality overview + +The example application is a social blogging site (i.e. a Medium.com clone) called "c{api}tal". It uses a custom API for all requests, including authentication. + +**General functionality:** + +- Authenticate users via JWT (login/signup pages + logout button on settings page) +- CRU* users (sign up & settings page - no deleting required) +- CRUD Articles +- CR*D Comments on articles (no updating required) +- GET and display paginated lists of articles +- Favorite articles +- Follow other users + +**The general page breakdown looks like this:** + +- Home page (URL: /#/ ) + - List of tags + - List of articles pulled from either Feed, Global, or by Tag + - Pagination for list of articles +- Sign in/Sign up pages (URL: /#/login, /#/register ) + - Use JWT (store the token in localStorage) +- Settings page (URL: /#/settings ) +- Editor page to create/edit articles (URL: /#/editor, /#/editor/article-slug-here ) +- Article page (URL: /#/article/article-slug-here ) + - Delete article button (only shown to article's author) + - Render markdown from server client side + - Comments section at bottom of page + - Delete comment button (only shown to comment's author) +- Profile page (URL: /#/@username, /#/@username/favorites ) + - Show basic user info + - List of articles populated from author's created articles or author's favorited articles + +
diff --git a/python/capital/frontend/cypress.json b/python/capital/frontend/cypress.json new file mode 100644 index 000000000..5881fbae4 --- /dev/null +++ b/python/capital/frontend/cypress.json @@ -0,0 +1,9 @@ +{ + "baseUrl": "http://localhost:4100", + "env": { + "apiUrl": "https://conduit.productionready.io/api", + "username": "warren_boyd", + "email": "warren.boyd@mailinator.com", + "password": "Pa$$w0rd!" + } +} diff --git a/python/capital/frontend/cypress/README.md b/python/capital/frontend/cypress/README.md new file mode 100644 index 000000000..53f3068d2 --- /dev/null +++ b/python/capital/frontend/cypress/README.md @@ -0,0 +1,22 @@ +# Cypress.io end-to-end tests 🚀 + +[Cypress.io](https://www.cypress.io) is an open source, MIT licensed end-to-end test runner + +## Folder structure + +These folders hold the end-to-end tests and supporting files for the [Cypress Test Runner](https://github.com/cypress-io/cypress). + +- [fixtures](fixtures) folder holds optional JSON data for mocking, [read more](https://on.cypress.io/fixture) +- [integration](integration) holds the actual test files, [read more](https://on.cypress.io/writing-and-organizing-tests) +- [plugins](plugins) allow you to customize how tests are loaded, [read more](https://on.cypress.io/plugins) +- [support](support) file runs before all tests and is a great place to write or load additional custom commands, [read more](https://on.cypress.io/writing-and-organizing-tests#Support-file) + +## `cypress.json` file + +You can configure project options in the [../cypress.json](../cypress.json) file, see [Cypress configuration doc](https://on.cypress.io/configuration). + +## More information + +- [https://github.com/cypress.io/cypress](https://github.com/cypress.io/cypress) +- [https://docs.cypress.io/](https://docs.cypress.io/) +- [Writing your first Cypress test](https://on.cypress.io/intro) diff --git a/python/capital/frontend/cypress/fixtures/example.json b/python/capital/frontend/cypress/fixtures/example.json new file mode 100644 index 000000000..02e425437 --- /dev/null +++ b/python/capital/frontend/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/python/capital/frontend/cypress/integration/article-spec.js b/python/capital/frontend/cypress/integration/article-spec.js new file mode 100644 index 000000000..8d34556ec --- /dev/null +++ b/python/capital/frontend/cypress/integration/article-spec.js @@ -0,0 +1,177 @@ +// enables intelligent code completion for Cypress commands +// https://on.cypress.io/intelligent-code-completion +/// +/// +import faker from 'faker'; + +describe('Article page', () => { + beforeEach(() => { + cy.intercept('GET', '**/articles?*') + .as('getAllArticles') + .intercept('GET', '**/articles/*/comments') + .as('getCommentsForArticle') + .intercept('GET', '**/articles/*') + .as('getArticle') + .visit('/'); + + cy.wait('@getAllArticles').get('.preview-link').first().click(); + }); + + it('should show the article', () => { + cy.wait('@getArticle') + .its('response.body.article') + .then((article) => { + cy.location('pathname').should('equal', `/article/${article.slug}`); + + cy.findByRole('heading', { name: article.title }).should('be.visible'); + + cy.get('.article-meta').within(() => { + cy.findByRole('img', { name: article.author.username }).should( + 'be.visible' + ); + + cy.findByText(article.author.username).should('be.visible'); + }); + + cy.get('.article-content .col-xs-12') + .children() + .first() + .should('not.be.empty'); + + cy.get('.tag-list') + .children() + .should('have.length', article.tagList.length); + }); + }); + + it('should require to be logged to comment', () => { + cy.findByText(/to add comments on this article/i).should('be.visible'); + }); +}); + +describe('Article page (authenticated)', () => { + const commentPlaceholder = 'Write a comment...'; + const postCommentButton = 'Post Comment'; + + beforeEach(() => { + cy.intercept('GET', '**/articles?*') + .as('getAllArticles') + .intercept('GET', '**/articles/*/comments') + .as('getCommentsForArticle') + .intercept('GET', '**/articles/*') + .as('getArticle') + .intercept('POST', '**/articles/*/comments') + .as('createComment') + .intercept('DELETE', '**/articles/*/comments/*') + .as('deleteComment') + .visit('/') + .login(); + + cy.wait('@getAllArticles').get('.preview-link').first().click(); + }); + + it('should show the comment box', () => { + cy.wait(['@getArticle', '@getCommentsForArticle']); + + cy.get('.comment-form').should('exist'); + }); + + it('should add a new comment', () => { + const comment = faker.lorem.paragraph(); + + cy.wait(['@getArticle', '@getCommentsForArticle']); + + cy.findByPlaceholderText(commentPlaceholder).type(comment); + + cy.findByRole('button', { name: postCommentButton }).click(); + + cy.wait('@createComment').its('response.statusCode').should('equal', 200); + + cy.wait(100) + .get('.card:not(form)') + .first() + .within(() => { + cy.findByText(comment).should('exist'); + }); + }); + + it('should validate the comment box', () => { + cy.wait(['@getArticle', '@getCommentsForArticle']); + + cy.findByRole('button', { name: postCommentButton }).click(); + + cy.wait('@createComment').its('response.statusCode').should('equal', 422); + + cy.get('.error-messages').within(() => { + cy.findAllByRole('listitem').should('have.length', 1); + }); + }); + + it('should remove my own comment', () => { + const comment = faker.lorem.sentence(); + + cy.wait(['@getArticle', '@getCommentsForArticle']); + + cy.findByPlaceholderText(commentPlaceholder).type(comment); + + cy.findByRole('button', { name: postCommentButton }).click(); + + cy.wait('@createComment'); + + cy.findByText(comment) + .as('comment') + .parent() + .parent() + .find('.mod-options i') + .click(); + + cy.wait('@deleteComment').its('response.statusCode').should('equal', 200); + + cy.findByText(comment).should('not.exist'); + }); +}); + +describe('Article page (author)', () => { + const commentPlaceholder = 'Write a comment...'; + const postCommentButton = 'Post Comment'; + + beforeEach(() => { + cy.intercept('GET', '**/articles/*') + .as('getArticle') + .intercept('GET', '**/articles/*/comments') + .as('getCommentsForArticle') + .intercept('POST', '**/articles/*/comments') + .as('createComment') + .intercept('DELETE', '**/articles/*') + .as('deleteArticle'); + + cy.visit('/') + .login() + .createArticle() + .then((article) => { + cy.visit(`/article/${article.slug}`); + + cy.wait(['@getArticle', '@getCommentsForArticle']); + }); + }); + + it('should add a new comment', () => { + const comment = faker.lorem.paragraph(); + + cy.findByPlaceholderText(commentPlaceholder).type(comment); + + cy.findByRole('button', { name: postCommentButton }).click(); + + cy.wait('@createComment').its('response.statusCode').should('equal', 200); + }); + + it('should remove my article', () => { + cy.findByRole('button', { + name: /delete article/i, + }).click(); + + cy.wait('@deleteArticle').its('response.statusCode').should('equal', 200); + + cy.location('pathname').should('equal', '/'); + }); +}); diff --git a/python/capital/frontend/cypress/integration/editor-spec.js b/python/capital/frontend/cypress/integration/editor-spec.js new file mode 100644 index 000000000..0c6a27eff --- /dev/null +++ b/python/capital/frontend/cypress/integration/editor-spec.js @@ -0,0 +1,175 @@ +// enables intelligent code completion for Cypress commands +// https://on.cypress.io/intelligent-code-completion +/// +/// +import faker from 'faker'; + +import { createArticle } from '../../src/reducers/article'; + +const titlePlaceholder = 'Article Title'; +const descriptionPlaceholder = "What's this article about?"; +const bodyPlaceholder = 'Write your article (in markdown)'; +const tagPlaceholder = 'Enter tags'; +const submitButton = 'Publish Article'; + +describe('New article', () => { + beforeEach(() => { + cy.visit('/').login(); + + cy.intercept('POST', '**/articles').as('createArticle'); + + cy.findByRole('link', { + name: /new post/i, + }).click(); + }); + + it('should submit a new article', () => { + cy.findByPlaceholderText(titlePlaceholder).type(faker.lorem.words()); + + cy.findByPlaceholderText(descriptionPlaceholder).type( + faker.lorem.sentence(), + { delay: 1 } + ); + + cy.findByPlaceholderText(bodyPlaceholder).type(faker.lorem.paragraphs(), { + delay: 1, + }); + + cy.findByPlaceholderText(tagPlaceholder).type( + 'react{enter}redux{enter}lorem ipsum{enter}' + ); + + cy.findByRole('button', { name: submitButton }).click(); + + cy.wait('@createArticle').its('response.statusCode').should('equal', 200); + + cy.location('pathname').should('match', /\/article\/[\w-]+/); + }); + + it('should validate the form', () => { + cy.findByRole('button', { name: submitButton }).click(); + + cy.wait('@createArticle').its('response.statusCode').should('equal', 422); + + cy.get('.error-messages').within(() => { + cy.findAllByRole('listitem').should('have.length', 3); + }); + }); + + it('should add or remove tags', () => { + cy.get('.tag-list').as('tagList').should('be.empty'); + + cy.findByPlaceholderText(tagPlaceholder).type( + 'lorem{enter}ipsum{enter}dolor{enter}sit{enter}amet{enter}' + ); + + cy.get('@tagList').children().should('have.length', 5); + + cy.get('@tagList').within(() => { + cy.findByText('dolor').find('i').click(); + cy.findByText('sit').find('i').click(); + }); + + cy.get('@tagList').children().should('have.length', 3); + }); +}); + +describe('Edit article', () => { + let article; + + before(() => { + cy.intercept('GET', '**/articles/*') + .as('getArticle') + .intercept('GET', '**/articles/*/comments') + .as('getCommentsForArticle'); + + cy.visit('/') + .login() + .createArticle() + .then((newArticle) => { + article = newArticle; + + cy.dispatch({ + type: createArticle.fulfilled.type, + payload: { article }, + }).wait(['@getArticle', '@getCommentsForArticle']); + + cy.findByRole('link', { + name: /edit article/i, + }).click(); + + cy.wait('@getArticle'); + }); + }); + + it("should fill the form with article's data", () => { + cy.findByPlaceholderText(titlePlaceholder).should( + 'have.value', + article.title + ); + + cy.findByPlaceholderText(descriptionPlaceholder).should( + 'have.value', + article.description + ); + + cy.findByPlaceholderText(bodyPlaceholder).should( + 'have.value', + article.body + ); + + cy.findByPlaceholderText(tagPlaceholder).should('have.value', ''); + + cy.get('.tag-list') + .children() + .should('have.length', article.tagList.length); + + cy.get('.tag-list').within(() => { + Cypress._.each(article.tagList, (tag) => { + cy.findByText(RegExp(`^${tag}$`, 'i')).should('exist'); + }); + }); + }); + + it('should update the article', () => { + const description = faker.lorem.paragraph(); + + cy.intercept('PUT', '**/articles/*').as('updateArticle'); + + cy.findByPlaceholderText(descriptionPlaceholder).clear().type(description); + + cy.get('.tag-list').within(() => { + Cypress._.each(article.tagList, (tag) => { + cy.findByText(RegExp(`^${tag}$`, 'i')) + .find('i') + .click(); + }); + }); + + cy.findByPlaceholderText(tagPlaceholder).type( + 'react{enter}redux{enter}markdown{enter}lorem ipsum{enter}' + ); + + cy.findByRole('button', { name: submitButton }).click(); + + cy.wait('@updateArticle') + .its('response') + .then((response) => { + expect(response.statusCode).to.equal(200); + + expect(response.body.article).to.haveOwnProperty( + 'description', + description + ); + expect(response.body.article).to.haveOwnProperty('tagList'); + expect(response.body.article.tagList).to.deep.equal([ + 'react', + 'redux', + 'markdown', + 'lorem ipsum', + ]); + }); + + cy.location('pathname').should('match', /\/article\/[\w-]+/); + }); +}); diff --git a/python/capital/frontend/cypress/integration/home-spec.js b/python/capital/frontend/cypress/integration/home-spec.js new file mode 100644 index 000000000..3285bbeea --- /dev/null +++ b/python/capital/frontend/cypress/integration/home-spec.js @@ -0,0 +1,176 @@ +// enables intelligent code completion for Cypress commands +// https://on.cypress.io/intelligent-code-completion +/// +/// + +describe('Home page', () => { + beforeEach(() => { + cy.intercept('**/articles?**') + .as('getAllArticles') + .intercept('**/tags') + .as('getAllTags') + .visit('/'); + }); + + it('should show the app name', () => { + cy.get('.banner').within(() => { + cy.findByRole('heading', { level: 1, name: /conduit/i }).should( + 'be.visible' + ); + + cy.findByText('A place to share your knowledge.').should('be.visible'); + }); + }); + + it('should have a header navbar', () => { + cy.findByRole('navigation').within(() => { + cy.findByRole('link', { name: /conduit/i }).should( + 'have.attr', + 'href', + '/' + ); + + cy.findByRole('link', { name: /home/i }).should('have.attr', 'href', '/'); + + cy.findByRole('link', { name: /sign in/i }).should( + 'have.attr', + 'href', + '/login' + ); + cy.findByRole('link', { name: /sign up/i }).should( + 'have.attr', + 'href', + '/register' + ); + }); + }); + + it('should render the list of articles', () => { + cy.findByRole('button', { name: /global feed/i }).should('be.visible'); + + cy.wait('@getAllArticles') + .its('response.body') + .then((body) => { + cy.get('.article-preview').should('have.length', body.articles.length); + + Cypress._.each(body.articles, (article, index) => { + cy.get('.article-preview') + .eq(index) + .within(() => { + cy.findByRole('img', { name: article.author.username }); + + cy.findByText(article.author.username); + + cy.findByRole('heading').should('have.text', article.title); + + cy.get('p').should('have.text', article.description); + + cy.findByRole('list') + .children() + .should('have.length', article.tagList.length); + + cy.findByRole('list').within(() => { + Cypress._.each(article.tagList, (tag) => { + cy.findByText(tag); + }); + }); + }); + }); + }); + }); + + it('should render the list of tags', () => { + cy.wait('@getAllTags') + .its('response.body') + .then((body) => { + cy.get('.sidebar').within(() => { + cy.findByText('Popular Tags'); + + cy.findAllByRole('button').should('have.length', body.tags.length); + }); + }); + }); + + it('should show the pagination', () => { + cy.wait('@getAllArticles') + .its('response.body') + .then((body) => { + const pages = Math.floor(body.articlesCount / body.articles.length); + + cy.get('.pagination').within(() => { + cy.findAllByRole('listitem').should('have.length.at.most', pages); + }); + }); + }); +}); + +describe('Home page (authenticated)', () => { + beforeEach(() => { + cy.task('createUserWithArticle', { followUser: true }); + + cy.intercept('**/articles?*') + .as('getAllArticles') + .intercept('**/articles/feed?*') + .as('getAllFeed') + .intercept('**/tags') + .as('getAllTags') + .intercept('POST', '**/articles/*/favorite') + .as('favoriteArticle') + .intercept('DELETE', '**/articles/*/favorite') + .as('unfavoriteArticle') + .visit('/') + .login(); + }); + + it('should mark an article as favorite', () => { + cy.findByRole('button', { + name: /global feed/i, + }).click(); + + cy.wait('@getAllArticles') + .its('response.body.articles') + .then((articles) => { + const article = articles[0]; + + cy.findAllByRole('heading', { name: article.title }) + .first() + .parent() + .parent() + .find('.article-meta') + .within(() => { + cy.findByRole('button').click(); + }); + }); + + cy.wait('@favoriteArticle').its('response.statusCode').should('equal', 200); + }); + + it('should mark an article as unfavorite', () => { + cy.findByRole('button', { + name: /global feed/i, + }).click(); + + cy.wait('@getAllArticles') + .its('response.body.articles') + .then((articles) => { + const article = articles[1]; + + cy.findAllByRole('heading', { name: article.title }) + .first() + .parent() + .parent() + .find('.article-meta') + .within(() => { + cy.findByRole('button').click(); + + cy.wait('@favoriteArticle'); + + cy.findByRole('button').click(); + }); + }); + + cy.wait('@unfavoriteArticle') + .its('response.statusCode') + .should('equal', 200); + }); +}); diff --git a/python/capital/frontend/cypress/integration/login-spec.js b/python/capital/frontend/cypress/integration/login-spec.js new file mode 100644 index 000000000..4a12ecf0d --- /dev/null +++ b/python/capital/frontend/cypress/integration/login-spec.js @@ -0,0 +1,65 @@ +// enables intelligent code completion for Cypress commands +// https://on.cypress.io/intelligent-code-completion +/// +/// +import faker from 'faker'; + +describe('Login page', () => { + const emailPlaceholder = 'Email'; + const passwordPlaceholder = 'Password'; + + beforeEach(() => { + cy.intercept('POST', '**/v2/users/login').as('login').visit('/login'); + }); + + it('should submit the login form', () => { + cy.findByPlaceholderText(emailPlaceholder).type(Cypress.env('email')); + + cy.findByPlaceholderText(passwordPlaceholder).type(Cypress.env('password')); + + cy.findByRole('button', { name: /sign in/i }).click(); + + cy.wait('@login').its('response.statusCode').should('equal', 200); + + cy.location('pathname').should('be.equal', '/'); + + cy.findByRole('navigation').within(() => { + cy.findByRole('link', { name: RegExp(Cypress.env('username'), 'i') }); + }); + }); + + it('should require all the fields', () => { + cy.findByRole('button', { name: /sign in/i }).click(); + + cy.wait('@login').its('response.statusCode').should('equal', 422); + + cy.get('.error-messages').within(() => { + cy.findAllByRole('listitem').should('have.length', 1); + }); + }); + + it('should validate the email and password', () => { + cy.findByPlaceholderText(emailPlaceholder).type(Cypress.env('email')); + + cy.findByPlaceholderText(passwordPlaceholder).type( + faker.internet.password() + ); + + cy.findByRole('button', { name: /sign in/i }).click(); + + cy.wait('@login').its('response.statusCode').should('equal', 422); + + cy.get('.error-messages').within(() => { + cy.findByRole('listitem').should( + 'have.text', + 'email or password is invalid' + ); + }); + }); + + it('should navigate to register page', () => { + cy.findByRole('link', { name: /need an account/i }).click(); + + cy.location('pathname').should('be.equal', '/register'); + }); +}); diff --git a/python/capital/frontend/cypress/integration/navigation-spec.js b/python/capital/frontend/cypress/integration/navigation-spec.js new file mode 100644 index 000000000..4bc7769de --- /dev/null +++ b/python/capital/frontend/cypress/integration/navigation-spec.js @@ -0,0 +1,190 @@ +// enables intelligent code completion for Cypress commands +// https://on.cypress.io/intelligent-code-completion +/// +/// + +describe('Navigation', () => { + beforeEach(() => { + cy.intercept('**/articles?*') + .as('getAllArticles') + .intercept('**/tags') + .as('getAllTags') + .intercept('**/articles/*/comments') + .as('getCommentsForArticle') + .intercept('**/articles/*') + .as('getArticle') + .visit('/'); + }); + + it('should navigate to the login page', () => { + cy.findByRole('link', { name: /sign in/i }).click(); + + cy.location('pathname').should('be.equal', '/login'); + + cy.findByRole('heading', { name: /sign in/i }); + }); + + it('should navigate to the register page', () => { + cy.findByRole('link', { name: /sign up/i }).click(); + + cy.location('pathname').should('be.equal', '/register'); + + cy.findByRole('heading', { name: /sign up/i }); + }); + + it('should navigate to the first article', () => { + cy.get('.preview-link') + .first() + .within(() => { + cy.findByText('Read more...').click(); + + cy.wait(['@getArticle', '@getCommentsForArticle']); + + cy.location('pathname').should('match', /\/article\/[\w-]+/); + }); + }); + + it('should navigate to the next page', () => { + cy.wait('@getAllArticles') + .its('response.body') + .then((body) => { + const pages = Math.floor(body.articlesCount / body.articles.length); + + for (let i = 1; i < 3; i++) { + const page = Math.round(Math.random() * (pages - 2 + 1) + 2); + + cy.get('.pagination').findByText(page.toString()).click(); + + cy.wait('@getAllArticles'); + + cy.get('.pagination') + .findByText(page.toString()) + .parent() + .should('have.class', 'active'); + + cy.get('.page-item:not(.active)') + .its('length') + .should('equal', pages - 1); + } + }); + }); + + it('should navigate by tag', () => { + cy.wait('@getAllTags') + .its('response.body') + .then(({ tags }) => { + let tag = tags[Math.floor(Math.random() * tags.length)]; + + cy.get('.sidebar').findByText(tag).click(); + + cy.wait('@getAllArticles'); + + cy.get('.feed-toggle').findByText(tag).should('have.class', 'active'); + + cy.get('.pagination').findByText('2').click(); + + cy.wait('@getAllArticles'); + + tag = tags[Math.floor(Math.random() * tags.length)]; + + cy.get('.sidebar').findByText(tag).click(); + + cy.wait('@getAllArticles'); + }); + }); +}); + +describe('Navigation (authenticated)', () => { + beforeEach(() => { + cy.intercept('**/articles?*') + .as('getAllArticles') + .intercept('**/tags') + .as('getAllTags') + .intercept(`**/profiles/*`) + .as('getProfile') + .visit('/') + .login(); + }); + + it('should switch between tabs', () => { + cy.findByRole('button', { + name: /global feed/i, + }).click(); + + cy.wait('@getAllArticles').its('response.statusCode').should('equal', 200); + + cy.findByRole('button', { + name: /my feed/i, + }).click(); + }); + + it('should navigate to new post page', () => { + cy.wait('@getAllTags'); + + cy.findByRole('link', { + name: /new post/i, + }).click(); + + cy.location('pathname').should('equal', '/editor'); + }); + + it('should navigate to settings page', () => { + cy.findByRole('link', { + name: /settings/i, + }).click(); + + cy.location('pathname').should('equal', '/settings'); + }); + + it('should navigate to my profile page', () => { + cy.findByRole('navigation') + .findByRole('link', { + name: RegExp(Cypress.env('username')), + }) + .click(); + + cy.location('pathname').should('equal', `/@${Cypress.env('username')}`); + + cy.get('.user-info').within(() => { + cy.findByRole('img', Cypress.env('username')).should('be.visible'); + + cy.findByRole('heading', Cypress.env('username')).should('be.visible'); + }); + }); + + it('should navigate to my favorited articles page', () => { + cy.findByRole('navigation') + .findByRole('link', { + name: RegExp(Cypress.env('username')), + }) + .click(); + + cy.wait(['@getAllArticles', '@getProfile']); + + cy.get('.user-info').within(() => { + cy.findByRole('img', Cypress.env('username')).should('be.visible'); + + cy.findByRole('heading', Cypress.env('username')).should('be.visible'); + }); + + cy.findByRole('link', { + name: /favorited articles/i, + }).click(); + + cy.wait('@getAllArticles') + .its('response.body') + .then((body) => { + const pages = Math.floor(body.articlesCount / body.articles.length); + const page = Math.round(Math.random() * (pages - 2 + 1) + 2); + + cy.get('.pagination').findByText(page.toString()).click(); + + cy.wait('@getAllArticles'); + }); + + cy.location('pathname').should( + 'equal', + `/@${Cypress.env('username')}/favorites` + ); + }); +}); diff --git a/python/capital/frontend/cypress/integration/profile-spec.js b/python/capital/frontend/cypress/integration/profile-spec.js new file mode 100644 index 000000000..93deabd40 --- /dev/null +++ b/python/capital/frontend/cypress/integration/profile-spec.js @@ -0,0 +1,67 @@ +// enables intelligent code completion for Cypress commands +// https://on.cypress.io/intelligent-code-completion +/// +/// +import faker from 'faker'; + +describe('Profile page', () => { + const firstname = faker.name.firstName(); + const lastname = faker.name.lastName(); + const username = + faker.internet + .userName(firstname, lastname) + .toLowerCase() + .replace(/\W/g, '_') + '_redux'; + const email = faker.internet + .email(firstname, lastname, 'redux.js.org') + .toLowerCase(); + + before(() => { + cy.task('createUserWithArticle', { username, email }); + + cy.intercept('GET', '**/user') + .as('getCurrentUser') + .intercept('GET', `**/profiles/${username}`) + .as('getProfile') + .intercept('GET', `**/articles?author=${encodeURIComponent(username)}*`) + .as('getArticlesByAuthor') + .visit('/') + .login() + .visit(`/@${username}`) + .wait(['@getCurrentUser', '@getProfile', '@getArticlesByAuthor']); + }); + + afterEach(function () { + if (this.currentTest.state === 'failed') { + Cypress.runner.stop(); + } + }); + + it("should show the user's image and user's username in the banner", () => { + cy.get('.user-info').within(() => { + cy.findByRole('img', username).should('be.visible'); + + cy.findByRole('heading', username).should('be.visible'); + }); + + cy.get('.article-preview').its('length').should('be.above', 0); + }); + + it('should follow the user', () => { + cy.intercept('POST', `**/profiles/${username}/follow`).as('followProfile'); + + cy.findByRole('button', { name: `Follow ${username}` }).click(); + + cy.wait('@followProfile').its('response.statusCode').should('equal', 200); + }); + + it('should unfollow the user', () => { + cy.intercept('DELETE', `**/profiles/${username}/follow`).as( + 'unfollowProfile' + ); + + cy.findByRole('button', { name: `Unfollow ${username}` }).click(); + + cy.wait('@unfollowProfile').its('response.statusCode').should('equal', 200); + }); +}); diff --git a/python/capital/frontend/cypress/integration/register-spec.js b/python/capital/frontend/cypress/integration/register-spec.js new file mode 100644 index 000000000..8f41418f3 --- /dev/null +++ b/python/capital/frontend/cypress/integration/register-spec.js @@ -0,0 +1,103 @@ +// enables intelligent code completion for Cypress commands +// https://on.cypress.io/intelligent-code-completion +/// +/// +import faker from 'faker'; + +describe('Register page', () => { + const usernamePlaceholder = 'Username'; + const emailPlaceholder = 'Email'; + const passwordPlaceholder = 'Password'; + + beforeEach(() => { + cy.intercept('POST', '**/users').as('register').visit('/register'); + }); + + it('should submit the register form', () => { + cy.findByPlaceholderText(usernamePlaceholder).type( + faker.internet.userName().toLowerCase().replace(/\W/g, '_').substr(0, 20) + ); + + cy.findByPlaceholderText(emailPlaceholder).type( + faker.internet.exampleEmail().toLowerCase() + ); + + cy.findByPlaceholderText(passwordPlaceholder).type('Pa$$w0rd!'); + + cy.findByRole('button', { name: /sign up/i }).click(); + + cy.wait('@register').its('response.statusCode').should('equal', 200); + + cy.location('pathname').should('be.equal', '/'); + }); + + it('should require all the fields', () => { + cy.findByRole('button', { name: /sign up/i }).click(); + + cy.wait('@register').its('response.statusCode').should('equal', 422); + + cy.get('.error-messages').within(() => { + cy.findAllByRole('listitem').should('have.length', 3); + }); + }); + + it('should require the username', () => { + cy.findByPlaceholderText(emailPlaceholder).type( + faker.internet.exampleEmail().toLowerCase() + ); + + cy.findByPlaceholderText(passwordPlaceholder).type( + faker.internet.password() + ); + + cy.findByRole('button', { name: /sign up/i }).click(); + + cy.wait('@register').its('response.statusCode').should('equal', 422); + + cy.get('.error-messages').within(() => { + cy.findByRole('listitem').should('contain.text', 'username'); + }); + }); + + it('should require the email', () => { + cy.findByPlaceholderText(usernamePlaceholder).type( + faker.internet.userName().toLowerCase().replace(/\W/g, '_').substr(0, 20) + ); + + cy.findByPlaceholderText(passwordPlaceholder).type( + faker.internet.password() + ); + + cy.findByRole('button', { name: /sign up/i }).click(); + + cy.wait('@register').its('response.statusCode').should('equal', 422); + + cy.get('.error-messages').within(() => { + cy.findByRole('listitem').should('contain.text', 'email'); + }); + }); + + it('should require the password', () => { + cy.findByPlaceholderText(usernamePlaceholder).type( + faker.internet.userName().toLowerCase().replace(/\W/g, '_').substr(0, 20) + ); + + cy.findByPlaceholderText(emailPlaceholder).type( + faker.internet.email().toLowerCase() + ); + + cy.findByRole('button', { name: /sign up/i }).click(); + + cy.wait('@register').its('response.statusCode').should('equal', 422); + + cy.get('.error-messages').within(() => { + cy.findByRole('listitem').should('contain.text', 'password'); + }); + }); + + it('should navigate to login page', () => { + cy.findByRole('link', { name: /have an account/i }).click(); + + cy.location('pathname').should('be.equal', '/login'); + }); +}); diff --git a/python/capital/frontend/cypress/integration/settings-spec.js b/python/capital/frontend/cypress/integration/settings-spec.js new file mode 100644 index 000000000..885856283 --- /dev/null +++ b/python/capital/frontend/cypress/integration/settings-spec.js @@ -0,0 +1,54 @@ +// enables intelligent code completion for Cypress commands +// https://on.cypress.io/intelligent-code-completion +/// +/// +import faker from 'faker'; + +describe('Settings page', () => { + const imagePlaceholder = 'URL of profile picture'; + const bioPlaceholder = 'Short bio about you'; + const passwordPlaceholder = 'New Password'; + const submitButton = 'Update Settings'; + + beforeEach(() => { + cy.visit('/').login(); + + cy.findByRole('link', { + name: /settings/i, + }).click(); + + cy.intercept('PUT', '**/user').as('saveUser'); + }); + + it("should update user's details", () => { + cy.findByPlaceholderText(imagePlaceholder) + .clear() + .type(faker.image.avatar()); + + cy.findByPlaceholderText(bioPlaceholder) + .clear() + .type(faker.hacker.phrase()); + + cy.findByRole('button', { name: submitButton }).click(); + + cy.wait('@saveUser').its('response.statusCode').should('equal', 200); + + cy.location('pathname').should('equal', '/'); + }); + + it('should change the password', () => { + cy.findByPlaceholderText(passwordPlaceholder).type(Cypress.env('password')); + + cy.findByRole('button', { name: submitButton }).click(); + + cy.wait('@saveUser').its('response.statusCode').should('equal', 200); + + cy.location('pathname').should('equal', '/'); + }); + + it('should logout the user', () => { + cy.findByRole('button', { name: 'Or click here to logout.' }).click(); + + cy.location('pathname').should('equal', '/'); + }); +}); diff --git a/python/capital/frontend/cypress/plugins/index.js b/python/capital/frontend/cypress/plugins/index.js new file mode 100644 index 000000000..63ddf4bc5 --- /dev/null +++ b/python/capital/frontend/cypress/plugins/index.js @@ -0,0 +1,172 @@ +/// + +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) +const faker = require('faker'); +const fetch = require('node-fetch'); + +/** + * @type {Cypress.PluginConfig} + */ +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config + require('@cypress/code-coverage/task')(on, config); + + on('task', { + async createUserWithArticle({ + username = faker.internet.userName(), + email = faker.internet.exampleEmail(), + password = 'Pa$$w0rd!', + followUser = false, + }) { + let user = { username: username.substr(-20), email, password }; + let article = { + title: faker.lorem.words(), + description: faker.lorem.sentences(), + body: faker.fake(`![{{lorem.words}}]({{image.city}}) + + > {{lorem.sentence}} + + ----- + + ## {{lorem.text}} + + {{lorem.paragraph}} + + - _{{lorem.word}}_ + - _{{lorem.word}}_ + - _{{lorem.word}}_ + + ### {{lorem.text}} + + {{lorem.paragraph}} + + 1. **{{lorem.words}}** + 2. **{{lorem.words}}** + 3. **{{lorem.words}}** + + {{lorem.paragraph}} + + * [x] ~{{lorem.words}}~ + * [x] ~{{lorem.words}}~ + * [ ] {{lorem.words}} + + > {{hacker.phrase}} + + \`\`\` + + \`\`\` + `), + tagList: ['lorem ipsum', 'markdown'].concat( + ...faker.lorem.words(5).split(' ') + ), + }; + + // Create a new user + user = await fetch(`${config.env.apiUrl}/users`, { + method: 'POST', + body: JSON.stringify({ user }), + headers: { + 'content-type': 'application/json', + }, + }) + .then((response) => response.json()) + .then((body) => body.user); + // Update the user + user = await fetch(`${config.env.apiUrl}/user`, { + method: 'PUT', + body: JSON.stringify({ + user: { + bio: faker.hacker.phrase(), + image: faker.image.avatar(), + }, + }), + headers: { + 'content-type': 'application/json', + authorization: `Token ${user.token}`, + }, + }) + .then((response) => response.json()) + .then((body) => body.user); + // Create an article + article = await fetch(`${config.env.apiUrl}/articles`, { + method: 'POST', + body: JSON.stringify({ article }), + headers: { + 'content-type': 'application/json', + authorization: `Token ${user.token}`, + }, + }) + .then((response) => response.json()) + .then((body) => body.article); + + const comment = await fetch( + `${config.env.apiUrl}/articles/${article.slug}/comments`, + { + method: 'POST', + body: JSON.stringify({ + comment: { + body: faker.lorem.text(), + }, + }), + headers: { + 'content-type': 'application/json', + authorization: `Token ${user.token}`, + }, + } + ) + .then((response) => response.json()) + .then((body) => body.comment); + + if (followUser) { + const { token } = await fetch(`${config.env.apiUrl}/v2/users/login`, { + method: 'POST', + body: JSON.stringify({ + user: { + email: config.env.email, + password: config.env.password, + }, + }), + headers: { + 'content-type': 'application/json', + }, + }) + .then((response) => response.json()) + .then((body) => body.user); + + await fetch(`${config.env.apiUrl}/profiles/${username}/follow`, { + method: 'POST', + headers: { + authorization: `Token ${token}`, + }, + }); + } + + return { + ...article, + author: { + ...article.author, + ...user, + token: undefined, + }, + comments: [comment], + }; + }, + }); + // IMPORTANT to return the config object + // with the any changed environment variables + return config; +}; diff --git a/python/capital/frontend/cypress/support/index.js b/python/capital/frontend/cypress/support/index.js new file mode 100644 index 000000000..48216a77c --- /dev/null +++ b/python/capital/frontend/cypress/support/index.js @@ -0,0 +1,74 @@ +/// +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** +import '@cypress/code-coverage/support'; +import '@testing-library/cypress/add-commands'; +import faker from 'faker'; + +import { login } from '../../src/reducers/auth'; + +/** + * Dispatches a given Redux action straight to the application + */ +Cypress.Commands.add('dispatch', (action) => { + expect(action).to.be.an('object').and.to.have.property('type'); + + cy.window().its('store').invoke('dispatch', action); +}); + +/** + * Login the user using the API, then dispatch the login action + */ +Cypress.Commands.add( + 'login', + (email = Cypress.env('email'), password = Cypress.env('password')) => { + cy.request({ + url: `${Cypress.env('apiUrl')}/v2/users/login`, + method: 'POST', + body: { user: { email, password } }, + }) + .its('body') + .then((body) => { + cy.dispatch({ type: login.fulfilled.type, payload: body }); + }); + } +); + +/** + * Create a new article (with a markdown body) using the API + * + * @returns {Object} article + */ +Cypress.Commands.add('createArticle', () => + cy + .request({ + method: 'POST', + url: `${Cypress.env('apiUrl')}/articles`, + body: { + article: { + title: faker.company.catchPhrase(), + description: faker.commerce.productDescription(), + body: faker.lorem.paragraph(), + tagList: ['lorem ipsum', 'markdown', 'faker'].concat( + ...faker.lorem.words(5).split(' ') + ), + }, + }, + headers: { + authorization: `Token ${localStorage.getItem('jwt')}`, + }, + }) + .its('body.article') +); diff --git a/python/capital/frontend/dockerfile b/python/capital/frontend/dockerfile new file mode 100644 index 000000000..391759014 --- /dev/null +++ b/python/capital/frontend/dockerfile @@ -0,0 +1,7 @@ +FROM node:18.5.0-slim +ENV REACT_APP_BACKEND_URL=http://localhost:8000/api +EXPOSE 4100 +WORKDIR /app +COPY . . +RUN npm install --force +CMD ["npm", "start"] \ No newline at end of file diff --git a/python/capital/frontend/package.json b/python/capital/frontend/package.json new file mode 100644 index 000000000..6604c98ed --- /dev/null +++ b/python/capital/frontend/package.json @@ -0,0 +1,59 @@ +{ + "name": "react-redux-realworld-example-app", + "version": "0.1.0", + "private": true, + "devDependencies": { + "@cypress/code-coverage": "^3.9.6", + "@cypress/instrument-cra": "^1.4.0", + "@testing-library/cypress": "^7.0.6", + "@testing-library/jest-dom": "^5.14.1", + "@testing-library/react": "^12.0.0", + "@testing-library/user-event": "^13.2.1", + "cross-env": "^7.0.3", + "cypress": "^7.5.0", + "faker": "^5.5.3", + "husky": "^7.0.0", + "jest-fetch-mock": "^3.0.3", + "node-fetch": "^2.6.1", + "prettier": "^2.3.2", + "pretty-quick": "^3.1.1", + "react-scripts": "^5.0.1", + "redux-devtools-extension": "^2.13.9", + "redux-testkit": "^1.0.6", + "start-server-and-test": "^1.12.5" + }, + "dependencies": { + "@reduxjs/toolkit": "^1.6.0", + "marked": "^4.0.14", + "prop-types": "^15.8.1", + "react": "^18.0.0", + "react-credit-cards": "^0.8.3", + "react-dom": "^18.0.0", + "react-redux": "^7.2.8", + "react-router": "^6.3.0", + "react-router-dom": "^6.3.0", + "redux": "^4.1.2", + "redux-logger": "^3.0.6", + "snarkdown": "^2.0.0", + "superagent": "^7.1.1", + "superagent-promise": "^1.1.0", + "xss": "^1.0.9" + }, + "scripts": { + "start": "cross-env PORT=4100 react-scripts start", + "start:e2e": "cross-env PORT=4100 BROWSER=none react-scripts -r @cypress/instrument-cra start", + "build": "react-scripts build", + "test": "cross-env PORT=4100 react-scripts test --env=jsdom", + "eject": "react-scripts eject", + "cy:open": "cypress open", + "cy:run": "cypress run", + "test:e2e": "start-test start:e2e :4100 cy:run", + "prepare": "husky install" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ] +} diff --git a/python/capital/frontend/project-logo.png b/python/capital/frontend/project-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..901b3b61b38fe94e2ded8be5632eb358be039cbc GIT binary patch literal 72206 zcmeFYWmKD8*DZ`wpt!r07MJ1@T#6NUCrEHBPM}cSp|}Khcei52p}4zSvEXoc-tWE7 zxS#*u&yz8dJ#r=ak#)^A*IaY$9j5$M1|5|I6$S^r2lE1mr1 zh;IvmrI?}^3`|W7+LOtD-ctd;s4Bw1czuL{`5p)Z^YC`*`vDA$D?1F#kueO6Kne^D zp<_m?iqP8!NT%{Kl5gwV<6ff8?`=VG{G#LX_6_X+JYk1FUWvT@97a}BOx#M@%;?8NizN{A{DOZ*-6 z|9km=G5CKt`2RHxQlRR2$d37)wId#NJ_O}Vj-3Jc&vb3gBBu5r(0q7`|2ra^Ki6&u z)_US~)SsajA7f-_XZD&VrcCl;ax`DuLk@>YzlLpm+vw}WK?|l`HqT!Cky4{=Tt2JS z)&|dmFf9CM*G1{4W?ALYpML2WqyGl*jk`KhknfTWfdixS%~^a$v&c^K5}7)nhryT>?F>b zJf=?A0<<7h7FnPy3+<lG% zBa8?Y8KR{^_76FcH_DR3YEl#Pi`pxsw)QA)^`oH`w?DlqG_AqjTBZ`~(JR%I#V1A` z=hsDwZW;g1n9yGL8HAu0c2KnUw@4eUeUF>*Ns4{36}4=2ew^Hu9(80dcH9kCKw&OcivgROAB^JgyJay6IebvKTAF-=QNZZa=le%EGb?4Mw?_|Vr*5V1^Y z1O^6)CdOzaXN?WNYDISle6_H#qS+C{m667!Fe7Yolh?b^2uQ@uV^rDB5 zGL4N}_HIX+gcZ%=R7d2-XC{uMo>ddMNzzSQM=DBT%jv+N>FP|VAx_)|65l|ip?Yc*i`>u zZGI|s6a?b#g=d@~sE{EFN$MrA@CkhYF+w5x;1ENbQ>4&$@$4c(vP`rP$#W$UBZ#Ekh$}CDX zKOu$!65o@-fGXdOp1@yj#3KQ!Ys?c-K}CDg@|HVwo9)%0wA{(QC~72x?^Owx!&6jH zp|)MNIb-GoywYqRR56A9Z4?K-q|5nZxE;G zP7Bl5f3zWdHu>H83dQywGydZ8>xY@=OyLtC@jVgH04b8yk2?}sp`Ju!;pj*7_usAQ zNNGZDqwhV8O%$Hn7x%TQvPzn`x0*#-#_F3M^=mtgtJYL@a^pgABM45D;RPWin0M-( zByH}>?~dWbR6Ko!xL;fnNt+{cGMr*qHQtpunD6kqd1hg82wH zhhf9mi6BeM5P3@rnP2<`;xqVCg{wsDas5f~q7c<$XFT;oIY$Sf9ZMD>_&MEjSm5CC zColh6v6XXqZg0W#Wz!hCv?slC)us@-x;^qNVTYw;1-yT20u|C}0s3pnx$$Ppna*4H zAMIeO+93k;&QeCi5HygasJNIy+!dY#BsX6)nfW5OfbMf�I0?CY)sAi^MFzJ)9tX zz?|9@*}6B9$S>T)&&j^7_3)ltO-uC*P4KiO{StZp|@^%=5fM7#n1lQX5;YS zRk+nzhbT3|rMG|Np>cm=)Vx=!-)9CdDH6@tYj|~-Z3fVzAPCvy*(7bs0BFgEabgnz z!*WAS*S0)kL6@=(2*PaFz|1QiI_0E3$AWb6xv6kCv#&=f&H_YH2F;9k=y~(9iVDNp8i5sh_ z+Z=lnD9m3^^tRL@Tfn6dL_Nv=X~Q31_NKRaZ@!m0ro*nXa(U~1-aXac$mzaSsLGSZ z(kOwu7*KFoJPebok5onW;V)i3SDPxuvS}K*(*opjLFaR%8G1j4Qb~Sc1zUYYFwIvA zZf4SmgKE~;#-jWTSX881q)JGRu&Zy~!})aD%$#%=0fe>lbv^>*F zueZuYM7gPRi(#Xr+^;Navrgx}t`puqXLR!M3!ArRS>ioo{5-)Jsx|9ThJcV;isRI2 zTCX!be^k1mWlSV(Z=A!7MkNnBM>-YTZZm4})Mvu+wWLx<_$LtHZE4WUwGI8p&M!8Cn^ zvb7M}JE$K}cbH9mIMrF*5V!H@G4Zjh^gzWmtOw8$0)tDUV5UM*@Ah$S!eHg{hiT*W z>ix*lT>JHyyh{{>^WX8qYT#x0A>Q;peJ7g6@3v0(v4^I<-wADtgkJj{i_!L&Sh2%p3rF3YUV6nk;}WD>+xu(LW=dAX>tEHBqN2p-UP*T7%`Ypj4B zo+cuUVL(8rp^RmtQOe18SLclM-Jz@p-kR`!31@0&%`%UT$60GTcw(F1+tBPn&Fd*e z0-hFIf3339#gLP-wOp~v?f`9z-u1alxO~~-qrgCLFH=(mac85$^1^+P?Tl#y6M7L> zr4rU7fNRH>?t=@NcG9q9-bLbs#PODdRA zS^{^jY?gd_%cx(pi?3r6njuMZ4W5r$HdoL@!SIY^UIdDb;|}lVgptL?l-vb!HPXF@ zr$X9|0sX$Cv+pc^+*g0)m!5sQj}itWe1(lg+68QsVywRDx4dV-Tu=%8sb;;pYxne+ z@!i2_ru0hf;z`87=Trjop{7~5Bo=MG*+l|M2j6xTK?_Wr+l7b?BRZC^zA6?EE4BB4 z8wOfozVaiKxbpj2j!b0(;wTHsrF3i(HJjgmsDSo`LsQL zxUJW(Unz7%-))e)xMEIT&1PC?(DXdpQLrs&Y)Q3=Ynq`8AoblqKHJ?Ul1zeh@746X`wF**|yH_z&E6BCt&aoPZXCJu1QXNLj z6><>5YOriV4G;x#C4|Q{d5uVn7QP>P^_c$ezF0fgnU?G0X^9+O---G4xn*BB)x`)=sXNKZMZH)}K3VI;qlHKq z>aibWlf9GoVkgseBbXh!58aN9Yr3`#yRgY@gE1l9G45*6Gz2pf`VNK<}qd7>J~cGPXt;Uoro{K?Z+zP6{- zOKD^AISwmwZ}dIijsYL0-9j$EiiRijw6 zUj2Cx5lpx2>Z3pQv@>_Ge4chrUXM94?=BTaE(Lisi*~OZUqn;s0~)rd8{B)B2mQWL z%mrlc&){`skQY~-&|iik=bdNhwQVD@Cv|RbAKmIjWZasKL&!k9yT11xA?Pk%5??!< zx>ww#LMSSBJG1P0gajs^c78mwt#$hDWD3qjWt7%`{g4J0s(>c=mhBBU=@23?lSI}a zaIneM-#w32^cI!b@wGr@~>vz?%RV73v(5Ry7b>`@M(?tD>D0=dk zZ{KNOV_E$9qq)iSTeZjDf$`?($rnDR_Z9n05zuSj-JQrFs>&%yRb6jCboLh7>AQx) zQX%p(3e57jUsm#Lp1NZCH>6FS3pJEM3$gS%Ur*UMj2IG1KX4^*lC;9jO++}U)fHAz zJo=50WguC=)>8s_IV@YY8r!}FcQ!@#s~454#k%yH zv#`bv9vk3W8SK(F+A#PL2bdMdDr1b&LJ`1^LK7d0y13(KIdb_#*J7gQZ*UHAzXSik zkc#+7sJ6n*HUPG%V=f>ItZN2RM4ycMf2l|bOGEskd;sIsWQmKp-g-QH`)snme!j%l zI&+&q*~d-apw-fNd7aBe1Vlj#{w*q02})1&YPXYqiq`(qg?1umvq(UoNRWr>n;pUS zyi6BDhV}IFiQ@Fe=O*y{Sj25YX*{!~`fxO-^Ow9APYl4)ws@fwERS_nvpYEnae{?Z zg)vD{lXBbX0z6!4c2k#qYY*31TfE{D;&J~)yPdNlYQ8pSOIsLgS^-^4WnF zd$3~YYW_5j=Vhb#{!i+dm)#ANf{0r&X6K|#YwxaYEtV6oD>tQ~dB=T5G)M#bvtZ6} zGwqgD)!?z>tRfGta~XG-hEDWaXoXA+Yi6lV;oND62uuivaVTqo{{d8UDDrGW~m3&0O-|OMn&PtawF3N zgumLX@--BNega$qRjz^ugALbKh$#)Pl-K=O`GEtC(i2VpB?D+VD+0t(q)MC22!pW7 z>H)Cyu!_37ZE$ms;r=B=BLx+u2rKv7hIJ}B4R+|zB|EzDaZ=D9tc}m!aJ!$qk!T23 z_ktos~>e|=`}W_wV$9p-o}^&a%NYNRQ;Yc^I5LXwUg!8-bTpp&L5!pG2~ zWw=;DnO5pKK_W*kOu7VaJZG+*)Wt*f*_B&FPfE#*Qn*!*+6Or=EAf6pa@#S2F06K( zFLwHxrBZ?d@{y}QNqk9s1+w6|&_((eHfGPvCAJGrojfx@{@Ncy)~`Y3L(9(rHcI7I zp55W|FIQH1-L&hE=Q2%q938AvWbIzYGSKkNPE#AKtG^>`a(c6tzpwCzKE~AKn2wZs zdsQ7mvX(krt`7Ds*=*0UMz#N)#^PK9Z_qns<#3@g<7r+4B;rN91Klh_F0}=%e=F^# zycsJ^ewI!5&X(Ctm1F5$23i|`YC;1M1DqRU1`xf1#aquQce(v@4~xKibrjdPLG4d3 z9BZ{L-CHTa9t9r^JsrYoAkb4JXAA0 zIyKC-F2>oXhWl*KT>$CfT{Ks;h_?j6rf620Qhf+0X{-&X|WCoEXl#2dBP1Q4>QB11c@ z9*(N}9LInTOloYCIi0m6nw{C<_zRj7cJYqucRGcWNz_h;m9xL-@X%MNXs^})+e;h! z-IQyt3eFDlTCgLzHF;P^sX}WnH=Dc3lmq)5`!bHb6^0Rd9YBTyh2_d6h3l>{n$8i*v^=tSWQk%sGUP?h5xd5?Q z!^eptnQjS@*I!3U1j!9&OEnyid;Y8UK?9O~TNH9pmhW|_lCIg~%pY6h=x(2VA4yp~ zUfRjK9Y~(Z{#|+l`Gnr$gQxc4GF%7y!~5+X+~L<~8@E9`LBnZ-bzgDh%M~|#ICnpn zCSv07={EdI;;58@`jKncWW%2@m%&NiGN&VnzXA!^3J2r|QZHF*u|UpOV0#Gh-=I|3 zuC{s&LJ}w2s??G(cP|A$)w+HciL!c^U%H{p{;H#)3<9tysIdLw-!30ykX|gFX@5+F z%O%Z?!`L{&Qjb73l(p-3T&TxRYkCkrHN3dZ1r$~>DXl+Tb{Q@GRY*^6H$M!f1RqVY z@w)bf%vF$iSgu1?w#3(Bzcdf%Bz|@VN#!ya+cxL(2=jOty4d_J96UQ&^ca>}Eh7QH z{}+qDqsCaRM?u|1ugT5f47h^`j>Ms$=X3oaf0?<)6-eoVlu$m-xLpmoP3-Xy-j|U} z?dj+b#P%SD`XB4*U+0#>4J z%L%bJoP;k!ZUYEpbh zr9boBD>Wk6={O`$SZ;fLSN7a5sCDwPeLDwMd=#+g?L0`=u~-kSpsVr z!Y3g_ehIg1Gg4Indzz|)KhG}k!rmqk!{6?08;yUJ`h>})^1+yo5Q`%%pGrXJ@Ru~t;^Z{DU>(nJwBnmE`ZB!oc6zQYP-a|XU~@7EpD3Ae^oYIp+mJR zW&}c0+Oy*tTAi6e6S(<3A~@S*G*conb5>RnbouMv5QU&w)s`RL-}K87U+F8<5B^{}3{i1TBwXGBWxkee{}-N=jwI%!-g&(Ss14K)_Y>K8EBz zb0kC8|nH0c(w>w?IWQ=>S|{NbX5f^->wiEx$2)+jRq zBu`IBwZNwKF4TrS)`NM3^PIe{s>^+0{69Hfz?IK6P{#bad{F}*h!j3$7~-bjxf2bk zY{0zfu$|vKVaQIPj8qD$c#vV$%XQJLJ-^$vTbFORYZ}d1PjI+iz#MGH)pZ`M0<}5y z2XXr0^>u8D^N_?iYXTlSt5kGLTrtrN+kx8$QXlgxrhtV1MwpZ1TQ$hnPvq9ibXDoV zFsLogNEH?fV>{vUlJDuQ%b;SD}up2Pt_y%1Jqu-_p%XPG?RwVpi|A zD0;y`JFPNEMw4~4-bZ_%kcxB`H2+O+k_~ShV10=vt3-^R1opZjn#{v~G3EmVi1VV8 z6l{h^A+Eh>N7r%R`D*m_!O$Js&(iJsAa?#*)-c@!1EGp@mk*>5cHVh0$i^SUqa^Qg z==Qt@BQaNWL#=Gp*q?`QXP2RhK*nS=!g4>4jYJ7>P>>vxkkNS}cOF;Dq$;zdThCAh zCUaNy<8p;TSVI)WqZwDj0ELd5@BFRce&0Od6}Clyy&OUd}uP!-%dwB%NQ~#{Pv&;hI7UqjgI4EC?OGELmoUCirHzl*MN+mdkkHMGU|v+!Ncw- zDP8=yO21bP87z!n6a>;Mv+~t<^lf<$p0tIaCs&cwiP6T`_>z59z=^=C8P>+fYUN1R zjQM4oWlB59H|w02bF}!`?bpxSn=X)9_QK}la6w>=J9&p`&v(`SpW{dERxk05Mp@r1 z=C$(%4beu}4%VE0m5_I3wPYU-Rhzv-vuF<_V`V4k!_#GQvm*fjZ<9_CNhv99?XR2v ztNJLEZ;B;_i()^7s}wjA6KFik)1Wy?m5$NQ{pJwnvuLFtu5)O8o7C9O#DW;zl~s0~ z_6$BsS&3!nCDCbEc&dUo`aY~HS|On!i3sLCBt(FyQ@>!GQkVqFhZ1_mNa3e-u6YksVa(hfU`uTp% z8)Oc|`dJ@$@nL;vO?dX;2evp_A`i`ePSIs6$@mn}H(~Iy zZeK}Ltqfclr%~+4QRK1q={n&ZyNy|#UlXvc8k6Q~NU*G|6*Y7=-wI!a^)pOS&SKTo z6fTmu{f2lnf`;?Nt{jOUiRP$rhnmK6Xmnj9?cX_yb!h;(dz5#eqSa1_$Lh#lcXf;KI-J!@>GrY75T%qIlXs27GZG20){u z)iHU6!H{55=CX#+?$cS(7-dp~QhJT~ILcU)`>334BhjyTvE!p`qCQ*-70^P9li7WD z<%+D*%KA39*UG=?k{Y_t>ji_4_)eZZpK7HpSO09;lto*LQMI1S)Ygxpu_lx5aVtdH zqqh==^XIVny<~H-?wTk{?pyEv+sR3o)8cO0=-VOI#!{-+^Xm!M>vF!80l!5RNmOXl zy*EZoU;sASZREutg0XQO24Q$x$Pw^8Is^P^Vf;H)!<+fN{nC| zc^0gDwis{){%IfUJIqI2=c`#+!##W>#hbc#5&-oJ`h$BI7K;gAxsA>O$`FjKI zBORLOk4!?~cAf71uRr+@v$=tc>JHnjiw`+61Ec#9!Qk^h;wI7+IGf6j}iuP-Y435OmRY%*5!lVRt+w8T;#dIC{g zoZcEDk}K(`ou>gZB)$(zNKS9aK7=$@?${L>a$Q;dq*iB{J#jcOjBRyYUfQ&Le-gPT zL#;ecP}=@Apjk$kaD1V~>SX2YUNPd$eVta!^?^@;&G`2HJmULov(!rZNKTuD`e4$d3DQy-xD?mXuc4V(Kd0>oAnC7PFvqV7DLFL-+s0 zEn4dEn-_Jpb3h2iklE*CE-xSPcPjGJg+BUiG7GtfSKN!vl<9!}Ozzl-g9Eg~hZsAH zVK^Ok3_bFm3qKMb2P3TwaivYVVNMIDl3RWNn0TiIM%;x(^~vnw{`4} z&?0b!^h2u)oQ+7}5l~f8ALK^c>}EQxT&9Hf8#hasXy-GMte1w%td6_sxL15K^C#WSBeZ%BG>X$ivSY6WuPPU@CKpzvvqgGptkyjt?VMMHmh;b zur4so2wH8hnfNKd#1kGR=7fLj>3Zek&Fti7pKSE8&=N;`g;fvPvWMYvqP78rd!muS zv_EH}yaasa@^QS08|@_mUyO3VQZ_xG2eqLyNSw_SUpjLq@$=3dEL!3{l-!%1%_OWr zT4&d97UU38#H9gBFW}0Qicu#ZMe78z{`5EUzQmgpu;(S;*w;9&JM&~Lw7VZ31e)-k z-r5_OMn%MPDX5I=`E{x1*IMu<;|Qh)bh^9XkH0T}?}EucPr%x?FP?T&Lhf8V_`&pu z;CS<)DGtF`)b0nn%u#Q5gX@BQhauWOT-HbNCi@Vu)ZW*Fxbw?Lgq546EP*+ol&Pa> zu(+*tfVk3*t)9{ED+kg>2L|1Er-4DBf37RBaM6e6i_woa>vGd|R5_Ts!nU%iQRAq0 z7l;){R~2d1pt4#PygqJFkRkuTZcNP5yhvPwOW~IGK4LMya7B)JW6gF|Sn-q42Y|Dp z?u}Bvb|L((CyO9YR3>-5pu0Hp-|QzL22q<(njc}zV-+?2{DZvfA}%J=Y0@z*YfqxL zqq;NSlx!?*J5X4bG!u#XX35l1Q9XZ>kTbGUo=s4$A8Q;+cS_W(G#WRQiC)JPGkP&uBw@E|e#dq-D=$DSC}}PuN6+e; zdBggG^8{^0aHPhM8;ZeiCQ)|8CqrzMr08nCqsVIbSoK&*%(@bKmerkaX7|+3uUQxM z6WId00cYZGW?IiUe+l}qTHvNf_UP7wFM|=b)SW~}kW{}uSJQtWvgFkL3KO$UP!YtJ z1VFH@+=hNi&*9YZGuS@u2p;hD1*6>DGkXb6&UbI){4X|~B2zTQs4;(ij4^=N)VF>5 zy~XgZ#*5ypu{Qq*5bYm}e;d6pUp2H?@MiOIHwx)U!RG}1#IZhI;WJ{?ITNZloP-m{ z=k%GQzgubR<&;H?gVAJZct~4Jzbtu2t={uJF-yCpljELSRLI7pglA+k=dm#*uzznz7}hqky7pCT?Ki`K2c!=G^@ znYJL6Bxq@${7qT_LDuLGmZOSm^D_j?e-1d55>FgCNI$gClCOr@nM|D!3e5~Z8wu-K zk688w@xFSfslCa!Xp$;83Yc-wwP+wHHf!E#d4G#!hv(O4V_EXX$9N?2m|S169IsBK zf68+xpfF!#A}8ERPMfF6jn=J}ng~v2%beS5+45a2 zTKK*cC}^5dPX>|;A6K7{MV$=2R#T_N@zSFkf~Guwum;o;1i>NBA;U$5+0bB?2ZIbV zRKr1^dUL$e!uK@!;CsED88poOyv+vX&3LSOW_INSkTVSczYYtPLd3pZ> zRzIGFR+zX$TwFi_i|U8=QW(hsbput2xk=yock`psCc-VNPdP)h%UdOJiu~!Pn07UE z%0UQt5y80{JnG?p9{0RMR<`%r03Q!fbog*M=Glck^g0p4H#K7v?Sv(we8CDq+P~5{ zc(nOy)IW2amUK`uBL@XEQ*t*9&J$csMN7xHGj{yFkF`H(((D^uHK19sUpd$_u()m%-Mtlq~LU!+lPq3M$GrK3LP)>4Q}itgd^o6zQ5S ze!*dMm}DX8Vxcl&srW%{;3@GbY5ZNQGxljt0j>35`H3zx_a#gvW|%f9eu0jT;i^2U3#6r4e0PurPnN_FD)s~ivoh- zqvTN@66u5HklX{C^j>z=01omgN`{A;Yo>mfBuI=s4}2!|^c0!pkha zB8n=xuV3z>w@)#f-zaw=6lhHax#Y>$4Pa*^GD7sK!YaP(4vZydaE(GI*rU9M^l>o5 z_#)_|uFN0wyyFP6&wk5g>1+HnMX>j4JHlNb9_7z#-2Y4FGwE++{yosNS*MP!vPKF` zvt=U?Q!)wK6N?+4Mqs=@@mSs^?4akPSQmtWm>E|hrsiWOoL-Ov@X)8mfXpn6pz z=C^H4!Yf)Sujvlu7lwaZ=~VItS)v7GH;TG-M3wT8Y5VZ*Sy+Me#l4M6J&0CrU)>}^ zHv3P0@*4ov$nh@yN8TtQv%5phgX|wth<2K0rB|<&R7@LFq>#7|eRKZlNg`{_XbG4K zzCNC~jX3tn(gx!um_!HN^Y=RSVxxu7?(Nn1_ek+GS5jh~WP0Y~=MOUo?7GpjmRYWg zX&`&RV7?o^B_8-8Ju60SV_^K{)TQeeVlrcl267m58EIQ2tDKe1l3Eo@mtL1Wwl+8+ zXb}R38Nosri!7(=Aa8_#r`Mq4I0!+rw8L|{&REOIlc?%d_ zK#z16YoT}vNI*tLJ|aAbUW2?~`6@&5ELE20v8{EF%dYu^jey)awX+DlEKid0K(;Vo z49p{_$55sm4)7nl%m^wd=I13CXE4j-$?lr!C6Fn&?@lf;x@Vv+OsC$k*l2NGda3hw zP};m`uODCTLO3ux@*T{;oN|Af&3Fe$qt3K?8!Z$I8|%;^udR<$ws}u<<2^! zIYB~X`=o1iA06!G7Jh~YM+b-zRvcU9GOVlLZ!j`FSf49o9Bqsk?pBqaTfUBF4ue&0 zZqISKYTeEsF}|gqbnYI0to;Xq_lh%fm6N(g ziw2NdL4XuLZ?AbZJsP5fp3)GcMX=s7MrxGae~x6!u;n_P(Gs@CkG)Xfs_^o4Hb!He z@m9+1Tn|KTNoAw2l>P3u!Fb?&Lg%+MJ4s8`;^njF_5&*Mu@~Yl`VMW=*l;5gubP3X z{rb1uTEL*`U{_IVSDY3G#eK79n9VyOc2sGW3NT&L8S`R=q3Z&>-1FYC(p;TStjEKnQsE}zd-0i}iR0$ek`tzHh_NXF&+Q(l zF(njNyhu+@s4-&y_`WH^Xm;yMc6${gb-%RROcfz6-V)=Nc)oPj8Yth02+zZ`XoNJv zM7^I$q33AoLw`A%qdah#HMGu9o%|^%zgP!Vh0@se{1BHb6VGPYi)6ujZ}LY?MIZYf zwTycJK95ObXT`VS*d&mZq`keLckY3O10SX)gIcq;&p*ZAqo>(WMpq(N-`aj1CJBy4 zZCqQ`n^NgUAJpI{RKn;zG`Mx1c&PsJ(i~uGwqG%F5p0n~N4bUrSlBHa-0SzaH0Qkb z$4QU2er|I%2+}WsFHrXKVP(2fWz=1=Kmga|H>p{n{wH$!;8oSDJS7of0;LCdB6U2S zx6P`Sw} zpyo(+!%D7o;X{St`kI*m$6DI!=74bNO40VuAgv}j7%}KgM}Xe8*qWj&?AkcHc^dr= zlj7?%K4c`>q5z$Eh!*3FPM-K>^_b76T!IUqM0eLwlUZg^wrA!B zrO*fo7izlSt!P(A;=k|DrK`%GJW&f}2xrVUqq4pXOT*{M34@Ee>G7(JPhyFYO%~mM zZ#}v}{DYAAsS!p=2TtUJW`GAwSo46A7h@`SMq3_;jRH*;!RvJ+8gVPFCO|5Ys$h6K z8pduUl_kSdjDvi-yZx{qxtxIU zzM=i}{(5I0(fgLPEsN&5wlYT^NB~MOET>{w`z0c*x@$ZR-?I5c zP{*Fw9V`;%RD?bwsRc~x)-DzUo2wR7MoCdn6_vV>3|&Ikmyt%{qITQ)g{4KxKevn{hXEZG>>4tK z{f*FgGi5V!D)vg6cAPjYh1cSXj{GP+oD~0e6U>!qWc^86Mr3hDe@w@z6e39o%`wA( zX?TXl4qC9kCq;TIT&{oUbQ8XgHM4Hn#Yx&^>&9>{pOSD};;YYx5r@bgNmNpuNVshc zM6jcXC7_%1HT=BMY6Pf0uv#_zsAB!}n&`&jpHMXEKk_hD^kJ6a3YhOeR?k*X5N~~6 z>qZA}$AOgG*UU!xxw@{84E_oyO}!_UJvN*QTNh^_dSV*F%%Hz*eEEuVQeBaLd>V>8*D1A6xHL1@KH#syx>t%Qc~8@O|+j2|Q$t=}R!| zgqjEB2u;4QDKuQv-7V|Fh;*dEe6T4Vovqe_td?8Jk7c4KqJuv@7(DFI*UP^h zWC&`R0=ZEWHcCoOmo%i!^YaqJ{DiQnxX>B@#p-g$T0mG)b2Y;8p#F=o5?}?$gwH?_ z;xFL*APHQ7@hSeg?921FKp^JTG`nv6u^5VTM2S}_bI1q(R6D*+@J~U6a?*+A_^sp6 z5%g=^g8B{xBcQ3z!IqKIqvV|5@Mv|5uVEQ)yQjK4cQ>Kv7#eWmNadJ0RVct%d3vOu z6m9jUZQ^z|bG9~D3--oMJ!b-4Qa7X{;iMtjs$7~{=bZJ*)sttO&CEk=dSKspNYx<8 zMW24x@>&i3z&t@ezpI*m(2_Y@6mlPPpk{yahi0H`2BmP^ry5oVOI~m5Iqklc%~1Y< zC4M;$%TabgN{+mDEOhc%Bu_ZraLV2%=`8Apoa*DMm+$jygJ-nEzETQ`RJP>HPQU3A zXZ!4zyUf+S=+4+NKM}1M9@FuUw2LHMR%4N-)N;g7HWm;INNIcU#<&Czy?xKF=quxu z`3;~$FQpKt!)2gHVlafQPb}Qa9e&WO#-Z(Y6?zZZ|tGP6K z8S*>AziIA1

+E75<=NJLR9*P$%(|J$2W~Nh={W3g=n9Xl)NB; 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 literal 0 HcmV?d00001 diff --git a/python/capital/frontend/public/favicon.ico b/python/capital/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5c125de5d897c1ff5692a656485b3216123dcd89 GIT binary patch literal 24838 zcmeI4X^>UL6@VY56)S&I{`6Nu0RscWCdj@GJHx(%?6_-;yKy1n;EEf9f}pr1CW5HA zYt$%U#C=}?jWH&%G@BaHBxsWAoUb3}&6%Ei@4Ii_JRa1`RQ23*yU)_wJ$?H0>6gj0 z${d_I^w5kvTW3xYEc?FvyP3>p$!py@`@T`|dVepIsjbbvR}af%KKy7YuQ%SDC^zmNWPYR^7avI5P-@dKev}UZ^aDAOyci9Nn zwR4qEz~tSvrp|#ACvWzo9`3B;`}^{t18dxaH;?xT7#hmJiKAaI;|O=$yxzXNOHGw~ z^!5pE^SW`av%t_$22LFPsM^l%=PSp!3r`>9w%s+^ZQYnnTQ*Ggd9-1~kj_o$YdW@b ztCkJ(ZGYjusqV5L4{^)R9Gt@gzU1t|?xhE&c^q(|(R#oa*}Sj5c({A$mhrB8*Y@tc zr)K#C{KOp-eHl35ZWJ1&zkmI>9DL%!KJE@_!=W?aH;i?ZDb0O1HPFy6 zcV0Kf)eZ0BHmz9vowF7EA{z*aue9M)iJP&Zd)qYlfJ-c^sS1qY^?>s)!!Ta@x zr@Lz|80r)7<{QVk9Z$}5SDaVtz*Rc?oH5~Wcjoc^eA&EdJ^h@aZ-BvL{K2s_7Cvfr zFL&(R?D&(9OxsS%z_BzI9^Ai^AOF$PUpGk~oO(=OpMc3@Zh&KH1a9>G%%0rC)t@oQ z4d~M`hX+g^Wf8P>A&&qjq|tZe*44Laq7qVPK#QIc)s*Qj34P`NL`Q{xBI`SnR!RC? zlGdTvC%oVZ@0BgcH>}qc!uzul@{i@sH}L0|=eZBJ9qF!HHaw?`s0(_DJj(v`(memI z6jH}=BfGlSlRV4)ouv#h*65yRR>G zo;I#~BVK&l&{+H=_~Nq$d%bFLh7GE5pS&>Fr{RMe>)MM19~z6F1oQo_y>vtlpEZF# zIc82TpMc3z9;{Q)=zG5B#4+96yHCvYy8p4;C%6x`%y$2HccC9|#vGVD)**C0xX|R| z%h)}ze!Tnrvvb@RZ!GX@2lMEq`=`08b`9$%FnN@*zJLo2wD5?MbE&LN)Z>Kty*;m= zt{Cn0>Q3nk)`bR^{dVf!3ECg6Yz4YcskI>$XH*L8E)MsudhnkP0B>+M(XEcErHUBKi~ z1`fEP&WPhp{@Ew?cPlR(ma9iw8NbJWHqp=btCtM*FnP*@ZwwlJ&-Y|LEjgvJzUtPc zz5CrWNBRV8d0-bpWAl<=zM1PU8lJseDxBK^QuuCj2fg{&2#*IG5ezf1B(o%lU+OZx7So4D?yi2*h zFBkr5pG3AJs83uy!~C3mQZLp~ss7-N9oAY>t)!eC#s)CrPukK!(!G*)H?v(~JCoj# zfvgTxMV{4?zL1neQ;ITVBAdFDf`1yG$o{g7^1sR_n{RZ7tnXio?tM%240}(z9xFY0 zlz{^-G*RET;-`7`>e0b{{`!2kM)t7Si9ZqD$~wh*hyGC>z~qs@0T&u*;h}hiKGEga zHkJ;%7aNc^o_0(>Z{Gp069H;TwPTUnvvX0SJ+kGGZ0lFBWocl>kaa)AoiMta+x_-J-?#KHFnJ*! zwD1V?)4s#|?O)DlMBhVv4IgZs?d>b<6%xK3<{o91H?-%8?PK!_fm#3d>{{gQ z?*8`b{G6?bZKdO{_9IVlz{R$PcGjeL|3*|@upby()_Lf^eQ&XQe)CjsbJ3Uolrgt< zweld3GH|fZpn(=1@PencO_a_)v6tU?WV-w8wfXLbOGae0{<*C?Ead$6v+> z|EQKThJTmwXK!c6AOD+FgtDv7i<48{-OPce!KDVkzR+XKOcREPha(;$}iUb!*)f-Fb}Y4@r9z-_{OIg z`xn^T#ZtEPv_T$M*Sr+=Z{q#~8$|7Y{0!*2u${D*Jj%dfOrS~FzpH*_|55J!7kl4w z?LT!7T(!3!632pmZh?dh`n-z$_ts42pn6;c`}hx;TSYd0idsqal5&0uGV=UM{c9xQ z1KK6&TS+a^H|6B_hPo1W3 zh+Dun!`UkP%H3}*@IE18q{7&MH2f3?T6o}Jf+xI@fh=SyUOArw`*w1_-PUlHZTHc@ z--yqIxPtI}IjPRzLIZ8cPv4P=>?A&=E~~0)>&J#V;TwAR*6}`01iu~U$@prtzW6YS ze}E>gUX+0YuF}B+Uhw2x7a7Q+oOzMNFHTNN<)40Rzg#`pABKF18@l}5A>RL`?Ri;Z zC8ExD$)im1@R{N7(wIog8$Yn(6%q$yd9(zKe};OnH%;mWBs7)>ls~T3Wi6!Xqw6+dpJLVS1P| z9qV%io-nE*rYcPxiS31>U_>mbPTXxkC*!?*zefr#2vF|qr8{|4|u^7-pD|f z&OPc->UKu)=iHgIpysp;Lsbyj}GJWoBkufOA={CRTUjr%af zc5pUH9{pg?M5%+)oN`q9yBbBt@+3xHV)qGm8b)Cp-w7~CwEhtBUk0rbjrqM zTb|tQ3-5-pw^cul`T+X&s?O;?V(FD!(Q9Qg@(LTCNz{0-vBM^SX5lti3|GpxFn4;Ax6pGc~t)R!Bo${lYH(* z!F&5X*?S&}YoDCyzwv1H+XI(+rL`;RN9}iLxlfr-r&vGG8OQa@=>+a)+Ij)sd_{wu z1Am(+3-RFr4&N8N6+hqo19S#;SA1-hG>07p3}&*j4CR+rqdV)^6n; z_vFr!(a%-=#=kb{pYmNL@6|DWkw~%E2V2jYl*e1}c{e$fib?(O+hs}eoBLRo&9(;J}YV}0Mi;LZAe{U$(s= zT<-IaV$Z+q-P!~3{HxN>Kbw30jXzM&I(S<6Ksx^}HvU2Vntb!etSsm0>)j}Me^+L5{2yz--)?W`Q?az z!WLG4UNP}+#C+NKH+ZG-Q=E>IPp%LuKLx$$8NAOGr(#~P>!EA zDYlpXDR=xM?Xv5(-qp74Cw3LzBeASHSBY`OezkbOyjP!G%WSymju_C$VBl--z + + + + + + + + + + + + c{api}tal + + + +

+ + + diff --git a/python/capital/frontend/public/logo-banner.png b/python/capital/frontend/public/logo-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..d7b23a02d4c636a6f3d50ea32c73297c38e8707c GIT binary patch literal 55673 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 WpSKX}Ob<^}&>JSdm0i2@$NvF$9b=aO literal 0 HcmV?d00001 diff --git a/python/capital/frontend/public/main.css b/python/capital/frontend/public/main.css new file mode 100644 index 000000000..c00a489c1 --- /dev/null +++ b/python/capital/frontend/public/main.css @@ -0,0 +1,14793 @@ +.logo-font { + font-family: titillium web, sans-serif; +} + +.banner-logo-img { + width: 50%; + display: block; + margin-left: auto; + margin-right: auto; +} + +.nav-logo { + color: #109cf1; +} + +html { + position: relative; + min-height: 100vh; + padding-bottom: 100px; +} + +body { + margin: 0; +} + +article, +aside, +details, +figcaption, +figure, +footer, +header, +main, +menu, +nav, +section, +summary { + display: block; +} + +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} + +audio:not([controls]) { + display: none; + height: 0; +} + +[hidden], +template { + display: none; +} + +a { + background-color: transparent; +} + +a:active { + outline: 0; +} + +a:hover { + outline: 0; +} + +abbr[title] { + border-bottom: 1px dotted; +} + +b, +strong { + font-weight: 700; +} + +dfn { + font-style: italic; +} + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +mark { + background: #ff0; + color: #000; +} + +small { + font-size: 80%; +} + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + border: 0; +} + +svg:not(:root) { + overflow: hidden; +} + +figure { + margin: 1em 40px; +} + +hr { + box-sizing: content-box; + height: 0; +} + +pre { + overflow: auto; +} + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +button, +input, +optgroup, +select, +textarea { + color: inherit; + font: inherit; + margin: 0; +} + +button { + overflow: visible; +} + +button, +select { + text-transform: none; +} + +button, +html input[type='button'], +input[type='reset'], +input[type='submit'] { + -webkit-appearance: button; + cursor: pointer; +} + +button[disabled], +html input[disabled] { + cursor: default; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +input { + line-height: normal; +} + +input[type='checkbox'], +input[type='radio'] { + box-sizing: border-box; + padding: 0; +} + +input[type='number']::-webkit-inner-spin-button, +input[type='number']::-webkit-outer-spin-button { + height: auto; +} + +input[type='search'] { + -webkit-appearance: textfield; +} + +input[type='search']::-webkit-search-cancel-button, +input[type='search']::-webkit-search-decoration { + -webkit-appearance: none; +} + +fieldset { + border: 1px solid silver; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +legend { + border: 0; + padding: 0; +} + +textarea { + overflow: auto; +} + +optgroup { + font-weight: 700; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} + +@media print { + *, + *::before, + *::after, + *::first-letter, + *::first-line { + text-shadow: none !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + abbr[title]::after { + content: ' (' attr(title) ')'; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + .navbar { + display: none; + } + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; + } + .tag { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #211f21 !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } +} + +html { + box-sizing: border-box; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +@-ms-viewport { + width: device-width; +} + +html { + font-size: 16px; + -ms-overflow-style: scrollbar; + -webkit-tap-highlight-color: transparent; +} + +body { + font-family: source sans pro, sans-serif; + font-size: 1rem; + line-height: 1.5; + color: #fff; + background-color: #211f21; +} + +[tabindex='-1']:focus { + outline: none !important; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin-top: 0; + margin-bottom: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #818a91; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: 0.5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +a { + color: #ec8cb5; + text-decoration: none; +} + +a:focus, +a:hover { + color: #ec8cb578; + text-decoration: underline; +} + +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +a:not([href]) { + color: inherit; + text-decoration: none; +} + +a:not([href]):focus, +a:not([href]):hover { + color: inherit; + text-decoration: none; +} + +a:not([href]):focus { + outline: none; +} + +pre { + margin-top: 0; + margin-bottom: 1rem; +} + +figure { + margin: 0 0 1rem; +} + +img { + vertical-align: middle; +} + +[role='button'] { + cursor: pointer; +} + +a, +area, +button, +[role='button'], +input, +label, +select, +summary, +textarea { + touch-action: manipulation; +} + +table { + background-color: transparent; +} + +caption { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + color: #818a91; + text-align: left; + caption-side: bottom; +} + +th { + text-align: left; +} + +label { + display: inline-block; + margin-bottom: 0.5rem; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +input, +button, +select, +textarea { + margin: 0; + line-height: inherit; + border-radius: 0; +} + +textarea { + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 0.5rem; + font-size: 1.5rem; + line-height: inherit; +} + +input[type='search'] { + -webkit-appearance: none; +} + +output { + display: inline-block; +} + +[hidden] { + display: none !important; +} + +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + margin-bottom: 0.5rem; + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} + +h1, +.h1 { + font-size: 2.5rem; +} + +h2, +.h2 { + font-size: 2rem; +} + +h3, +.h3 { + font-size: 1.75rem; +} + +h4, +.h4 { + font-size: 1.5rem; +} + +h5, +.h5 { + font-size: 1.25rem; +} + +h6, +.h6 { + font-size: 1rem; +} + +.lead { + font-size: 1.25rem; + font-weight: 300; +} + +.display-1 { + font-size: 6rem; + font-weight: 300; +} + +.display-2 { + font-size: 5.5rem; + font-weight: 300; +} + +.display-3 { + font-size: 4.5rem; + font-weight: 300; +} + +.display-4 { + font-size: 3.5rem; + font-weight: 300; +} + +hr { + margin-top: 1rem; + margin-bottom: 1rem; + border: 0; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +small, +.small { + font-size: 80%; + font-weight: 400; +} + +mark, +.mark { + padding: 0.2em; + background-color: #fcf8e3; +} + +.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: 5px; +} + +.initialism { + font-size: 90%; + text-transform: uppercase; +} + +.blockquote { + padding: 0.5rem 1rem; + margin-bottom: 1rem; + font-size: 1.25rem; + border-left: 0.25rem solid #eceeef; +} + +.blockquote-footer { + display: block; + font-size: 80%; + color: #818a91; +} + +.blockquote-footer::before { + content: '\2014 \00A0'; +} + +.blockquote-reverse { + padding-right: 1rem; + padding-left: 0; + text-align: right; + border-right: 0.25rem solid #eceeef; + border-left: 0; +} + +.blockquote-reverse .blockquote-footer::before { + content: ''; +} + +.blockquote-reverse .blockquote-footer::after { + content: '\00A0 \2014'; +} + +.img-fluid, +.carousel-inner > .carousel-item > img, +.carousel-inner > .carousel-item > a > img { + display: block; + max-width: 100%; + height: auto; +} + +.img-rounded { + border-radius: 0.3rem; +} + +.img-thumbnail { + padding: 0.25rem; + background-color: #211f21; + border: 1px solid #ddd; + border-radius: 0.25rem; + transition: all 0.2s ease-in-out; + display: inline-block; + max-width: 100%; + height: auto; +} + +.img-circle { + border-radius: 50%; +} + +.figure { + display: inline-block; +} + +.figure-img { + margin-bottom: 0.5rem; + line-height: 1; +} + +.figure-caption { + font-size: 90%; + color: #818a91; +} + +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, liberation mono, courier new, monospace; +} + +code { + padding: 0.2rem 0.4rem; + font-size: 90%; + color: #bd4147; + background-color: #f7f7f9; + border-radius: 0.25rem; +} + +kbd { + padding: 0.2rem 0.4rem; + font-size: 90%; + color: #fff; + background-color: #333; + border-radius: 0.2rem; +} + +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: 700; +} + +pre { + display: block; + margin-top: 0; + margin-bottom: 1rem; + font-size: 90%; + color: #fff; +} + +pre code { + padding: 0; + font-size: inherit; + color: inherit; + background-color: transparent; + border-radius: 0; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +.container { + margin-left: auto; + margin-right: auto; + padding-left: 15px; + padding-right: 15px; +} + +@media (min-width: 544px) { + .container { + max-width: 576px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 940px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} + +.container-fluid { + margin-left: auto; + margin-right: auto; + padding-left: 15px; + padding-right: 15px; +} + +.row { + display: flex; + flex-wrap: wrap; + margin-left: -15px; + margin-right: -15px; +} + +.col-xs { + position: relative; + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} + +.col-xs-1 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 8.33333%; + max-width: 8.33333%; +} + +.col-xs-2 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 16.66667%; + max-width: 16.66667%; +} + +.col-xs-3 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 25%; + max-width: 25%; +} + +.col-xs-4 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 33.33333%; + max-width: 33.33333%; +} + +.col-xs-5 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 41.66667%; + max-width: 41.66667%; +} + +.col-xs-6 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 50%; + max-width: 50%; +} + +.col-xs-7 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 58.33333%; + max-width: 58.33333%; +} + +.col-xs-8 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 66.66667%; + max-width: 66.66667%; +} + +.col-xs-9 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 75%; + max-width: 75%; +} + +.col-xs-10 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 83.33333%; + max-width: 83.33333%; +} + +.col-xs-11 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 91.66667%; + max-width: 91.66667%; +} + +.col-xs-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 100%; + max-width: 100%; +} + +.pull-xs-0 { + right: auto; +} + +.pull-xs-1 { + right: 8.33333%; +} + +.pull-xs-2 { + right: 16.66667%; +} + +.pull-xs-3 { + right: 25%; +} + +.pull-xs-4 { + right: 33.33333%; +} + +.pull-xs-5 { + right: 41.66667%; +} + +.pull-xs-6 { + right: 50%; +} + +.pull-xs-7 { + right: 58.33333%; +} + +.pull-xs-8 { + right: 66.66667%; +} + +.pull-xs-9 { + right: 75%; +} + +.pull-xs-10 { + right: 83.33333%; +} + +.pull-xs-11 { + right: 91.66667%; +} + +.pull-xs-12 { + right: 100%; +} + +.push-xs-0 { + left: auto; +} + +.push-xs-1 { + left: 8.33333%; +} + +.push-xs-2 { + left: 16.66667%; +} + +.push-xs-3 { + left: 25%; +} + +.push-xs-4 { + left: 33.33333%; +} + +.push-xs-5 { + left: 41.66667%; +} + +.push-xs-6 { + left: 50%; +} + +.push-xs-7 { + left: 58.33333%; +} + +.push-xs-8 { + left: 66.66667%; +} + +.push-xs-9 { + left: 75%; +} + +.push-xs-10 { + left: 83.33333%; +} + +.push-xs-11 { + left: 91.66667%; +} + +.push-xs-12 { + left: 100%; +} + +.offset-xs-1 { + margin-left: 8.33333%; +} + +.offset-xs-2 { + margin-left: 16.66667%; +} + +.offset-xs-3 { + margin-left: 25%; +} + +.offset-xs-4 { + margin-left: 33.33333%; +} + +.offset-xs-5 { + margin-left: 41.66667%; +} + +.offset-xs-6 { + margin-left: 50%; +} + +.offset-xs-7 { + margin-left: 58.33333%; +} + +.offset-xs-8 { + margin-left: 66.66667%; +} + +.offset-xs-9 { + margin-left: 75%; +} + +.offset-xs-10 { + margin-left: 83.33333%; +} + +.offset-xs-11 { + margin-left: 91.66667%; +} + +@media (min-width: 544px) { + .col-sm { + position: relative; + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + } + .col-sm-1 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 8.33333%; + max-width: 8.33333%; + } + .col-sm-2 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 16.66667%; + max-width: 16.66667%; + } + .col-sm-3 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 25%; + max-width: 25%; + } + .col-sm-4 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 33.33333%; + max-width: 33.33333%; + } + .col-sm-5 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 41.66667%; + max-width: 41.66667%; + } + .col-sm-6 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 50%; + max-width: 50%; + } + .col-sm-7 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 58.33333%; + max-width: 58.33333%; + } + .col-sm-8 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 66.66667%; + max-width: 66.66667%; + } + .col-sm-9 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 75%; + max-width: 75%; + } + .col-sm-10 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 83.33333%; + max-width: 83.33333%; + } + .col-sm-11 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 91.66667%; + max-width: 91.66667%; + } + .col-sm-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 100%; + max-width: 100%; + } + .pull-sm-0 { + right: auto; + } + .pull-sm-1 { + right: 8.33333%; + } + .pull-sm-2 { + right: 16.66667%; + } + .pull-sm-3 { + right: 25%; + } + .pull-sm-4 { + right: 33.33333%; + } + .pull-sm-5 { + right: 41.66667%; + } + .pull-sm-6 { + right: 50%; + } + .pull-sm-7 { + right: 58.33333%; + } + .pull-sm-8 { + right: 66.66667%; + } + .pull-sm-9 { + right: 75%; + } + .pull-sm-10 { + right: 83.33333%; + } + .pull-sm-11 { + right: 91.66667%; + } + .pull-sm-12 { + right: 100%; + } + .push-sm-0 { + left: auto; + } + .push-sm-1 { + left: 8.33333%; + } + .push-sm-2 { + left: 16.66667%; + } + .push-sm-3 { + left: 25%; + } + .push-sm-4 { + left: 33.33333%; + } + .push-sm-5 { + left: 41.66667%; + } + .push-sm-6 { + left: 50%; + } + .push-sm-7 { + left: 58.33333%; + } + .push-sm-8 { + left: 66.66667%; + } + .push-sm-9 { + left: 75%; + } + .push-sm-10 { + left: 83.33333%; + } + .push-sm-11 { + left: 91.66667%; + } + .push-sm-12 { + left: 100%; + } + .offset-sm-0 { + margin-left: 0%; + } + .offset-sm-1 { + margin-left: 8.33333%; + } + .offset-sm-2 { + margin-left: 16.66667%; + } + .offset-sm-3 { + margin-left: 25%; + } + .offset-sm-4 { + margin-left: 33.33333%; + } + .offset-sm-5 { + margin-left: 41.66667%; + } + .offset-sm-6 { + margin-left: 50%; + } + .offset-sm-7 { + margin-left: 58.33333%; + } + .offset-sm-8 { + margin-left: 66.66667%; + } + .offset-sm-9 { + margin-left: 75%; + } + .offset-sm-10 { + margin-left: 83.33333%; + } + .offset-sm-11 { + margin-left: 91.66667%; + } +} + +@media (min-width: 768px) { + .col-md { + position: relative; + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + } + .col-md-1 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 8.33333%; + max-width: 8.33333%; + } + .col-md-2 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 16.66667%; + max-width: 16.66667%; + } + .col-md-3 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 25%; + max-width: 25%; + } + .col-md-4 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 33.33333%; + max-width: 33.33333%; + } + .col-md-5 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 41.66667%; + max-width: 41.66667%; + } + .col-md-6 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 50%; + max-width: 50%; + } + .col-md-7 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 58.33333%; + max-width: 58.33333%; + } + .col-md-8 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 66.66667%; + max-width: 66.66667%; + } + .col-md-9 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 75%; + max-width: 75%; + } + .col-md-10 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 83.33333%; + max-width: 83.33333%; + } + .col-md-11 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 91.66667%; + max-width: 91.66667%; + } + .col-md-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 100%; + max-width: 100%; + } + .pull-md-0 { + right: auto; + } + .pull-md-1 { + right: 8.33333%; + } + .pull-md-2 { + right: 16.66667%; + } + .pull-md-3 { + right: 25%; + } + .pull-md-4 { + right: 33.33333%; + } + .pull-md-5 { + right: 41.66667%; + } + .pull-md-6 { + right: 50%; + } + .pull-md-7 { + right: 58.33333%; + } + .pull-md-8 { + right: 66.66667%; + } + .pull-md-9 { + right: 75%; + } + .pull-md-10 { + right: 83.33333%; + } + .pull-md-11 { + right: 91.66667%; + } + .pull-md-12 { + right: 100%; + } + .push-md-0 { + left: auto; + } + .push-md-1 { + left: 8.33333%; + } + .push-md-2 { + left: 16.66667%; + } + .push-md-3 { + left: 25%; + } + .push-md-4 { + left: 33.33333%; + } + .push-md-5 { + left: 41.66667%; + } + .push-md-6 { + left: 50%; + } + .push-md-7 { + left: 58.33333%; + } + .push-md-8 { + left: 66.66667%; + } + .push-md-9 { + left: 75%; + } + .push-md-10 { + left: 83.33333%; + } + .push-md-11 { + left: 91.66667%; + } + .push-md-12 { + left: 100%; + } + .offset-md-0 { + margin-left: 0%; + } + .offset-md-1 { + margin-left: 8.33333%; + } + .offset-md-2 { + margin-left: 16.66667%; + } + .offset-md-3 { + margin-left: 25%; + } + .offset-md-4 { + margin-left: 33.33333%; + } + .offset-md-5 { + margin-left: 41.66667%; + } + .offset-md-6 { + margin-left: 50%; + } + .offset-md-7 { + margin-left: 58.33333%; + } + .offset-md-8 { + margin-left: 66.66667%; + } + .offset-md-9 { + margin-left: 75%; + } + .offset-md-10 { + margin-left: 83.33333%; + } + .offset-md-11 { + margin-left: 91.66667%; + } +} + +@media (min-width: 992px) { + .col-lg { + position: relative; + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + } + .col-lg-1 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 8.33333%; + max-width: 8.33333%; + } + .col-lg-2 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 16.66667%; + max-width: 16.66667%; + } + .col-lg-3 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 25%; + max-width: 25%; + } + .col-lg-4 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 33.33333%; + max-width: 33.33333%; + } + .col-lg-5 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 41.66667%; + max-width: 41.66667%; + } + .col-lg-6 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 50%; + max-width: 50%; + } + .col-lg-7 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 58.33333%; + max-width: 58.33333%; + } + .col-lg-8 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 66.66667%; + max-width: 66.66667%; + } + .col-lg-9 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 75%; + max-width: 75%; + } + .col-lg-10 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 83.33333%; + max-width: 83.33333%; + } + .col-lg-11 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 91.66667%; + max-width: 91.66667%; + } + .col-lg-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 100%; + max-width: 100%; + } + .pull-lg-0 { + right: auto; + } + .pull-lg-1 { + right: 8.33333%; + } + .pull-lg-2 { + right: 16.66667%; + } + .pull-lg-3 { + right: 25%; + } + .pull-lg-4 { + right: 33.33333%; + } + .pull-lg-5 { + right: 41.66667%; + } + .pull-lg-6 { + right: 50%; + } + .pull-lg-7 { + right: 58.33333%; + } + .pull-lg-8 { + right: 66.66667%; + } + .pull-lg-9 { + right: 75%; + } + .pull-lg-10 { + right: 83.33333%; + } + .pull-lg-11 { + right: 91.66667%; + } + .pull-lg-12 { + right: 100%; + } + .push-lg-0 { + left: auto; + } + .push-lg-1 { + left: 8.33333%; + } + .push-lg-2 { + left: 16.66667%; + } + .push-lg-3 { + left: 25%; + } + .push-lg-4 { + left: 33.33333%; + } + .push-lg-5 { + left: 41.66667%; + } + .push-lg-6 { + left: 50%; + } + .push-lg-7 { + left: 58.33333%; + } + .push-lg-8 { + left: 66.66667%; + } + .push-lg-9 { + left: 75%; + } + .push-lg-10 { + left: 83.33333%; + } + .push-lg-11 { + left: 91.66667%; + } + .push-lg-12 { + left: 100%; + } + .offset-lg-0 { + margin-left: 0%; + } + .offset-lg-1 { + margin-left: 8.33333%; + } + .offset-lg-2 { + margin-left: 16.66667%; + } + .offset-lg-3 { + margin-left: 25%; + } + .offset-lg-4 { + margin-left: 33.33333%; + } + .offset-lg-5 { + margin-left: 41.66667%; + } + .offset-lg-6 { + margin-left: 50%; + } + .offset-lg-7 { + margin-left: 58.33333%; + } + .offset-lg-8 { + margin-left: 66.66667%; + } + .offset-lg-9 { + margin-left: 75%; + } + .offset-lg-10 { + margin-left: 83.33333%; + } + .offset-lg-11 { + margin-left: 91.66667%; + } +} + +@media (min-width: 1200px) { + .col-xl { + position: relative; + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + } + .col-xl-1 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 8.33333%; + max-width: 8.33333%; + } + .col-xl-2 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 16.66667%; + max-width: 16.66667%; + } + .col-xl-3 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 25%; + max-width: 25%; + } + .col-xl-4 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 33.33333%; + max-width: 33.33333%; + } + .col-xl-5 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 41.66667%; + max-width: 41.66667%; + } + .col-xl-6 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 50%; + max-width: 50%; + } + .col-xl-7 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 58.33333%; + max-width: 58.33333%; + } + .col-xl-8 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 66.66667%; + max-width: 66.66667%; + } + .col-xl-9 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 75%; + max-width: 75%; + } + .col-xl-10 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 83.33333%; + max-width: 83.33333%; + } + .col-xl-11 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 91.66667%; + max-width: 91.66667%; + } + .col-xl-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 100%; + max-width: 100%; + } + .pull-xl-0 { + right: auto; + } + .pull-xl-1 { + right: 8.33333%; + } + .pull-xl-2 { + right: 16.66667%; + } + .pull-xl-3 { + right: 25%; + } + .pull-xl-4 { + right: 33.33333%; + } + .pull-xl-5 { + right: 41.66667%; + } + .pull-xl-6 { + right: 50%; + } + .pull-xl-7 { + right: 58.33333%; + } + .pull-xl-8 { + right: 66.66667%; + } + .pull-xl-9 { + right: 75%; + } + .pull-xl-10 { + right: 83.33333%; + } + .pull-xl-11 { + right: 91.66667%; + } + .pull-xl-12 { + right: 100%; + } + .push-xl-0 { + left: auto; + } + .push-xl-1 { + left: 8.33333%; + } + .push-xl-2 { + left: 16.66667%; + } + .push-xl-3 { + left: 25%; + } + .push-xl-4 { + left: 33.33333%; + } + .push-xl-5 { + left: 41.66667%; + } + .push-xl-6 { + left: 50%; + } + .push-xl-7 { + left: 58.33333%; + } + .push-xl-8 { + left: 66.66667%; + } + .push-xl-9 { + left: 75%; + } + .push-xl-10 { + left: 83.33333%; + } + .push-xl-11 { + left: 91.66667%; + } + .push-xl-12 { + left: 100%; + } + .offset-xl-0 { + margin-left: 0%; + } + .offset-xl-1 { + margin-left: 8.33333%; + } + .offset-xl-2 { + margin-left: 16.66667%; + } + .offset-xl-3 { + margin-left: 25%; + } + .offset-xl-4 { + margin-left: 33.33333%; + } + .offset-xl-5 { + margin-left: 41.66667%; + } + .offset-xl-6 { + margin-left: 50%; + } + .offset-xl-7 { + margin-left: 58.33333%; + } + .offset-xl-8 { + margin-left: 66.66667%; + } + .offset-xl-9 { + margin-left: 75%; + } + .offset-xl-10 { + margin-left: 83.33333%; + } + .offset-xl-11 { + margin-left: 91.66667%; + } +} + +.table { + width: 100%; + max-width: 100%; + margin-bottom: 1rem; +} + +.table th, +.table td { + padding: 0.75rem; + vertical-align: top; + border-top: 1px solid #eceeef; +} + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid #eceeef; +} + +.table tbody + tbody { + border-top: 2px solid #eceeef; +} + +.table .table { + background-color: #211f21; +} + +.table-sm th, +.table-sm td { + padding: 0.3rem; +} + +.table-bordered { + border: 1px solid #eceeef; +} + +.table-bordered th, +.table-bordered td { + border: 1px solid #eceeef; +} + +.table-bordered thead th, +.table-bordered thead td { + border-bottom-width: 2px; +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: #f9f9f9; +} + +.table-hover tbody tr:hover { + background-color: #f5f5f5; +} + +.table-active, +.table-active > th, +.table-active > td { + background-color: #f5f5f5; +} + +.table-hover .table-active:hover { + background-color: #e8e8e8; +} + +.table-hover .table-active:hover > td, +.table-hover .table-active:hover > th { + background-color: #e8e8e8; +} + +.table-success, +.table-success > th, +.table-success > td { + background-color: #dff0d8; +} + +.table-hover .table-success:hover { + background-color: #d0e9c6; +} + +.table-hover .table-success:hover > td, +.table-hover .table-success:hover > th { + background-color: #d0e9c6; +} + +.table-info, +.table-info > th, +.table-info > td { + background-color: #d9edf7; +} + +.table-hover .table-info:hover { + background-color: #c4e3f3; +} + +.table-hover .table-info:hover > td, +.table-hover .table-info:hover > th { + background-color: #c4e3f3; +} + +.table-warning, +.table-warning > th, +.table-warning > td { + background-color: #fcf8e3; +} + +.table-hover .table-warning:hover { + background-color: #faf2cc; +} + +.table-hover .table-warning:hover > td, +.table-hover .table-warning:hover > th { + background-color: #faf2cc; +} + +.table-danger, +.table-danger > th, +.table-danger > td { + background-color: #f2dede; +} + +.table-hover .table-danger:hover { + background-color: #ebcccc; +} + +.table-hover .table-danger:hover > td, +.table-hover .table-danger:hover > th { + background-color: #ebcccc; +} + +.table-responsive { + display: block; + width: 100%; + min-height: 0.01%; + overflow-x: auto; +} + +.thead-inverse th { + color: #fff; + background-color: #fff; +} + +.thead-default th { + color: #55595c; + background-color: #eceeef; +} + +.table-inverse { + color: #eceeef; + background-color: #fff; +} + +.table-inverse.table-bordered { + border: 0; +} + +.table-inverse th, +.table-inverse td, +.table-inverse thead th { + border-color: #55595c; +} + +.table-reflow thead { + float: left; +} + +.table-reflow tbody { + display: block; + white-space: nowrap; +} + +.table-reflow th, +.table-reflow td { + border-top: 1px solid #eceeef; + border-left: 1px solid #eceeef; +} + +.table-reflow th:last-child, +.table-reflow td:last-child { + border-right: 1px solid #eceeef; +} + +.table-reflow thead:last-child tr:last-child th, +.table-reflow thead:last-child tr:last-child td, +.table-reflow tbody:last-child tr:last-child th, +.table-reflow tbody:last-child tr:last-child td, +.table-reflow tfoot:last-child tr:last-child th, +.table-reflow tfoot:last-child tr:last-child td { + border-bottom: 1px solid #eceeef; +} + +.table-reflow tr { + float: left; +} + +.table-reflow tr th, +.table-reflow tr td { + display: block !important; + border: 1px solid #eceeef; +} + +.form-control { + display: block; + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 1rem; + line-height: 1.25; + background-color: #333; + background-image: none; + background-clip: padding-box; + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 0.25rem; +} + +.form-control::-ms-expand { + background-color: transparent; + border: 0; +} + +.form-control:focus { + border-color: #66afe9; + outline: none; +} + +.form-control::placeholder { + color: #999; + opacity: 1; +} + +.form-control:disabled, +.form-control[readonly] { + background-color: #444; + opacity: 1; +} + +.form-control:disabled { + cursor: not-allowed; +} + +select.form-control:not([size]):not([multiple]) { + height: 2.5rem; +} + +.form-control-file, +.form-control-range { + display: block; +} + +.form-control-label { + padding: 0.5rem 0.75rem; + margin-bottom: 0; +} + +.form-control-legend { + padding: 0.5rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; +} + +_::-webkit-full-page-media.form-control, +input[type='date'].form-control, +input[type='time'].form-control, +input[type='datetime-local'].form-control, +input[type='month'].form-control { + line-height: 2.5rem; +} + +_::-webkit-full-page-media.input-sm, +.input-group-sm _::-webkit-full-page-media.form-control, +input[type='date'].input-sm, +.input-group-sm input[type='date'].form-control, +input[type='time'].input-sm, +.input-group-sm input[type='time'].form-control, +input[type='datetime-local'].input-sm, +.input-group-sm input[type='datetime-local'].form-control, +input[type='month'].input-sm, +.input-group-sm input[type='month'].form-control { + line-height: 1.8125rem; +} + +_::-webkit-full-page-media.input-lg, +.input-group-lg _::-webkit-full-page-media.form-control, +input[type='date'].input-lg, +.input-group-lg input[type='date'].form-control, +input[type='time'].input-lg, +.input-group-lg input[type='time'].form-control, +input[type='datetime-local'].input-lg, +.input-group-lg input[type='datetime-local'].form-control, +input[type='month'].input-lg, +.input-group-lg input[type='month'].form-control { + line-height: 3.16667rem; +} + +.form-control-static { + min-height: 2.5rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + margin-bottom: 0; +} + +.form-control-static.form-control-sm, +.input-group-sm > .form-control-static.form-control, +.input-group-sm > .form-control-static.input-group-addon, +.input-group-sm > .input-group-btn > .form-control-static.btn, +.form-control-static.form-control-lg, +.input-group-lg > .form-control-static.form-control, +.input-group-lg > .form-control-static.input-group-addon, +.input-group-lg > .input-group-btn > .form-control-static.btn { + padding-right: 0; + padding-left: 0; +} + +.form-control-sm, +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 0.2rem; +} + +.form-control-lg, +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; + border-radius: 0.3rem; +} + +.form-group { + margin-bottom: 1rem; +} + +.radio, +.checkbox { + position: relative; + display: block; + margin-bottom: 0.75rem; +} + +.radio label, +.checkbox label { + padding-left: 1.25rem; + margin-bottom: 0; + cursor: pointer; +} + +.radio label input:only-child, +.checkbox label input:only-child { + position: static; +} + +.radio input[type='radio'], +.radio-inline input[type='radio'], +.checkbox input[type='checkbox'], +.checkbox-inline input[type='checkbox'] { + position: absolute; + margin-top: 0.25rem; + margin-left: -1.25rem; +} + +.radio + .radio, +.checkbox + .checkbox { + margin-top: -0.25rem; +} + +.radio-inline, +.checkbox-inline { + position: relative; + display: inline-block; + padding-left: 1.25rem; + margin-bottom: 0; + vertical-align: middle; + cursor: pointer; +} + +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 0.75rem; +} + +input[type='radio']:disabled, +input[type='radio'].disabled, +input[type='checkbox']:disabled, +input[type='checkbox'].disabled { + cursor: not-allowed; +} + +.radio-inline.disabled, +.checkbox-inline.disabled { + cursor: not-allowed; +} + +.radio.disabled label, +.checkbox.disabled label { + cursor: not-allowed; +} + +.form-control-success, +.form-control-warning, +.form-control-danger { + padding-right: 2.25rem; + background-repeat: no-repeat; + background-position: center right 0.625rem; + background-size: 1.25rem 1.25rem; +} + +.has-success .text-help, +.has-success .form-control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline, +.has-success.radio label, +.has-success.checkbox label, +.has-success.radio-inline label, +.has-success.checkbox-inline label, +.has-success .custom-control { + color: #ec8cb5; +} + +.has-success .form-control { + border-color: #ec8cb5; +} + +.has-success .input-group-addon { + color: #ec8cb5; + border-color: #ec8cb5; + background-color: #eaf6ea; +} + +.has-success .form-control-feedback { + color: #ec8cb5; +} + +.has-success .form-control-success { + background-image: url(data:image/svg+xml;charset=utf8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA4IDgnPjxwYXRoIGZpbGw9JyM1Y2I4NWMnIGQ9J00yLjMgNi43M0wuNiA0LjUzYy0uNC0xLjA0LjQ2LTEuNCAxLjEtLjhsMS4xIDEuNCAzLjQtMy44Yy42LS42MyAxLjYtLjI3IDEuMi43bC00IDQuNmMtLjQzLjUtLjguNC0xLjEuMXonLz48L3N2Zz4=); +} + +.has-warning .text-help, +.has-warning .form-control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline, +.has-warning.radio label, +.has-warning.checkbox label, +.has-warning.radio-inline label, +.has-warning.checkbox-inline label, +.has-warning .custom-control { + color: #f0ad4e; +} + +.has-warning .form-control { + border-color: #f0ad4e; +} + +.has-warning .input-group-addon { + color: #f0ad4e; + border-color: #f0ad4e; + background-color: #211f21; +} + +.has-warning .form-control-feedback { + color: #f0ad4e; +} + +.has-warning .form-control-warning { + background-image: url(data:image/svg+xml;charset=utf8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA4IDgnPjxwYXRoIGZpbGw9JyNmMGFkNGUnIGQ9J000LjQgNS4zMjRoLS44di0yLjQ2aC44em0wIDEuNDJoLS44VjUuODloLjh6TTMuNzYuNjNMLjA0IDcuMDc1Yy0uMTE1LjIuMDE2LjQyNS4yNi40MjZoNy4zOTdjLjI0MiAwIC4zNzItLjIyNi4yNTgtLjQyNkM2LjcyNiA0LjkyNCA1LjQ3IDIuNzkgNC4yNTMuNjNjLS4xMTMtLjE3NC0uMzktLjE3NC0uNDk0IDB6Jy8+PC9zdmc+); +} + +.has-danger .text-help, +.has-danger .form-control-label, +.has-danger .radio, +.has-danger .checkbox, +.has-danger .radio-inline, +.has-danger .checkbox-inline, +.has-danger.radio label, +.has-danger.checkbox label, +.has-danger.radio-inline label, +.has-danger.checkbox-inline label, +.has-danger .custom-control { + color: #b85c5c; +} + +.has-danger .form-control { + border-color: #b85c5c; +} + +.has-danger .input-group-addon { + color: #b85c5c; + border-color: #b85c5c; + background-color: #f6eaea; +} + +.has-danger .form-control-feedback { + color: #b85c5c; +} + +.has-danger .form-control-danger { + background-image: url(data:image/svg+xml;charset=utf8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIGZpbGw9JyNkOTUzNGYnIHZpZXdCb3g9Jy0yIC0yIDcgNyc+PHBhdGggc3Ryb2tlPScjZDk1MzRmJyBkPSdNMCAwbDMgM20wLTNMMCAzJy8+PGNpcmNsZSByPScuNScvPjxjaXJjbGUgY3g9JzMnIHI9Jy41Jy8+PGNpcmNsZSBjeT0nMycgcj0nLjUnLz48Y2lyY2xlIGN4PSczJyBjeT0nMycgcj0nLjUnLz48L3N2Zz4=); +} + +@media (min-width: 544px) { + .form-inline .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-static { + display: inline-block; + } + .form-inline .input-group { + display: inline-table; + vertical-align: middle; + } + .form-inline .input-group .input-group-addon, + .form-inline .input-group .input-group-btn, + .form-inline .input-group .form-control { + width: auto; + } + .form-inline .input-group > .form-control { + width: 100%; + } + .form-inline .form-control-label { + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio, + .form-inline .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio label, + .form-inline .checkbox label { + padding-left: 0; + } + .form-inline .radio input[type='radio'], + .form-inline .checkbox input[type='checkbox'] { + position: relative; + margin-left: 0; + } + .form-inline .has-feedback .form-control-feedback { + top: 0; + } +} + +.btn { + display: inline-block; + font-weight: 400; + line-height: 1.25; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + user-select: none; + border: 1px solid transparent; + padding: 0.5rem 1rem; + font-size: 1rem; + border-radius: 0.25rem; +} + +.btn:focus, +.btn.focus, +.btn:active:focus, +.btn:active.focus, +.btn.active:focus, +.btn.active.focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn:focus, +.btn:hover { + text-decoration: none; +} + +.btn.focus { + text-decoration: none; +} + +.btn:active, +.btn.active { + background-image: none; + outline: 0; +} + +.btn.disabled, +.btn:disabled { + cursor: not-allowed; + opacity: 0.65; +} + +a.btn.disabled, +fieldset[disabled] a.btn { + pointer-events: none; +} + +.btn-primary { + color: #fff; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-primary:hover { + color: #fff; + background-color: #7a8992; + border-color: #8ea1ac; +} + +.btn-primary:focus, +.btn-primary.focus { + color: #fff; + background-color: #7a8992; + border-color: #8ea1ac; +} + +.btn-primary:active, +.btn-primary.active, +.open > .btn-primary.dropdown-toggle { + color: #fff; + background-color: #7a8992; + border-color: #8ea1ac; + background-image: none; +} + +.btn-primary:active:hover, +.btn-primary:active:focus, +.btn-primary:active.focus, +.btn-primary.active:hover, +.btn-primary.active:focus, +.btn-primary.active.focus, +.open > .btn-primary.dropdown-toggle:hover, +.open > .btn-primary.dropdown-toggle:focus, +.open > .btn-primary.dropdown-toggle.focus { + color: #fff; + background-color: #ec8cb56d; + border-color: #2d672d; +} + +.btn-primary.disabled:focus, +.btn-primary.disabled.focus, +.btn-primary:disabled:focus, +.btn-primary:disabled.focus { + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-primary.disabled:hover, +.btn-primary:disabled:hover { + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-secondary { + color: #fff; + background-color: #211f21; + border-color: #ccc; +} + +.btn-secondary:hover { + color: #fff; + background-color: #e6e6e6; + border-color: #adadad; +} + +.btn-secondary:focus, +.btn-secondary.focus { + color: #fff; + background-color: #e6e6e6; + border-color: #adadad; +} + +.btn-secondary:active, +.btn-secondary.active, +.open > .btn-secondary.dropdown-toggle { + color: #fff; + background-color: #e6e6e6; + border-color: #adadad; + background-image: none; +} + +.btn-secondary:active:hover, +.btn-secondary:active:focus, +.btn-secondary:active.focus, +.btn-secondary.active:hover, +.btn-secondary.active:focus, +.btn-secondary.active.focus, +.open > .btn-secondary.dropdown-toggle:hover, +.open > .btn-secondary.dropdown-toggle:focus, +.open > .btn-secondary.dropdown-toggle.focus { + color: #fff; + background-color: #d4d4d4; + border-color: #8c8c8c; +} + +.btn-secondary.disabled:focus, +.btn-secondary.disabled.focus, +.btn-secondary:disabled:focus, +.btn-secondary:disabled.focus { + background-color: #211f21; + border-color: #ccc; +} + +.btn-secondary.disabled:hover, +.btn-secondary:disabled:hover { + background-color: #211f21; + border-color: #ccc; +} + +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #5bc0de; +} + +.btn-info:hover { + color: #fff; + background-color: #31b0d5; + border-color: #2aabd2; +} + +.btn-info:focus, +.btn-info.focus { + color: #fff; + background-color: #31b0d5; + border-color: #2aabd2; +} + +.btn-info:active, +.btn-info.active, +.open > .btn-info.dropdown-toggle { + color: #fff; + background-color: #31b0d5; + border-color: #2aabd2; + background-image: none; +} + +.btn-info:active:hover, +.btn-info:active:focus, +.btn-info:active.focus, +.btn-info.active:hover, +.btn-info.active:focus, +.btn-info.active.focus, +.open > .btn-info.dropdown-toggle:hover, +.open > .btn-info.dropdown-toggle:focus, +.open > .btn-info.dropdown-toggle.focus { + color: #fff; + background-color: #269abc; + border-color: #1f7e9a; +} + +.btn-info.disabled:focus, +.btn-info.disabled.focus, +.btn-info:disabled:focus, +.btn-info:disabled.focus { + background-color: #5bc0de; + border-color: #5bc0de; +} + +.btn-info.disabled:hover, +.btn-info:disabled:hover { + background-color: #5bc0de; + border-color: #5bc0de; +} + +.btn-success { + color: #fff; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-success:hover { + color: #fff; + background-color: #7a8992; + border-color: #8ea1ac; +} + +.btn-success:focus, +.btn-success.focus { + color: #fff; + background-color: #7a8992; + border-color: #8ea1ac; +} + +.btn-success:active, +.btn-success.active, +.open > .btn-success.dropdown-toggle { + color: #fff; + background-color: #7a8992; + border-color: #8ea1ac; + background-image: none; +} + +.btn-success:active:hover, +.btn-success:active:focus, +.btn-success:active.focus, +.btn-success.active:hover, +.btn-success.active:focus, +.btn-success.active.focus, +.open > .btn-success.dropdown-toggle:hover, +.open > .btn-success.dropdown-toggle:focus, +.open > .btn-success.dropdown-toggle.focus { + color: #fff; + background-color: #ec8cb56d; + border-color: #2d672d; +} + +.btn-success.disabled:focus, +.btn-success.disabled.focus, +.btn-success:disabled:focus, +.btn-success:disabled.focus { + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-success.disabled:hover, +.btn-success:disabled:hover { + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-warning { + color: #fff; + background-color: #f0ad4e; + border-color: #f0ad4e; +} + +.btn-warning:hover { + color: #fff; + background-color: #ec971f; + border-color: #eb9316; +} + +.btn-warning:focus, +.btn-warning.focus { + color: #fff; + background-color: #ec971f; + border-color: #eb9316; +} + +.btn-warning:active, +.btn-warning.active, +.open > .btn-warning.dropdown-toggle { + color: #fff; + background-color: #ec971f; + border-color: #eb9316; + background-image: none; +} + +.btn-warning:active:hover, +.btn-warning:active:focus, +.btn-warning:active.focus, +.btn-warning.active:hover, +.btn-warning.active:focus, +.btn-warning.active.focus, +.open > .btn-warning.dropdown-toggle:hover, +.open > .btn-warning.dropdown-toggle:focus, +.open > .btn-warning.dropdown-toggle.focus { + color: #fff; + background-color: #d58512; + border-color: #b06d0f; +} + +.btn-warning.disabled:focus, +.btn-warning.disabled.focus, +.btn-warning:disabled:focus, +.btn-warning:disabled.focus { + background-color: #f0ad4e; + border-color: #f0ad4e; +} + +.btn-warning.disabled:hover, +.btn-warning:disabled:hover { + background-color: #f0ad4e; + border-color: #f0ad4e; +} + +.btn-danger { + color: #fff; + background-color: #b85c5c; + border-color: #b85c5c; +} + +.btn-danger:hover { + color: #fff; + background-color: #9d4444; + border-color: #964141; +} + +.btn-danger:focus, +.btn-danger.focus { + color: #fff; + background-color: #9d4444; + border-color: #964141; +} + +.btn-danger:active, +.btn-danger.active, +.open > .btn-danger.dropdown-toggle { + color: #fff; + background-color: #9d4444; + border-color: #964141; + background-image: none; +} + +.btn-danger:active:hover, +.btn-danger:active:focus, +.btn-danger:active.focus, +.btn-danger.active:hover, +.btn-danger.active:focus, +.btn-danger.active.focus, +.open > .btn-danger.dropdown-toggle:hover, +.open > .btn-danger.dropdown-toggle:focus, +.open > .btn-danger.dropdown-toggle.focus { + color: #fff; + background-color: #843939; + border-color: #672d2d; +} + +.btn-danger.disabled:focus, +.btn-danger.disabled.focus, +.btn-danger:disabled:focus, +.btn-danger:disabled.focus { + background-color: #b85c5c; + border-color: #b85c5c; +} + +.btn-danger.disabled:hover, +.btn-danger:disabled:hover { + background-color: #b85c5c; + border-color: #b85c5c; +} + +.btn-outline-primary { + color: #ec8cb5; + background-image: none; + background-color: transparent; + border-color: #ec8cb5; +} + +.btn-outline-primary:hover { + color: #fff; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-outline-primary:focus, +.btn-outline-primary.focus { + color: #fff; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-outline-primary:active, +.btn-outline-primary.active, +.open > .btn-outline-primary.dropdown-toggle { + color: #fff; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-outline-primary:active:hover, +.btn-outline-primary:active:focus, +.btn-outline-primary:active.focus, +.btn-outline-primary.active:hover, +.btn-outline-primary.active:focus, +.btn-outline-primary.active.focus, +.open > .btn-outline-primary.dropdown-toggle:hover, +.open > .btn-outline-primary.dropdown-toggle:focus, +.open > .btn-outline-primary.dropdown-toggle.focus { + color: #fff; + background-color: #ec8cb56d; + border-color: #2d672d; +} + +.btn-outline-primary.disabled:focus, +.btn-outline-primary.disabled.focus, +.btn-outline-primary:disabled:focus, +.btn-outline-primary:disabled.focus { + border-color: #a3d7a3; +} + +.btn-outline-primary.disabled:hover, +.btn-outline-primary:disabled:hover { + border-color: #a3d7a3; +} + +.btn-outline-secondary { + color: #ccc; + background-image: none; + background-color: transparent; + border-color: #ccc; +} + +.btn-outline-secondary:hover { + color: #fff; + background-color: #ccc; + border-color: #ccc; +} + +.btn-outline-secondary:focus, +.btn-outline-secondary.focus { + color: #fff; + background-color: #ccc; + border-color: #ccc; +} + +.btn-outline-secondary:active, +.btn-outline-secondary.active, +.open > .btn-outline-secondary.dropdown-toggle { + color: #fff; + background-color: #ccc; + border-color: #ccc; +} + +.btn-outline-secondary:active:hover, +.btn-outline-secondary:active:focus, +.btn-outline-secondary:active.focus, +.btn-outline-secondary.active:hover, +.btn-outline-secondary.active:focus, +.btn-outline-secondary.active.focus, +.open > .btn-outline-secondary.dropdown-toggle:hover, +.open > .btn-outline-secondary.dropdown-toggle:focus, +.open > .btn-outline-secondary.dropdown-toggle.focus { + color: #fff; + background-color: #a1a1a1; + border-color: #8c8c8c; +} + +.btn-outline-secondary.disabled:focus, +.btn-outline-secondary.disabled.focus, +.btn-outline-secondary:disabled:focus, +.btn-outline-secondary:disabled.focus { + border-color: #fff; +} + +.btn-outline-secondary.disabled:hover, +.btn-outline-secondary:disabled:hover { + border-color: #fff; +} + +.btn-outline-info { + color: #5bc0de; + background-image: none; + background-color: transparent; + border-color: #5bc0de; +} + +.btn-outline-info:hover { + color: #fff; + background-color: #5bc0de; + border-color: #5bc0de; +} + +.btn-outline-info:focus, +.btn-outline-info.focus { + color: #fff; + background-color: #5bc0de; + border-color: #5bc0de; +} + +.btn-outline-info:active, +.btn-outline-info.active, +.open > .btn-outline-info.dropdown-toggle { + color: #fff; + background-color: #5bc0de; + border-color: #5bc0de; +} + +.btn-outline-info:active:hover, +.btn-outline-info:active:focus, +.btn-outline-info:active.focus, +.btn-outline-info.active:hover, +.btn-outline-info.active:focus, +.btn-outline-info.active.focus, +.open > .btn-outline-info.dropdown-toggle:hover, +.open > .btn-outline-info.dropdown-toggle:focus, +.open > .btn-outline-info.dropdown-toggle.focus { + color: #fff; + background-color: #269abc; + border-color: #1f7e9a; +} + +.btn-outline-info.disabled:focus, +.btn-outline-info.disabled.focus, +.btn-outline-info:disabled:focus, +.btn-outline-info:disabled.focus { + border-color: #b0e1ef; +} + +.btn-outline-info.disabled:hover, +.btn-outline-info:disabled:hover { + border-color: #b0e1ef; +} + +.btn-outline-success { + color: #ec8cb5; + background-image: none; + background-color: transparent; + border-color: #ec8cb5; +} + +.btn-outline-success:hover { + color: #fff; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-outline-success:focus, +.btn-outline-success.focus { + color: #fff; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-outline-success:active, +.btn-outline-success.active, +.open > .btn-outline-success.dropdown-toggle { + color: #fff; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-outline-success:active:hover, +.btn-outline-success:active:focus, +.btn-outline-success:active.focus, +.btn-outline-success.active:hover, +.btn-outline-success.active:focus, +.btn-outline-success.active.focus, +.open > .btn-outline-success.dropdown-toggle:hover, +.open > .btn-outline-success.dropdown-toggle:focus, +.open > .btn-outline-success.dropdown-toggle.focus { + color: #fff; + background-color: #ec8cb56d; + border-color: #2d672d; +} + +.btn-outline-success.disabled:focus, +.btn-outline-success.disabled.focus, +.btn-outline-success:disabled:focus, +.btn-outline-success:disabled.focus { + border-color: #a3d7a3; +} + +.btn-outline-success.disabled:hover, +.btn-outline-success:disabled:hover { + border-color: #a3d7a3; +} + +.btn-outline-warning { + color: #f0ad4e; + background-image: none; + background-color: transparent; + border-color: #f0ad4e; +} + +.btn-outline-warning:hover { + color: #fff; + background-color: #f0ad4e; + border-color: #f0ad4e; +} + +.btn-outline-warning:focus, +.btn-outline-warning.focus { + color: #fff; + background-color: #f0ad4e; + border-color: #f0ad4e; +} + +.btn-outline-warning:active, +.btn-outline-warning.active, +.open > .btn-outline-warning.dropdown-toggle { + color: #fff; + background-color: #f0ad4e; + border-color: #f0ad4e; +} + +.btn-outline-warning:active:hover, +.btn-outline-warning:active:focus, +.btn-outline-warning:active.focus, +.btn-outline-warning.active:hover, +.btn-outline-warning.active:focus, +.btn-outline-warning.active.focus, +.open > .btn-outline-warning.dropdown-toggle:hover, +.open > .btn-outline-warning.dropdown-toggle:focus, +.open > .btn-outline-warning.dropdown-toggle.focus { + color: #fff; + background-color: #d58512; + border-color: #b06d0f; +} + +.btn-outline-warning.disabled:focus, +.btn-outline-warning.disabled.focus, +.btn-outline-warning:disabled:focus, +.btn-outline-warning:disabled.focus { + border-color: #f8d9ac; +} + +.btn-outline-warning.disabled:hover, +.btn-outline-warning:disabled:hover { + border-color: #f8d9ac; +} + +.btn-outline-danger { + color: #b85c5c; + background-image: none; + background-color: transparent; + border-color: #b85c5c; +} + +.btn-outline-danger:hover { + color: #fff; + background-color: #b85c5c; + border-color: #b85c5c; +} + +.btn-outline-danger:focus, +.btn-outline-danger.focus { + color: #fff; + background-color: #b85c5c; + border-color: #b85c5c; +} + +.btn-outline-danger:active, +.btn-outline-danger.active, +.open > .btn-outline-danger.dropdown-toggle { + color: #fff; + background-color: #b85c5c; + border-color: #b85c5c; +} + +.btn-outline-danger:active:hover, +.btn-outline-danger:active:focus, +.btn-outline-danger:active.focus, +.btn-outline-danger.active:hover, +.btn-outline-danger.active:focus, +.btn-outline-danger.active.focus, +.open > .btn-outline-danger.dropdown-toggle:hover, +.open > .btn-outline-danger.dropdown-toggle:focus, +.open > .btn-outline-danger.dropdown-toggle.focus { + color: #fff; + background-color: #843939; + border-color: #672d2d; +} + +.btn-outline-danger.disabled:focus, +.btn-outline-danger.disabled.focus, +.btn-outline-danger:disabled:focus, +.btn-outline-danger:disabled.focus { + border-color: #d7a3a3; +} + +.btn-outline-danger.disabled:hover, +.btn-outline-danger:disabled:hover { + border-color: #d7a3a3; +} + +.btn-link { + font-weight: 400; + color: #ec8cb5; + border-radius: 0; +} + +.btn-link, +.btn-link:active, +.btn-link.active, +.btn-link:disabled { + background-color: transparent; +} + +.btn-link, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} + +.btn-link:hover { + border-color: transparent; +} + +.btn-link:focus, +.btn-link:hover { + color: #3d8b3d; + text-decoration: underline; + background-color: transparent; +} + +.btn-link:disabled:focus, +.btn-link:disabled:hover { + color: #818a91; + text-decoration: none; +} + +.btn-lg, +.btn-group-lg > .btn { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; + border-radius: 0.3rem; +} + +.btn-sm, +.btn-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 0.2rem; +} + +.btn-block { + display: block; + width: 100%; +} + +.btn-block + .btn-block { + margin-top: 5px; +} + +input[type='submit'].btn-block, +input[type='reset'].btn-block, +input[type='button'].btn-block { + width: 100%; +} + +.fade { + opacity: 0; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.collapse { + display: none; +} + +.collapse.in { + display: block; +} + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + transition-timing-function: ease; + transition-duration: 0.35s; + transition-property: height; +} + +.dropup, +.dropdown { + position: relative; +} + +.dropdown-toggle::after { + display: inline-block; + width: 0; + height: 0; + margin-right: 0.25rem; + margin-left: 0.25rem; + vertical-align: middle; + content: ''; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-left: 0.3em solid transparent; +} + +.dropdown-toggle:focus { + outline: 0; +} + +.dropup .dropdown-toggle::after { + border-top: 0; + border-bottom: 0.3em solid; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 1rem; + color: #fff; + text-align: left; + list-style: none; + background-color: #211f21; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; +} + +.dropdown-divider { + height: 1px; + margin: 0.5rem 0; + overflow: hidden; + background-color: #e5e5e5; +} + +.dropdown-item { + display: block; + width: 100%; + padding: 3px 20px; + clear: both; + font-weight: 400; + color: #fff; + text-align: inherit; + white-space: nowrap; + background: 0 0; + border: 0; +} + +.dropdown-item:focus, +.dropdown-item:hover { + color: #2b2d2f; + text-decoration: none; + background-color: #f5f5f5; +} + +.dropdown-item.active, +.dropdown-item.active:focus, +.dropdown-item.active:hover { + color: #fff; + text-decoration: none; + background-color: #ec8cb5; + outline: 0; +} + +.dropdown-item.disabled, +.dropdown-item.disabled:focus, +.dropdown-item.disabled:hover { + color: #818a91; +} + +.dropdown-item.disabled:focus, +.dropdown-item.disabled:hover { + text-decoration: none; + cursor: not-allowed; + background-color: transparent; + background-image: none; + filter: 'progid:DXImageTransform.Microsoft.gradient(enabled = false)'; +} + +.open > .dropdown-menu { + display: block; +} + +.open > a { + outline: 0; +} + +.dropdown-menu-right { + right: 0; + left: auto; +} + +.dropdown-menu-left { + right: auto; + left: 0; +} + +.dropdown-header { + display: block; + padding: 5px 20px; + font-size: 0.875rem; + color: #818a91; + white-space: nowrap; +} + +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + content: ''; + border-top: 0; + border-bottom: 0.3em solid; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 2px; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} + +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} + +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn:focus, +.btn-group-vertical > .btn:active, +.btn-group-vertical > .btn.active { + z-index: 2; +} + +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover { + z-index: 2; +} + +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} + +.btn-toolbar { + margin-left: -5px; +} + +.btn-toolbar::after { + content: ''; + display: table; + clear: both; +} + +.btn-toolbar .btn-group, +.btn-toolbar .input-group { + float: left; +} + +.btn-toolbar > .btn, +.btn-toolbar > .btn-group, +.btn-toolbar > .input-group { + margin-left: 5px; +} + +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} + +.btn-group > .btn:first-child { + margin-left: 0; +} + +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} + +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.btn-group > .btn-group { + float: left; +} + +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} + +.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} + +.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group > .btn + .dropdown-toggle { + padding-right: 8px; + padding-left: 8px; +} + +.btn-group > .btn-lg + .dropdown-toggle, +.btn-group-lg.btn-group > .btn + .dropdown-toggle { + padding-right: 12px; + padding-left: 12px; +} + +.btn .caret { + margin-left: 0; +} + +.btn-lg .caret, +.btn-group-lg > .btn .caret { + border-width: 0.3em 0.3em 0; + border-bottom-width: 0; +} + +.dropup .btn-lg .caret, +.dropup .btn-group-lg > .btn .caret { + border-width: 0 0.3em 0.3em; +} + +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; +} + +.btn-group-vertical > .btn-group::after { + content: ''; + display: table; + clear: both; +} + +.btn-group-vertical > .btn-group > .btn { + float: none; +} + +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} + +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} + +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical + > .btn-group:first-child:not(:last-child) + > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical + > .btn-group:last-child:not(:first-child) + > .btn:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +[data-toggle='buttons'] > .btn input[type='radio'], +[data-toggle='buttons'] > .btn input[type='checkbox'], +[data-toggle='buttons'] > .btn-group > .btn input[type='radio'], +[data-toggle='buttons'] > .btn-group > .btn input[type='checkbox'] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} + +.input-group { + position: relative; + width: 100%; + display: flex; +} + +.input-group .form-control { + position: relative; + z-index: 2; + flex: 1; + margin-bottom: 0; +} + +.input-group .form-control:focus, +.input-group .form-control:active, +.input-group .form-control:hover { + z-index: 3; +} + +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} + +.input-group-addon, +.input-group-btn { + white-space: nowrap; + vertical-align: middle; +} + +.input-group-addon { + padding: 0.5rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; + font-weight: 400; + line-height: 1.25; + color: #55595c; + text-align: center; + background-color: #eceeef; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; +} + +.input-group-addon.form-control-sm, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .input-group-addon.btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 0.2rem; +} + +.input-group-addon.form-control-lg, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .input-group-addon.btn { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; + border-radius: 0.3rem; +} + +.input-group-addon input[type='radio'], +.input-group-addon input[type='checkbox'] { + margin-top: 0; +} + +.input-group .form-control:not(:last-child), +.input-group-addon:not(:last-child), +.input-group-btn:not(:last-child) > .btn, +.input-group-btn:not(:last-child) > .btn-group > .btn, +.input-group-btn:not(:last-child) > .dropdown-toggle, +.input-group-btn:not(:first-child) + > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:not(:first-child) > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} + +.input-group-addon:not(:last-child) { + border-right: 0; +} + +.input-group .form-control:not(:first-child), +.input-group-addon:not(:first-child), +.input-group-btn:not(:first-child) > .btn, +.input-group-btn:not(:first-child) > .btn-group > .btn, +.input-group-btn:not(:first-child) > .dropdown-toggle, +.input-group-btn:not(:last-child) > .btn:not(:first-child), +.input-group-btn:not(:last-child) > .btn-group:not(:first-child) > .btn { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.form-control + .input-group-addon:not(:first-child) { + border-left: 0; +} + +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; +} + +.input-group-btn > .btn { + position: relative; +} + +.input-group-btn > .btn + .btn { + margin-left: -1px; +} + +.input-group-btn > .btn:focus, +.input-group-btn > .btn:active, +.input-group-btn > .btn:hover { + z-index: 3; +} + +.input-group-btn:not(:last-child) > .btn, +.input-group-btn:not(:last-child) > .btn-group { + margin-right: -1px; +} + +.input-group-btn:not(:first-child) > .btn, +.input-group-btn:not(:first-child) > .btn-group { + z-index: 2; + margin-left: -1px; +} + +.input-group-btn:not(:first-child) > .btn:focus, +.input-group-btn:not(:first-child) > .btn:active, +.input-group-btn:not(:first-child) > .btn:hover, +.input-group-btn:not(:first-child) > .btn-group:focus, +.input-group-btn:not(:first-child) > .btn-group:active, +.input-group-btn:not(:first-child) > .btn-group:hover { + z-index: 3; +} + +.custom-control { + position: relative; + display: inline; + padding-left: 1.5rem; + cursor: pointer; +} + +.custom-control + .custom-control { + margin-left: 1rem; +} + +.custom-control-input { + position: absolute; + z-index: -1; + opacity: 0; +} + +.custom-control-input:checked ~ .custom-control-indicator { + color: #fff; + background-color: #0074d9; +} + +.custom-control-input:focus ~ .custom-control-indicator { + box-shadow: 0 0 0 0.075rem #fff, 0 0 0 0.2rem #0074d9; +} + +.custom-control-input:active ~ .custom-control-indicator { + color: #fff; + background-color: #84c6ff; +} + +.custom-control-input:disabled ~ .custom-control-indicator { + cursor: not-allowed; + background-color: #eee; +} + +.custom-control-input:disabled ~ .custom-control-description { + color: #767676; + cursor: not-allowed; +} + +.custom-control-indicator { + position: absolute; + top: 0.0625rem; + left: 0; + display: block; + width: 1rem; + height: 1rem; + pointer-events: none; + user-select: none; + background-color: #ddd; + background-repeat: no-repeat; + background-position: center center; + background-size: 50% 50%; +} + +.custom-checkbox .custom-control-indicator { + border-radius: 0.25rem; +} + +.custom-checkbox .custom-control-input:checked ~ .custom-control-indicator { + background-image: url(data:image/svg+xml;charset=utf8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA4IDgnPjxwYXRoIGZpbGw9JyNmZmYnIGQ9J002LjU2NC43NWwtMy41OSAzLjYxMi0xLjUzOC0xLjU1TDAgNC4yNiAyLjk3NCA3LjI1IDggMi4xOTN6Jy8+PC9zdmc+); +} + +.custom-checkbox + .custom-control-input:indeterminate + ~ .custom-control-indicator { + background-color: #0074d9; + background-image: url(data:image/svg+xml;charset=utf8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA0IDQnPjxwYXRoIHN0cm9rZT0nI2ZmZicgZD0nTTAgMmg0Jy8+PC9zdmc+); +} + +.custom-radio .custom-control-indicator { + border-radius: 50%; +} + +.custom-radio .custom-control-input:checked ~ .custom-control-indicator { + background-image: url(data:image/svg+xml;charset=utf8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9Jy00IC00IDggOCc+PGNpcmNsZSByPSczJyBmaWxsPScjZmZmJy8+PC9zdmc+); +} + +.custom-controls-stacked .custom-control { + display: inline; +} + +.custom-controls-stacked .custom-control::after { + display: block; + margin-bottom: 0.25rem; + content: ''; +} + +.custom-controls-stacked .custom-control + .custom-control { + margin-left: 0; +} + +.custom-select { + display: inline-block; + max-width: 100%; + padding: 0.375rem 1.75rem 0.375rem 0.75rem; + padding-right: 0.75rem \9; + color: #55595c; + vertical-align: middle; + background: #211f21 + url(data:image/svg+xml;charset=utf8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA0IDUnPjxwYXRoIGZpbGw9JyMzMzMnIGQ9J00yIDBMMCAyaDR6bTAgNUwwIDNoNHonLz48L3N2Zz4=) + no-repeat right 0.75rem center; + background-image: none \9; + background-size: 8px 10px; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; + -moz-appearance: none; + -webkit-appearance: none; +} + +.custom-select:focus { + border-color: #51a7e8; + outline: none; +} + +.custom-select::-ms-expand { + opacity: 0; +} + +.custom-select-sm { + padding-top: 0.375rem; + padding-bottom: 0.375rem; + font-size: 75%; +} + +.custom-file { + position: relative; + display: inline-block; + max-width: 100%; + height: 2.5rem; + cursor: pointer; +} + +.custom-file-input { + min-width: 14rem; + max-width: 100%; + margin: 0; + filter: alpha(opacity=0); + opacity: 0; +} + +.custom-file-control { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 5; + height: 2.5rem; + padding: 0.5rem 1rem; + line-height: 1.5; + color: #555; + user-select: none; + background-color: #211f21; + border: 1px solid #ddd; + border-radius: 0.25rem; +} + +.custom-file-control:lang(en)::after { + content: 'Choose file...'; +} + +.custom-file-control::before { + position: absolute; + top: -1px; + right: -1px; + bottom: -1px; + z-index: 6; + display: block; + height: 2.5rem; + padding: 0.5rem 1rem; + line-height: 1.5; + color: #555; + background-color: #eee; + border: 1px solid #ddd; + border-radius: 0 0.25rem 0.25rem 0; +} + +.custom-file-control:lang(en)::before { + content: 'Browse'; +} + +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav-link { + display: inline-block; +} + +.nav-link:focus, +.nav-link:hover { + text-decoration: none; +} + +.nav-link.disabled { + color: #818a91; +} + +.nav-link.disabled, +.nav-link.disabled:focus, +.nav-link.disabled:hover { + color: #818a91; + cursor: not-allowed; + background-color: transparent; +} + +.nav-inline .nav-item { + display: inline-block; +} + +.nav-inline .nav-item + .nav-item, +.nav-inline .nav-link + .nav-link { + margin-left: 1rem; +} + +.nav-tabs { + border-bottom: 1px solid #ddd; +} + +.nav-tabs::after { + content: ''; + display: table; + clear: both; +} + +.nav-tabs .nav-item { + float: left; + margin-bottom: -1px; +} + +.nav-tabs .nav-item + .nav-item { + margin-left: 0.2rem; +} + +.nav-tabs .nav-link { + display: block; + padding: 0.5em 1em; + border: 1px solid transparent; + border-radius: 0.25rem 0.25rem 0 0; +} + +.nav-tabs .nav-link:focus, +.nav-tabs .nav-link:hover { + border-color: #eceeef #eceeef #ddd; +} + +.nav-tabs .nav-link.disabled, +.nav-tabs .nav-link.disabled:focus, +.nav-tabs .nav-link.disabled:hover { + color: #818a91; + background-color: transparent; + border-color: transparent; +} + +.nav-tabs .nav-link.active, +.nav-tabs .nav-link.active:focus, +.nav-tabs .nav-link.active:hover, +.nav-tabs .nav-item.open .nav-link, +.nav-tabs .nav-item.open .nav-link:focus, +.nav-tabs .nav-item.open .nav-link:hover { + color: #55595c; + background-color: #211f21; + border-color: #ddd #ddd transparent; +} + +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.nav-pills::after { + content: ''; + display: table; + clear: both; +} + +.nav-pills .nav-item { + float: left; +} + +.nav-pills .nav-item + .nav-item { + margin-left: 0.2rem; +} + +.nav-pills .nav-link { + display: block; + padding: 0.5em 1em; + border-radius: 0.25rem; +} + +.nav-pills .nav-link.active, +.nav-pills .nav-link.active:focus, +.nav-pills .nav-link.active:hover, +.nav-pills .nav-item.open .nav-link, +.nav-pills .nav-item.open .nav-link:focus, +.nav-pills .nav-item.open .nav-link:hover { + color: #fff; + cursor: default; + background-color: #ec8cb5; +} + +.nav-stacked .nav-item { + display: block; + float: none; +} + +.nav-stacked .nav-item + .nav-item { + margin-top: 0.2rem; + margin-left: 0; +} + +.tab-content > .tab-pane { + display: none; +} + +.tab-content > .active { + display: block; +} + +.navbar { + position: relative; + padding: 0.5rem 1rem; +} + +.navbar::after { + content: ''; + display: table; + clear: both; +} + +@media (min-width: 544px) { + .navbar { + border-radius: 0.25rem; + } +} + +.navbar-full { + z-index: 1000; +} + +@media (min-width: 544px) { + .navbar-full { + border-radius: 0; + } +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; +} + +@media (min-width: 544px) { + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; + } +} + +.navbar-fixed-top { + top: 0; +} + +.navbar-fixed-bottom { + bottom: 0; +} + +.navbar-sticky-top { + position: sticky; + top: 0; + z-index: 1030; + width: 100%; +} + +@media (min-width: 544px) { + .navbar-sticky-top { + border-radius: 0; + } +} + +.navbar-brand { + float: left; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + margin-right: 1rem; + font-size: 1.25rem; +} + +.navbar-brand:focus, +.navbar-brand:hover { + text-decoration: none; +} + +.navbar-brand > img { + display: block; +} + +.navbar-divider { + float: left; + width: 1px; + padding-top: 0.425rem; + padding-bottom: 0.425rem; + margin-right: 1rem; + margin-left: 1rem; + overflow: hidden; +} + +.navbar-divider::before { + content: '\00a0'; +} + +.navbar-toggler { + padding: 0.5rem 0.75rem; + font-size: 1.25rem; + line-height: 1; + background: 0 0; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.navbar-toggler:focus, +.navbar-toggler:hover { + text-decoration: none; +} + +@media (min-width: 544px) { + .navbar-toggleable-xs { + display: block !important; + } +} + +@media (min-width: 768px) { + .navbar-toggleable-sm { + display: block !important; + } +} + +@media (min-width: 992px) { + .navbar-toggleable-md { + display: block !important; + } +} + +.navbar-nav .nav-item { + float: left; +} + +.navbar-nav .nav-link { + display: block; + padding-top: 0.425rem; + padding-bottom: 0.425rem; +} + +.navbar-nav .nav-link + .nav-link { + margin-left: 1rem; +} + +.navbar-nav .nav-item + .nav-item { + margin-left: 1rem; +} + +.navbar-light .navbar-brand { + color: rgba(0, 0, 0, 0.8); +} + +.navbar-light .navbar-brand:focus, +.navbar-light .navbar-brand:hover { + color: rgba(0, 0, 0, 0.8); +} + +.navbar-light .navbar-nav .nav-link { + color: #8c8889; +} + +.navbar-light .navbar-nav .nav-link:focus, +.navbar-light .navbar-nav .nav-link:hover { + color: rgba(255, 255, 255, 1); +} + +.navbar-light .navbar-nav .open > .nav-link, +.navbar-light .navbar-nav .open > .nav-link:focus, +.navbar-light .navbar-nav .open > .nav-link:hover, +.navbar-light .navbar-nav .active > .nav-link, +.navbar-light .navbar-nav .active > .nav-link:focus, +.navbar-light .navbar-nav .active > .nav-link:hover, +.navbar-light .navbar-nav .nav-link.open, +.navbar-light .navbar-nav .nav-link.open:focus, +.navbar-light .navbar-nav .nav-link.open:hover, +.navbar-light .navbar-nav .nav-link.active, +.navbar-light .navbar-nav .nav-link.active:focus, +.navbar-light .navbar-nav .nav-link.active:hover { + color: rgba(0, 0, 0, 0.8); +} + +.navbar-light .navbar-divider { + background-color: rgba(0, 0, 0, 0.075); +} + +.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, 0.5); +} + +.navbar-dark .navbar-nav .nav-link:focus, +.navbar-dark .navbar-nav .nav-link:hover { + color: rgba(255, 255, 255, 0.75); +} + +.navbar-dark .navbar-nav .open > .nav-link, +.navbar-dark .navbar-nav .open > .nav-link:focus, +.navbar-dark .navbar-nav .open > .nav-link:hover, +.navbar-dark .navbar-nav .active > .nav-link, +.navbar-dark .navbar-nav .active > .nav-link:focus, +.navbar-dark .navbar-nav .active > .nav-link:hover, +.navbar-dark .navbar-nav .nav-link.open, +.navbar-dark .navbar-nav .nav-link.open:focus, +.navbar-dark .navbar-nav .nav-link.open:hover, +.navbar-dark .navbar-nav .nav-link.active, +.navbar-dark .navbar-nav .nav-link.active:focus, +.navbar-dark .navbar-nav .nav-link.active:hover { + color: #fff; +} + +.navbar-dark .navbar-divider { + background-color: rgba(255, 255, 255, 0.075); +} + +.card { + position: relative; + display: block; + margin-bottom: 0.75rem; + background-color: #211f21; + border-radius: 0.25rem; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.125); +} + +.card-block { + padding: 1.25rem; +} + +.card-block::after { + content: ''; + display: table; + clear: both; +} + +.card-title { + margin-bottom: 0.75rem; +} + +.card-subtitle { + margin-top: -0.375rem; + margin-bottom: 0; +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link:hover { + text-decoration: none; +} + +.card-link + .card-link { + margin-left: 1.25rem; +} + +.card > .list-group:first-child .list-group-item:first-child { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.card > .list-group:last-child .list-group-item:last-child { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.card-header { + padding: 0.75rem 1.25rem; + background-color: #f5f5f5; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.125); +} + +.card-header::after { + content: ''; + display: table; + clear: both; +} + +.card-header:first-child { + border-radius: 0.25rem 0.25rem 0 0; +} + +.card-footer { + padding: 0.75rem 1.25rem; + background-color: #785468; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.125); +} + +.card-footer::after { + content: ''; + display: table; + clear: both; +} + +.card-footer:last-child { + border-radius: 0 0 0.25rem 0.25rem; +} + +.card-header-tabs { + margin-right: -0.625rem; + margin-bottom: -0.75rem; + margin-left: -0.625rem; + border-bottom: 0; +} + +.card-header-tabs .nav-item { + margin-bottom: 0; +} + +.card-header-pills { + margin-right: -0.625rem; + margin-left: -0.625rem; +} + +.card-primary { + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.card-success { + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.card-info { + background-color: #5bc0de; + border-color: #5bc0de; +} + +.card-warning { + background-color: #f0ad4e; + border-color: #f0ad4e; +} + +.card-danger { + background-color: #b85c5c; + border-color: #b85c5c; +} + +.card-outline-primary { + background-color: transparent; + border-color: #ec8cb5; +} + +.card-outline-secondary { + background-color: transparent; + border-color: #ccc; +} + +.card-outline-info { + background-color: transparent; + border-color: #5bc0de; +} + +.card-outline-success { + background-color: transparent; + border-color: #ec8cb5; +} + +.card-outline-warning { + background-color: transparent; + border-color: #f0ad4e; +} + +.card-outline-danger { + background-color: transparent; + border-color: #b85c5c; +} + +.card-inverse .card-header, +.card-inverse .card-footer { + border-bottom: 1px solid rgba(255, 255, 255, 0.2); +} + +.card-inverse .card-header, +.card-inverse .card-footer, +.card-inverse .card-title, +.card-inverse .card-blockquote { + color: #fff; +} + +.card-inverse .card-link, +.card-inverse .card-text, +.card-inverse .card-blockquote > footer { + color: rgba(255, 255, 255, 0.65); +} + +.card-inverse .card-link:focus, +.card-inverse .card-link:hover { + color: #fff; +} + +.card-blockquote { + padding: 0; + margin-bottom: 0; + border-left: 0; +} + +.card-img { + border-radius: 0.25rem; +} + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 1.25rem; +} + +.card-img-top { + border-radius: 0.25rem 0.25rem 0 0; +} + +.card-img-bottom { + border-radius: 0 0 0.25rem 0.25rem; +} + +@media (min-width: 544px) { + .card-deck { + display: flex; + flex-flow: row wrap; + margin-right: -0.625rem; + margin-left: -0.625rem; + } + .card-deck .card { + flex: 1 0 0; + margin-right: 0.625rem; + margin-left: 0.625rem; + } +} + +@media (min-width: 544px) { + .card-group { + display: flex; + flex-flow: row wrap; + } + .card-group .card { + flex: 1 0 0; + } + .card-group .card + .card { + margin-left: 0; + border-left: 0; + } + .card-group .card:first-child { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + } + .card-group .card:first-child .card-img-top { + border-top-right-radius: 0; + } + .card-group .card:first-child .card-img-bottom { + border-bottom-right-radius: 0; + } + .card-group .card:last-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + } + .card-group .card:last-child .card-img-top { + border-top-left-radius: 0; + } + .card-group .card:last-child .card-img-bottom { + border-bottom-left-radius: 0; + } + .card-group .card:not(:first-child):not(:last-child) { + border-radius: 0; + } + .card-group .card:not(:first-child):not(:last-child) .card-img-top, + .card-group .card:not(:first-child):not(:last-child) .card-img-bottom { + border-radius: 0; + } +} + +@media (min-width: 544px) { + .card-columns { + column-count: 3; + column-gap: 1.25rem; + } + .card-columns .card { + display: inline-block; + width: 100%; + } +} + +.breadcrumb { + padding: 0.75rem 1rem; + margin-bottom: 1rem; + list-style: none; + background-color: #eceeef; + border-radius: 0.25rem; +} + +.breadcrumb::after { + content: ''; + display: table; + clear: both; +} + +.breadcrumb-item { + float: left; +} + +.breadcrumb-item + .breadcrumb-item::before { + display: inline-block; + padding-right: 0.5rem; + padding-left: 0.5rem; + color: #818a91; + content: '/'; +} + +.breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: underline; +} + +.breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: none; +} + +.breadcrumb-item.active { + color: #818a91; +} + +.pagination { + display: inline-block; + padding-left: 0; + margin-top: 1rem; + margin-bottom: 1rem; + border-radius: 0.25rem; +} + +.page-item { + display: inline; +} + +.page-item:first-child .page-link { + margin-left: 0; + border-bottom-left-radius: 0.25rem; + border-top-left-radius: 0.25rem; +} + +.page-item:last-child .page-link { + border-bottom-right-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.page-item.active .page-link, +.page-item.active .page-link:focus, +.page-item.active .page-link:hover { + z-index: 2; + color: #fff; + cursor: default; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.page-item.disabled .page-link, +.page-item.disabled .page-link:focus, +.page-item.disabled .page-link:hover { + color: #818a91; + pointer-events: none; + cursor: not-allowed; + background-color: #211f21; + border-color: #ddd; +} + +.page-link { + position: relative; + float: left; + padding: 0.5rem 0.75rem; + margin-left: -1px; + color: #ec8cb5; + text-decoration: none; + background-color: #211f21; + border: 1px solid #ddd; +} + +.page-link:focus, +.page-link:hover { + color: #3d8b3d; + background-color: #eceeef; + border-color: #ddd; +} + +.pagination-lg .page-link { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; +} + +.pagination-lg .page-item:first-child .page-link { + border-bottom-left-radius: 0.3rem; + border-top-left-radius: 0.3rem; +} + +.pagination-lg .page-item:last-child .page-link { + border-bottom-right-radius: 0.3rem; + border-top-right-radius: 0.3rem; +} + +.pagination-sm .page-link { + padding: 0.275rem 0.75rem; + font-size: 0.875rem; +} + +.pagination-sm .page-item:first-child .page-link { + border-bottom-left-radius: 0.2rem; + border-top-left-radius: 0.2rem; +} + +.pagination-sm .page-item:last-child .page-link { + border-bottom-right-radius: 0.2rem; + border-top-right-radius: 0.2rem; +} + +.tag { + display: inline-block; + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; +} + +.tag:empty { + display: none; +} + +.btn .tag { + position: relative; + top: -1px; +} + +a.tag:focus, +a.tag:hover { + color: #fff; + text-decoration: none; + cursor: pointer; +} + +.tag-pill { + border: 1px solid rgba(0, 0, 0, 0.5); + padding-right: 0.6em; + padding-left: 0.6em; + border-radius: 8px; +} + +.tag-default { + background-color: #818a91; +} + +.tag-default[href]:focus, +.tag-default[href]:hover { + background-color: #687077; +} + +.tag-primary { + background-color: #ec8cb5; +} + +.tag-primary[href]:focus, +.tag-primary[href]:hover { + background-color: #7a8992; +} + +.tag-success { + background-color: #ec8cb5; +} + +.tag-success[href]:focus, +.tag-success[href]:hover { + background-color: #7a8992; +} + +.tag-info { + background-color: #5bc0de; +} + +.tag-info[href]:focus, +.tag-info[href]:hover { + background-color: #31b0d5; +} + +.tag-warning { + background-color: #f0ad4e; +} + +.tag-warning[href]:focus, +.tag-warning[href]:hover { + background-color: #ec971f; +} + +.tag-danger { + background-color: #b85c5c; +} + +.tag-danger[href]:focus, +.tag-danger[href]:hover { + background-color: #9d4444; +} + +.jumbotron { + padding: 2rem 1rem; + margin-bottom: 2rem; + background-color: #eceeef; + border-radius: 0.3rem; +} + +@media (min-width: 544px) { + .jumbotron { + padding: 4rem 2rem; + } +} + +.jumbotron-hr { + border-top-color: #d0d5d8; +} + +.jumbotron-fluid { + padding-right: 0; + padding-left: 0; + border-radius: 0; +} + +.alert { + padding: 15px; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.alert-heading { + color: inherit; +} + +.alert-link { + font-weight: 700; +} + +.alert-dismissible { + padding-right: 35px; +} + +.alert-dismissible .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} + +.alert-success { + background-color: #dff0d8; + border-color: #d0e9c6; + color: #3c763d; +} + +.alert-success hr { + border-top-color: #c1e2b3; +} + +.alert-success .alert-link { + color: #2b542c; +} + +.alert-info { + background-color: #d9edf7; + border-color: #bcdff1; + color: #31708f; +} + +.alert-info hr { + border-top-color: #a6d5ec; +} + +.alert-info .alert-link { + color: #245269; +} + +.alert-warning { + background-color: #fcf8e3; + border-color: #faf2cc; + color: #8a6d3b; +} + +.alert-warning hr { + border-top-color: #f7ecb5; +} + +.alert-warning .alert-link { + color: #66512c; +} + +.alert-danger { + background-color: #f2dede; + border-color: #ebcccc; + color: #a94442; +} + +.alert-danger hr { + border-top-color: #e4b9b9; +} + +.alert-danger .alert-link { + color: #843534; +} + +@keyframes progress-bar-stripes { + from { + background-position: 1rem 0; + } + to { + background-position: 0 0; + } +} + +.progress { + display: block; + width: 100%; + height: 1rem; + margin-bottom: 1rem; +} + +.progress[value] { + background-color: #eee; + border: 0; + appearance: none; + border-radius: 0.25rem; +} + +.progress[value]::-ms-fill { + background-color: #0074d9; + border: 0; +} + +.progress[value]::-moz-progress-bar { + background-color: #0074d9; + border-bottom-left-radius: 0.25rem; + border-top-left-radius: 0.25rem; +} + +.progress[value]::-webkit-progress-value { + background-color: #0074d9; + border-bottom-left-radius: 0.25rem; + border-top-left-radius: 0.25rem; +} + +.progress[value='100']::-moz-progress-bar { + border-bottom-right-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.progress[value='100']::-webkit-progress-value { + border-bottom-right-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.progress[value]::-webkit-progress-bar { + background-color: #eee; + border-radius: 0.25rem; +} + +base::-moz-progress-bar, +.progress[value] { + background-color: #eee; + border-radius: 0.25rem; +} + +@media screen and (min-width: 0\0) { + .progress { + background-color: #eee; + border-radius: 0.25rem; + } + .progress-bar { + display: inline-block; + height: 1rem; + text-indent: -999rem; + background-color: #0074d9; + border-bottom-left-radius: 0.25rem; + border-top-left-radius: 0.25rem; + } + .progress[width='100%'] { + border-bottom-right-radius: 0.25rem; + border-top-right-radius: 0.25rem; + } +} + +.progress-striped[value]::-webkit-progress-value { + background-image: linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-size: 1rem 1rem; +} + +.progress-striped[value]::-moz-progress-bar { + background-image: linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-size: 1rem 1rem; +} + +.progress-striped[value]::-ms-fill { + background-image: linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-size: 1rem 1rem; +} + +@media screen and (min-width: 0\0) { + .progress-bar-striped { + background-image: linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-size: 1rem 1rem; + } +} + +.progress-animated[value]::-webkit-progress-value { + animation: progress-bar-stripes 2s linear infinite; +} + +.progress-animated[value]::-moz-progress-bar { + animation: progress-bar-stripes 2s linear infinite; +} + +@media screen and (min-width: 0\0) { + .progress-animated .progress-bar-striped { + animation: progress-bar-stripes 2s linear infinite; + } +} + +.progress-success[value]::-webkit-progress-value { + background-color: #ec8cb5; +} + +.progress-success[value]::-moz-progress-bar { + background-color: #ec8cb5; +} + +.progress-success[value]::-ms-fill { + background-color: #ec8cb5; +} + +@media screen and (min-width: 0\0) { + .progress-success .progress-bar { + background-color: #ec8cb5; + } +} + +.progress-info[value]::-webkit-progress-value { + background-color: #5bc0de; +} + +.progress-info[value]::-moz-progress-bar { + background-color: #5bc0de; +} + +.progress-info[value]::-ms-fill { + background-color: #5bc0de; +} + +@media screen and (min-width: 0\0) { + .progress-info .progress-bar { + background-color: #5bc0de; + } +} + +.progress-warning[value]::-webkit-progress-value { + background-color: #f0ad4e; +} + +.progress-warning[value]::-moz-progress-bar { + background-color: #f0ad4e; +} + +.progress-warning[value]::-ms-fill { + background-color: #f0ad4e; +} + +@media screen and (min-width: 0\0) { + .progress-warning .progress-bar { + background-color: #f0ad4e; + } +} + +.progress-danger[value]::-webkit-progress-value { + background-color: #b85c5c; +} + +.progress-danger[value]::-moz-progress-bar { + background-color: #b85c5c; +} + +.progress-danger[value]::-ms-fill { + background-color: #b85c5c; +} + +@media screen and (min-width: 0\0) { + .progress-danger .progress-bar { + background-color: #b85c5c; + } +} + +.media { + display: flex; + margin-bottom: 1rem; +} + +.media-body { + flex: 1; +} + +.media-middle { + align-self: center; +} + +.media-bottom { + align-self: flex-end; +} + +.media-object { + display: block; +} + +.media-object.img-thumbnail { + max-width: none; +} + +.media-right { + padding-left: 10px; +} + +.media-left { + padding-right: 10px; +} + +.media-heading { + margin-top: 0; + margin-bottom: 5px; +} + +.media-list { + padding-left: 0; + list-style: none; +} + +.list-group { + padding-left: 0; + margin-bottom: 0; +} + +.list-group-item { + position: relative; + display: block; + padding: 0.75rem 1.25rem; + margin-bottom: -1px; + background-color: #211f21; + border: 1px solid #ddd; +} + +.list-group-item:first-child { + border-top-right-radius: 0.25rem; + border-top-left-radius: 0.25rem; +} + +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.list-group-item.disabled, +.list-group-item.disabled:focus, +.list-group-item.disabled:hover { + color: #818a91; + cursor: not-allowed; + background-color: #eceeef; +} + +.list-group-item.disabled .list-group-item-heading, +.list-group-item.disabled:focus .list-group-item-heading, +.list-group-item.disabled:hover .list-group-item-heading { + color: inherit; +} + +.list-group-item.disabled .list-group-item-text, +.list-group-item.disabled:focus .list-group-item-text, +.list-group-item.disabled:hover .list-group-item-text { + color: #818a91; +} + +.list-group-item.active, +.list-group-item.active:focus, +.list-group-item.active:hover { + z-index: 2; + color: #fff; + text-decoration: none; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.list-group-item.active .list-group-item-heading, +.list-group-item.active .list-group-item-heading > small, +.list-group-item.active .list-group-item-heading > .small, +.list-group-item.active:focus .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading > small, +.list-group-item.active:focus .list-group-item-heading > .small, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading > small, +.list-group-item.active:hover .list-group-item-heading > .small { + color: inherit; +} + +.list-group-item.active .list-group-item-text, +.list-group-item.active:focus .list-group-item-text, +.list-group-item.active:hover .list-group-item-text { + color: #eaf6ea; +} + +.list-group-flush .list-group-item { + border-radius: 0; +} + +.list-group-item-action { + width: 100%; + color: #555; + text-align: inherit; +} + +.list-group-item-action .list-group-item-heading { + color: #333; +} + +.list-group-item-action:focus, +.list-group-item-action:hover { + color: #555; + text-decoration: none; + background-color: #f5f5f5; +} + +.list-group-item-success { + color: #3c763d; + background-color: #dff0d8; +} + +a.list-group-item-success, +button.list-group-item-success { + color: #3c763d; +} + +a.list-group-item-success .list-group-item-heading, +button.list-group-item-success .list-group-item-heading { + color: inherit; +} + +a.list-group-item-success:focus, +a.list-group-item-success:hover, +button.list-group-item-success:focus, +button.list-group-item-success:hover { + color: #3c763d; + background-color: #d0e9c6; +} + +a.list-group-item-success.active, +a.list-group-item-success.active:focus, +a.list-group-item-success.active:hover, +button.list-group-item-success.active, +button.list-group-item-success.active:focus, +button.list-group-item-success.active:hover { + color: #fff; + background-color: #3c763d; + border-color: #3c763d; +} + +.list-group-item-info { + color: #31708f; + background-color: #d9edf7; +} + +a.list-group-item-info, +button.list-group-item-info { + color: #31708f; +} + +a.list-group-item-info .list-group-item-heading, +button.list-group-item-info .list-group-item-heading { + color: inherit; +} + +a.list-group-item-info:focus, +a.list-group-item-info:hover, +button.list-group-item-info:focus, +button.list-group-item-info:hover { + color: #31708f; + background-color: #c4e3f3; +} + +a.list-group-item-info.active, +a.list-group-item-info.active:focus, +a.list-group-item-info.active:hover, +button.list-group-item-info.active, +button.list-group-item-info.active:focus, +button.list-group-item-info.active:hover { + color: #fff; + background-color: #31708f; + border-color: #31708f; +} + +.list-group-item-warning { + color: #8a6d3b; + background-color: #fcf8e3; +} + +a.list-group-item-warning, +button.list-group-item-warning { + color: #8a6d3b; +} + +a.list-group-item-warning .list-group-item-heading, +button.list-group-item-warning .list-group-item-heading { + color: inherit; +} + +a.list-group-item-warning:focus, +a.list-group-item-warning:hover, +button.list-group-item-warning:focus, +button.list-group-item-warning:hover { + color: #8a6d3b; + background-color: #faf2cc; +} + +a.list-group-item-warning.active, +a.list-group-item-warning.active:focus, +a.list-group-item-warning.active:hover, +button.list-group-item-warning.active, +button.list-group-item-warning.active:focus, +button.list-group-item-warning.active:hover { + color: #fff; + background-color: #8a6d3b; + border-color: #8a6d3b; +} + +.list-group-item-danger { + color: #a94442; + background-color: #f2dede; +} + +a.list-group-item-danger, +button.list-group-item-danger { + color: #a94442; +} + +a.list-group-item-danger .list-group-item-heading, +button.list-group-item-danger .list-group-item-heading { + color: inherit; +} + +a.list-group-item-danger:focus, +a.list-group-item-danger:hover, +button.list-group-item-danger:focus, +button.list-group-item-danger:hover { + color: #a94442; + background-color: #ebcccc; +} + +a.list-group-item-danger.active, +a.list-group-item-danger.active:focus, +a.list-group-item-danger.active:hover, +button.list-group-item-danger.active, +button.list-group-item-danger.active:focus, +button.list-group-item-danger.active:hover { + color: #fff; + background-color: #a94442; + border-color: #a94442; +} + +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} + +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} + +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; +} + +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} + +.embed-responsive-21by9 { + padding-bottom: 42.85714%; +} + +.embed-responsive-16by9 { + padding-bottom: 56.25%; +} + +.embed-responsive-4by3 { + padding-bottom: 75%; +} + +.embed-responsive-1by1 { + padding-bottom: 100%; +} + +.close { + float: right; + font-size: 1.5rem; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: 0.2; +} + +.close:focus, +.close:hover { + color: #000; + text-decoration: none; + cursor: pointer; + opacity: 0.5; +} + +button.close { + padding: 0; + cursor: pointer; + background: 0 0; + border: 0; + -webkit-appearance: none; +} + +.modal-open { + overflow: hidden; +} + +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + display: none; + overflow: hidden; + outline: 0; + -webkit-overflow-scrolling: touch; +} + +.modal.fade .modal-dialog { + transition: transform 0.3s ease-out; + transform: translate(0, -25%); +} + +.modal.in .modal-dialog { + transform: translate(0, 0); +} + +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} + +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} + +.modal-content { + position: relative; + background-color: #211f21; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; + outline: 0; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop.in { + opacity: 0.5; +} + +.modal-header { + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} + +.modal-header::after { + content: ''; + display: table; + clear: both; +} + +.modal-header .close { + margin-top: -2px; +} + +.modal-title { + margin: 0; + line-height: 1.5; +} + +.modal-body { + position: relative; + padding: 15px; +} + +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} + +.modal-footer::after { + content: ''; + display: table; + clear: both; +} + +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} + +@media (min-width: 544px) { + .modal-dialog { + max-width: 600px; + margin: 30px auto; + } + .modal-sm { + max-width: 300px; + } +} + +@media (min-width: 992px) { + .modal-lg { + max-width: 900px; + } +} + +.tooltip { + position: absolute; + z-index: 1070; + display: block; + font-family: source sans pro, sans-serif; + font-style: normal; + font-weight: 400; + letter-spacing: normal; + line-break: auto; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + white-space: normal; + word-break: normal; + word-spacing: normal; + font-size: 0.875rem; + word-wrap: break-word; + opacity: 0; +} + +.tooltip.in { + opacity: 0.9; +} + +.tooltip.tooltip-top, +.tooltip.bs-tether-element-attached-bottom { + padding: 5px 0; + margin-top: -3px; +} + +.tooltip.tooltip-top .tooltip-arrow, +.tooltip.bs-tether-element-attached-bottom .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} + +.tooltip.tooltip-right, +.tooltip.bs-tether-element-attached-left { + padding: 0 5px; + margin-left: 3px; +} + +.tooltip.tooltip-right .tooltip-arrow, +.tooltip.bs-tether-element-attached-left .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #000; +} + +.tooltip.tooltip-bottom, +.tooltip.bs-tether-element-attached-top { + padding: 5px 0; + margin-top: 3px; +} + +.tooltip.tooltip-bottom .tooltip-arrow, +.tooltip.bs-tether-element-attached-top .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} + +.tooltip.tooltip-left, +.tooltip.bs-tether-element-attached-right { + padding: 0 5px; + margin-left: -3px; +} + +.tooltip.tooltip-left .tooltip-arrow, +.tooltip.bs-tether-element-attached-right .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: #000; +} + +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 0.25rem; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: block; + max-width: 276px; + padding: 1px; + font-family: source sans pro, sans-serif; + font-style: normal; + font-weight: 400; + letter-spacing: normal; + line-break: auto; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + white-space: normal; + word-break: normal; + word-spacing: normal; + font-size: 0.875rem; + word-wrap: break-word; + background-color: #211f21; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; +} + +.popover.popover-top, +.popover.bs-tether-element-attached-bottom { + margin-top: -10px; +} + +.popover.popover-top .popover-arrow, +.popover.bs-tether-element-attached-bottom .popover-arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: rgba(0, 0, 0, 0.25); + border-bottom-width: 0; +} + +.popover.popover-top .popover-arrow::after, +.popover.bs-tether-element-attached-bottom .popover-arrow::after { + bottom: 1px; + margin-left: -10px; + content: ''; + border-top-color: #fff; + border-bottom-width: 0; +} + +.popover.popover-right, +.popover.bs-tether-element-attached-left { + margin-left: 10px; +} + +.popover.popover-right .popover-arrow, +.popover.bs-tether-element-attached-left .popover-arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: rgba(0, 0, 0, 0.25); + border-left-width: 0; +} + +.popover.popover-right .popover-arrow::after, +.popover.bs-tether-element-attached-left .popover-arrow::after { + bottom: -10px; + left: 1px; + content: ''; + border-right-color: #fff; + border-left-width: 0; +} + +.popover.popover-bottom, +.popover.bs-tether-element-attached-top { + margin-top: 10px; +} + +.popover.popover-bottom .popover-arrow, +.popover.bs-tether-element-attached-top .popover-arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: rgba(0, 0, 0, 0.25); +} + +.popover.popover-bottom .popover-arrow::after, +.popover.bs-tether-element-attached-top .popover-arrow::after { + top: 1px; + margin-left: -10px; + content: ''; + border-top-width: 0; + border-bottom-color: #fff; +} + +.popover.popover-left, +.popover.bs-tether-element-attached-right { + margin-left: -10px; +} + +.popover.popover-left .popover-arrow, +.popover.bs-tether-element-attached-right .popover-arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: rgba(0, 0, 0, 0.25); +} + +.popover.popover-left .popover-arrow::after, +.popover.bs-tether-element-attached-right .popover-arrow::after { + right: 1px; + bottom: -10px; + content: ''; + border-right-width: 0; + border-left-color: #fff; +} + +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 1rem; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 0.2375rem 0.2375rem 0 0; +} + +.popover-content { + padding: 9px 14px; +} + +.popover-arrow, +.popover-arrow::after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover-arrow { + border-width: 11px; +} + +.popover-arrow::after { + content: ''; + border-width: 10px; +} + +.carousel { + position: relative; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel-inner > .carousel-item { + position: relative; + display: none; + transition: 0.6s ease-in-out left; +} + +.carousel-inner > .carousel-item > img, +.carousel-inner > .carousel-item > a > img { + line-height: 1; +} + +@media all and (transform-3d), (-webkit-transform-3d) { + .carousel-inner > .carousel-item { + transition: transform 0.6s ease-in-out; + backface-visibility: hidden; + perspective: 1000px; + } + .carousel-inner > .carousel-item.next, + .carousel-inner > .carousel-item.active.right { + left: 0; + transform: translate3d(100%, 0, 0); + } + .carousel-inner > .carousel-item.prev, + .carousel-inner > .carousel-item.active.left { + left: 0; + transform: translate3d(-100%, 0, 0); + } + .carousel-inner > .carousel-item.next.left, + .carousel-inner > .carousel-item.prev.right, + .carousel-inner > .carousel-item.active { + left: 0; + transform: translate3d(0, 0, 0); + } +} + +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} + +.carousel-inner > .active { + left: 0; +} + +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} + +.carousel-inner > .next { + left: 100%; +} + +.carousel-inner > .prev { + left: -100%; +} + +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} + +.carousel-inner > .active.left { + left: -100%; +} + +.carousel-inner > .active.right { + left: 100%; +} + +.carousel-control { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 15%; + font-size: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); + opacity: 0.5; +} + +.carousel-control.left { + background-image: linear-gradient( + to right, + rgba(0, 0, 0, 0.5) 0%, + rgba(0, 0, 0, 0.0001) 100% + ); + background-repeat: repeat-x; + filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); +} + +.carousel-control.right { + right: 0; + left: auto; + background-image: linear-gradient( + to right, + rgba(0, 0, 0, 0.0001) 0%, + rgba(0, 0, 0, 0.5) 100% + ); + background-repeat: repeat-x; + filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); +} + +.carousel-control:focus, +.carousel-control:hover { + color: #fff; + text-decoration: none; + outline: 0; + opacity: 0.9; +} + +.carousel-control .icon-prev, +.carousel-control .icon-next { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; + width: 20px; + height: 20px; + margin-top: -10px; + font-family: serif; + line-height: 1; +} + +.carousel-control .icon-prev { + left: 50%; + margin-left: -10px; +} + +.carousel-control .icon-next { + right: 50%; + margin-right: -10px; +} + +.carousel-control .icon-prev::before { + content: '\2039'; +} + +.carousel-control .icon-next::before { + content: '\203a'; +} + +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + padding-left: 0; + margin-left: -30%; + text-align: center; + list-style: none; +} + +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + cursor: pointer; + background-color: transparent; + border: 1px solid #fff; + border-radius: 10px; +} + +.carousel-indicators .active { + width: 12px; + height: 12px; + margin: 0; + background-color: #211f21; +} + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); +} + +.carousel-caption .btn { + text-shadow: none; +} + +@media (min-width: 544px) { + .carousel-control .icon-prev, + .carousel-control .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + font-size: 30px; + } + .carousel-control .icon-prev { + margin-left: -15px; + } + .carousel-control .icon-next { + margin-right: -15px; + } + .carousel-caption { + right: 20%; + left: 20%; + padding-bottom: 30px; + } + .carousel-indicators { + bottom: 20px; + } +} + +.bg-inverse { + color: #eceeef; + background-color: #fff; +} + +.bg-faded { + background-color: #f7f7f9; +} + +.bg-primary { + color: #fff !important; + background-color: #ec8cb5 !important; +} + +a.bg-primary:focus, +a.bg-primary:hover { + background-color: #7a8992 !important; +} + +.bg-success { + color: #fff !important; + background-color: #ec8cb5 !important; +} + +a.bg-success:focus, +a.bg-success:hover { + background-color: #7a8992 !important; +} + +.bg-info { + color: #fff !important; + background-color: #5bc0de !important; +} + +a.bg-info:focus, +a.bg-info:hover { + background-color: #31b0d5 !important; +} + +.bg-warning { + color: #fff !important; + background-color: #f0ad4e !important; +} + +a.bg-warning:focus, +a.bg-warning:hover { + background-color: #ec971f !important; +} + +.bg-danger { + color: #fff !important; + background-color: #b85c5c !important; +} + +a.bg-danger:focus, +a.bg-danger:hover { + background-color: #9d4444 !important; +} + +.clearfix::after { + content: ''; + display: table; + clear: both; +} + +.pull-xs-left { + float: left !important; +} + +.pull-xs-right { + float: right !important; +} + +.pull-xs-none { + float: none !important; +} + +@media (min-width: 544px) { + .pull-sm-left { + float: left !important; + } + .pull-sm-right { + float: right !important; + } + .pull-sm-none { + float: none !important; + } +} + +@media (min-width: 768px) { + .pull-md-left { + float: left !important; + } + .pull-md-right { + float: right !important; + } + .pull-md-none { + float: none !important; + } +} + +@media (min-width: 992px) { + .pull-lg-left { + float: left !important; + } + .pull-lg-right { + float: right !important; + } + .pull-lg-none { + float: none !important; + } +} + +@media (min-width: 1200px) { + .pull-xl-left { + float: left !important; + } + .pull-xl-right { + float: right !important; + } + .pull-xl-none { + float: none !important; + } +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} + +.m-x-auto { + margin-right: auto !important; + margin-left: auto !important; +} + +.m-a-0 { + margin: 0 !important; +} + +.m-t-0 { + margin-top: 0 !important; +} + +.m-r-0 { + margin-right: 0 !important; +} + +.m-b-0 { + margin-bottom: 0 !important; +} + +.m-l-0 { + margin-left: 0 !important; +} + +.m-x-0 { + margin-right: 0 !important; + margin-left: 0 !important; +} + +.m-y-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +.m-a-1 { + margin: 1rem !important; +} + +.m-t-1 { + margin-top: 1rem !important; +} + +.m-r-1 { + margin-right: 1rem !important; +} + +.m-b-1 { + margin-bottom: 1rem !important; +} + +.m-l-1 { + margin-left: 1rem !important; +} + +.m-x-1 { + margin-right: 1rem !important; + margin-left: 1rem !important; +} + +.m-y-1 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; +} + +.m-a-2 { + margin: 1.5rem !important; +} + +.m-t-2 { + margin-top: 1.5rem !important; +} + +.m-r-2 { + margin-right: 1.5rem !important; +} + +.m-b-2 { + margin-bottom: 1.5rem !important; +} + +.m-l-2 { + margin-left: 1.5rem !important; +} + +.m-x-2 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; +} + +.m-y-2 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; +} + +.m-a-3 { + margin: 3rem !important; +} + +.m-t-3 { + margin-top: 3rem !important; +} + +.m-r-3 { + margin-right: 3rem !important; +} + +.m-b-3 { + margin-bottom: 3rem !important; +} + +.m-l-3 { + margin-left: 3rem !important; +} + +.m-x-3 { + margin-right: 3rem !important; + margin-left: 3rem !important; +} + +.m-y-3 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; +} + +.p-a-0 { + padding: 0 !important; +} + +.p-t-0 { + padding-top: 0 !important; +} + +.p-r-0 { + padding-right: 0 !important; +} + +.p-b-0 { + padding-bottom: 0 !important; +} + +.p-l-0 { + padding-left: 0 !important; +} + +.p-x-0 { + padding-right: 0 !important; + padding-left: 0 !important; +} + +.p-y-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.p-a-1 { + padding: 1rem !important; +} + +.p-t-1 { + padding-top: 1rem !important; +} + +.p-r-1 { + padding-right: 1rem !important; +} + +.p-b-1 { + padding-bottom: 1rem !important; +} + +.p-l-1 { + padding-left: 1rem !important; +} + +.p-x-1 { + padding-right: 1rem !important; + padding-left: 1rem !important; +} + +.p-y-1 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; +} + +.p-a-2 { + padding: 1.5rem !important; +} + +.p-t-2 { + padding-top: 1.5rem !important; +} + +.p-r-2 { + padding-right: 1.5rem !important; +} + +.p-b-2 { + padding-bottom: 1.5rem !important; +} + +.p-l-2 { + padding-left: 1.5rem !important; +} + +.p-x-2 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; +} + +.p-y-2 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; +} + +.p-a-3 { + padding: 3rem !important; +} + +.p-t-3 { + padding-top: 3rem !important; +} + +.p-r-3 { + padding-right: 3rem !important; +} + +.p-b-3 { + padding-bottom: 3rem !important; +} + +.p-l-3 { + padding-left: 3rem !important; +} + +.p-x-3 { + padding-right: 3rem !important; + padding-left: 3rem !important; +} + +.p-y-3 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; +} + +.pos-f-t { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; +} + +.text-justify { + text-align: justify !important; +} + +.text-nowrap { + white-space: nowrap !important; +} + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.text-xs-left { + text-align: left !important; +} + +.text-xs-right { + text-align: right !important; +} + +.text-xs-center { + text-align: center !important; +} + +@media (min-width: 544px) { + .text-sm-left { + text-align: left !important; + } + .text-sm-right { + text-align: right !important; + } + .text-sm-center { + text-align: center !important; + } +} + +@media (min-width: 768px) { + .text-md-left { + text-align: left !important; + } + .text-md-right { + text-align: right !important; + } + .text-md-center { + text-align: center !important; + } +} + +@media (min-width: 992px) { + .text-lg-left { + text-align: left !important; + } + .text-lg-right { + text-align: right !important; + } + .text-lg-center { + text-align: center !important; + } +} + +@media (min-width: 1200px) { + .text-xl-left { + text-align: left !important; + } + .text-xl-right { + text-align: right !important; + } + .text-xl-center { + text-align: center !important; + } +} + +.text-lowercase { + text-transform: lowercase !important; +} + +.text-uppercase { + text-transform: uppercase !important; +} + +.text-capitalize { + text-transform: capitalize !important; +} + +.font-weight-normal { + font-weight: 400; +} + +.font-weight-bold { + font-weight: 700; +} + +.font-italic { + font-style: italic; +} + +.text-muted { + color: #818a91 !important; +} + +a.text-muted:focus, +a.text-muted:hover { + color: #687077; +} + +.text-primary { + color: #ec8cb5 !important; +} + +a.text-primary:focus, +a.text-primary:hover { + color: #7a8992; +} + +.text-success { + color: #ec8cb5 !important; +} + +a.text-success:focus, +a.text-success:hover { + color: #7a8992; +} + +.text-info { + color: #5bc0de !important; +} + +a.text-info:focus, +a.text-info:hover { + color: #31b0d5; +} + +.text-warning { + color: #f0ad4e !important; +} + +a.text-warning:focus, +a.text-warning:hover { + color: #ec971f; +} + +.text-danger { + color: #b85c5c !important; +} + +a.text-danger:focus, +a.text-danger:hover { + color: #9d4444; +} + +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.invisible { + visibility: hidden !important; +} + +.hidden-xs-up { + display: none !important; +} + +@media (max-width: 543px) { + .hidden-xs-down { + display: none !important; + } +} + +@media (min-width: 544px) { + .hidden-sm-up { + display: none !important; + } +} + +@media (max-width: 767px) { + .hidden-sm-down { + display: none !important; + } +} + +@media (min-width: 768px) { + .hidden-md-up { + display: none !important; + } +} + +@media (max-width: 991px) { + .hidden-md-down { + display: none !important; + } +} + +@media (min-width: 992px) { + .hidden-lg-up { + display: none !important; + } +} + +@media (max-width: 1199px) { + .hidden-lg-down { + display: none !important; + } +} + +@media (min-width: 1200px) { + .hidden-xl-up { + display: none !important; + } +} + +.hidden-xl-down { + display: none !important; +} + +.visible-print-block { + display: none !important; +} + +@media print { + .visible-print-block { + display: block !important; + } +} + +.visible-print-inline { + display: none !important; +} + +@media print { + .visible-print-inline { + display: inline !important; + } +} + +.visible-print-inline-block { + display: none !important; +} + +@media print { + .visible-print-inline-block { + display: inline-block !important; + } +} + +@media print { + .hidden-print { + display: none !important; + } +} + +.flex-xs-first { + order: -1; +} + +.flex-xs-last { + order: 1; +} + +.flex-items-xs-top { + align-items: flex-start; +} + +.flex-items-xs-middle { + align-items: center; +} + +.flex-items-xs-bottom { + align-items: flex-end; +} + +.flex-xs-top { + align-self: flex-start; +} + +.flex-xs-middle { + align-self: center; +} + +.flex-xs-bottom { + align-self: flex-end; +} + +.flex-items-xs-left { + justify-content: flex-start; +} + +.flex-items-xs-center { + justify-content: center; +} + +.flex-items-xs-right { + justify-content: flex-end; +} + +.flex-items-xs-around { + justify-content: space-around; +} + +.flex-items-xs-between { + justify-content: space-between; +} + +@media (min-width: 544px) { + .flex-sm-first { + order: -1; + } + .flex-sm-last { + order: 1; + } +} + +@media (min-width: 544px) { + .flex-items-sm-top { + align-items: flex-start; + } + .flex-items-sm-middle { + align-items: center; + } + .flex-items-sm-bottom { + align-items: flex-end; + } +} + +@media (min-width: 544px) { + .flex-sm-top { + align-self: flex-start; + } + .flex-sm-middle { + align-self: center; + } + .flex-sm-bottom { + align-self: flex-end; + } +} + +@media (min-width: 544px) { + .flex-items-sm-left { + justify-content: flex-start; + } + .flex-items-sm-center { + justify-content: center; + } + .flex-items-sm-right { + justify-content: flex-end; + } + .flex-items-sm-around { + justify-content: space-around; + } + .flex-items-sm-between { + justify-content: space-between; + } +} + +@media (min-width: 768px) { + .flex-md-first { + order: -1; + } + .flex-md-last { + order: 1; + } +} + +@media (min-width: 768px) { + .flex-items-md-top { + align-items: flex-start; + } + .flex-items-md-middle { + align-items: center; + } + .flex-items-md-bottom { + align-items: flex-end; + } +} + +@media (min-width: 768px) { + .flex-md-top { + align-self: flex-start; + } + .flex-md-middle { + align-self: center; + } + .flex-md-bottom { + align-self: flex-end; + } +} + +@media (min-width: 768px) { + .flex-items-md-left { + justify-content: flex-start; + } + .flex-items-md-center { + justify-content: center; + } + .flex-items-md-right { + justify-content: flex-end; + } + .flex-items-md-around { + justify-content: space-around; + } + .flex-items-md-between { + justify-content: space-between; + } +} + +@media (min-width: 992px) { + .flex-lg-first { + order: -1; + } + .flex-lg-last { + order: 1; + } +} + +@media (min-width: 992px) { + .flex-items-lg-top { + align-items: flex-start; + } + .flex-items-lg-middle { + align-items: center; + } + .flex-items-lg-bottom { + align-items: flex-end; + } +} + +@media (min-width: 992px) { + .flex-lg-top { + align-self: flex-start; + } + .flex-lg-middle { + align-self: center; + } + .flex-lg-bottom { + align-self: flex-end; + } +} + +@media (min-width: 992px) { + .flex-items-lg-left { + justify-content: flex-start; + } + .flex-items-lg-center { + justify-content: center; + } + .flex-items-lg-right { + justify-content: flex-end; + } + .flex-items-lg-around { + justify-content: space-around; + } + .flex-items-lg-between { + justify-content: space-between; + } +} + +@media (min-width: 1200px) { + .flex-xl-first { + order: -1; + } + .flex-xl-last { + order: 1; + } +} + +@media (min-width: 1200px) { + .flex-items-xl-top { + align-items: flex-start; + } + .flex-items-xl-middle { + align-items: center; + } + .flex-items-xl-bottom { + align-items: flex-end; + } +} + +@media (min-width: 1200px) { + .flex-xl-top { + align-self: flex-start; + } + .flex-xl-middle { + align-self: center; + } + .flex-xl-bottom { + align-self: flex-end; + } +} + +@media (min-width: 1200px) { + .flex-items-xl-left { + justify-content: flex-start; + } + .flex-items-xl-center { + justify-content: center; + } + .flex-items-xl-right { + justify-content: flex-end; + } + .flex-items-xl-around { + justify-content: space-around; + } + .flex-items-xl-between { + justify-content: space-between; + } +} + +.tag-default { + color: #fff !important; + font-size: 0.8rem; + padding-top: 0.1rem; + padding-bottom: 0.1rem; + white-space: nowrap; + margin-right: 3px; + margin-bottom: 0.2rem; + display: inline-block; +} + +.tag-default:hover { + text-decoration: none; +} + +.tag-default.tag-outline { + border: 1px solid #ddd; + color: #aaa !important; + background: 0 0 !important; +} + +ul.tag-list { + padding-left: 0 !important; + display: inline-block; + list-style: none !important; +} + +ul.tag-list li { + display: inline-block !important; +} + +.navbar-brand { + font-family: titillium web, sans-serif; + font-size: 1.5rem !important; + padding-top: 0 !important; + margin-right: 2rem !important; + color: #ec8cb5 !important; +} + +.nav-link .user-pic { + height: 26px; + border-radius: 50px; + float: left; + margin-right: 5px; +} + +.nav-link:hover { + transition: 0.1s all; +} + +.nav-pills.outline-active .nav-link { + border-radius: 0; + border: none; + border-bottom: 2px solid transparent; + background: 0 0; + color: #aaa; +} + +.nav-pills.outline-active .nav-link:hover { + color: #ffff; +} + +.nav-pills.outline-active .nav-link.active { + background: #211f21 !important; + border-bottom: 2px solid #ec8cb5 !important; + color: #ec8cb5 !important; +} + +footer { + text-align: center; + font-size: 0.875em; + color: rgb(255, 255, 255, 0.3); + margin-top: 3rem; + padding: 1rem 0; + position: absolute; + bottom: 0; + width: 100%; +} + +footer .logo-font { + vertical-align: middle; +} + +footer .attribution { + vertical-align: middle; + margin-left: 10px; + font-size: 0.8rem; + color: #bbb; + font-weight: 300; +} + +.error-messages { + color: #b85c5c !important; + font-weight: 700; +} + +.banner { + color: #fff; + background: #ec8cb578 !important; + padding: 2rem; + margin-bottom: 2rem; +} + +.banner h1 { + text-shadow: 0 1px 3px #8c8889; + margin-bottom: 0; +} + +.container.page { + margin-top: 1.5rem; +} + +.preview-link { + color: inherit !important; +} + +.preview-link:hover { + text-decoration: inherit !important; +} + +.article-meta { + display: block; + position: relative; + font-weight: 300; +} + +.article-meta img { + display: inline-block; + vertical-align: middle; + height: 32px; + width: 32px; + border-radius: 30px; +} + +.article-meta .info { + margin: 0 1.5rem 0 0.3rem; + display: inline-block; + vertical-align: middle; + line-height: 1rem; +} + +.article-meta .info .author { + display: block; + font-weight: 500 !important; +} + +.article-meta .info .date { + color: #bbb; + font-size: 0.8rem; + display: block; +} + +.article-preview { + background-color: rgb(13, 10, 11); + border-radius: 20px; + border-top: 10px solid rgba(255, 255, 255, 0.1); + padding: 1.5rem; +} + +.article-preview .article-meta { + margin: 0 0 1rem; +} + +.article-preview .preview-link h1 { + font-weight: 600 !important; + font-size: 1.5rem !important; + margin-bottom: 3px; +} + +.article-preview .preview-link p { + font-weight: 300; + font-size: 24px; + color: #999; + margin-bottom: 15px; + font-size: 1rem; + line-height: 1.3rem; +} + +.article-preview .preview-link span { + max-width: 30%; + font-size: 0.8rem; + font-weight: 300; + color: #bbb; + vertical-align: middle; +} + +.article-preview .preview-link ul { + float: right; + max-width: 50%; + vertical-align: top; +} + +.article-preview .preview-link ul li { + font-weight: 300; + font-size: 0.8rem !important; + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.btn .counter { + font-size: 0.8rem !important; +} + +.home-page .banner { + background: #ec8cb5; + box-shadow: inset 0 8px 8px -8px #8c8889, inset 0 -8px 8px -8px #8c8889; +} + +.home-page .banner p { + color: #fff; + text-align: center; + font-size: 1.5rem; + font-weight: 300 !important; + margin-bottom: 0; +} + +.home-page .banner h1 { + text-shadow: 0 1px 3px #8c8889; + font-weight: 700 !important; + text-align: center; + font-size: 3.5rem; + padding-bottom: 0.5rem; +} + +.home-page .feed-toggle { + margin-bottom: -1px; +} + +.home-page .sidebar { + padding: 5px 10px 10px; + background: #6a7287; + border-radius: 10px; +} + +.home-page .sidebar p { + margin-bottom: 0.2rem; +} + +.article-page .banner { + padding: 2rem 0; +} + +.article-page .banner h1 { + font-size: 2.8rem; + font-weight: 600; +} + +.article-page .banner .btn { + opacity: 0.8; +} + +.article-page .banner .btn:hover { + transition: 0.1s all; + opacity: 1; +} + +.article-page .banner .article-meta { + margin: 2rem 0 0; +} + +.article-page .banner .article-meta .author { + color: #fff; +} + +.article-page .article-content p { + font-family: 'source serif pro', serif; + font-size: 1.2rem; + line-height: 1.8rem; + margin-bottom: 2rem; +} + +.article-page .article-content div { + font-size: 1.2rem; +} + +.article-page .article-content h1, +.article-page .article-content h2, +.article-page .article-content h3, +.article-page .article-content h4, +.article-page .article-content h5, +.article-page .article-content h6 { + font-weight: 500 !important; + margin: 1.6rem 0 1rem; +} + +.article-page .article-actions { + text-align: center; + margin: 1.5rem 0 3rem; +} + +.article-page .article-actions .article-meta .info { + text-align: left; +} + +.article-page .comment-form .card-block { + padding: 0; +} + +.article-page .comment-form .card-block textarea { + border: 0; + padding: 1.25rem; +} + +.article-page .comment-form .card-footer .btn { + font-weight: 700; + float: right; +} + +.article-page .comment-form .card-footer .comment-author-img { + height: 30px; + width: 30px; +} + +.article-page .card { + border: 1px solid #e5e5e5; + box-shadow: none !important; +} + +.article-page .card .card-footer { + border-top: 1px solid #e5e5e5; + box-shadow: none !important; + font-size: 0.8rem; + font-weight: 300; +} + +.article-page .card .comment-author-img { + display: inline-block; + vertical-align: middle; + height: 20px; + width: 20px; + border-radius: 30px; +} + +.article-page .card .comment-author { + display: inline-block; + vertical-align: middle; +} + +.article-page .card .date-posted { + display: inline-block; + vertical-align: middle; + margin-left: 5px; + color: #bbb; +} + +.article-page .card .mod-options { + float: right; + color: #fff; + font-size: 1rem; +} + +.article-page .card .mod-options i { + margin-left: 5px; + opacity: 0.6; + cursor: pointer; +} + +.article-page .card .mod-options i:hover { + opacity: 1; +} + +.profile-page .user-info { + text-align: center; + background: #6a7287; + padding: 2rem 0 1rem; +} + +.profile-page .user-info .user-img { + width: 100px; + height: 100px; + border-radius: 100px; + margin-bottom: 1rem; +} + +.profile-page .user-info h4 { + font-weight: 700; +} + +.profile-page .user-info p { + margin: 0 auto 0.5rem; + color: #aaa; + max-width: 450px; + font-weight: 300; +} + +.profile-page .user-info .action-btn { + float: right; + color: #999; + border: 1px solid #999; +} + +.profile-page .articles-toggle { + margin: 1.5rem 0 -1px; +} + +.editor-page .tag-list i { + font-size: 0.6rem; + margin-right: 5px; + cursor: pointer; +} + + +article, +aside, +details, +figcaption, +figure, +footer, +header, +main, +menu, +nav, +section, +summary { + display: block; +} + +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} + +audio:not([controls]) { + display: none; + height: 0; +} + +[hidden], +template { + display: none; +} + +a { + background-color: transparent; +} + +a:active { + outline: 0; +} + +a:hover { + outline: 0; +} + +abbr[title] { + border-bottom: 1px dotted; +} + +b, +strong { + font-weight: 700; +} + +dfn { + font-style: italic; +} + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +mark { + background: #ff0; + color: #000; +} + +small { + font-size: 80%; +} + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + border: 0; +} + +svg:not(:root) { + overflow: hidden; +} + +figure { + margin: 1em 40px; +} + +hr { + box-sizing: content-box; + height: 0; +} + +pre { + overflow: auto; +} + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +button, +input, +optgroup, +select, +textarea { + color: inherit; + font: inherit; + margin: 0; +} + +button { + overflow: visible; +} + +button, +select { + text-transform: none; +} + +button, +html input[type='button'], +input[type='reset'], +input[type='submit'] { + -webkit-appearance: button; + cursor: pointer; +} + +button[disabled], +html input[disabled] { + cursor: default; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +input { + line-height: normal; +} + +input[type='checkbox'], +input[type='radio'] { + box-sizing: border-box; + padding: 0; +} + +input[type='number']::-webkit-inner-spin-button, +input[type='number']::-webkit-outer-spin-button { + height: auto; +} + +input[type='search'] { + -webkit-appearance: textfield; +} + +input[type='search']::-webkit-search-cancel-button, +input[type='search']::-webkit-search-decoration { + -webkit-appearance: none; +} + +fieldset { + border: 1px solid silver; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +legend { + border: 0; + padding: 0; +} + +textarea { + overflow: auto; +} + +optgroup { + font-weight: 700; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} + +@media print { + *, + *::before, + *::after, + *::first-letter, + *::first-line { + text-shadow: none !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + abbr[title]::after { + content: ' (' attr(title) ')'; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + .navbar { + display: none; + } + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; + } + .tag { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #211f21 !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } +} + +html { + box-sizing: border-box; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +@-ms-viewport { + width: device-width; +} + +html { + font-size: 16px; + -ms-overflow-style: scrollbar; + -webkit-tap-highlight-color: transparent; +} + +body { + font-family: source sans pro, sans-serif; + font-size: 1rem; + line-height: 1.5; + color: #fff; + background-color: #211f21; +} + +[tabindex='-1']:focus { + outline: none !important; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin-top: 0; + margin-bottom: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #818a91; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: 0.5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +a { + color: #ec8cb5; + text-decoration: none; +} + +a:focus, +a:hover { + color: #ec8cb578; + text-decoration: underline; +} + +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +a:not([href]) { + color: inherit; + text-decoration: none; +} + +a:not([href]):focus, +a:not([href]):hover { + color: inherit; + text-decoration: none; +} + +a:not([href]):focus { + outline: none; +} + +pre { + margin-top: 0; + margin-bottom: 1rem; +} + +figure { + margin: 0 0 1rem; +} + +img { + vertical-align: middle; +} + +[role='button'] { + cursor: pointer; +} + +a, +area, +button, +[role='button'], +input, +label, +select, +summary, +textarea { + touch-action: manipulation; +} + +table { + background-color: transparent; +} + +caption { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + color: #818a91; + text-align: left; + caption-side: bottom; +} + +th { + text-align: left; +} + +label { + display: inline-block; + margin-bottom: 0.5rem; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +input, +button, +select, +textarea { + margin: 0; + line-height: inherit; + border-radius: 0; +} + +textarea { + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 0.5rem; + font-size: 1.5rem; + line-height: inherit; +} + +input[type='search'] { + -webkit-appearance: none; +} + +output { + display: inline-block; +} + +[hidden] { + display: none !important; +} + +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + margin-bottom: 0.5rem; + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} + +h1, +.h1 { + font-size: 2.5rem; +} + +h2, +.h2 { + font-size: 2rem; +} + +h3, +.h3 { + font-size: 1.75rem; +} + +h4, +.h4 { + font-size: 1.5rem; +} + +h5, +.h5 { + font-size: 1.25rem; +} + +h6, +.h6 { + font-size: 1rem; +} + +.lead { + font-size: 1.25rem; + font-weight: 300; +} + +.display-1 { + font-size: 6rem; + font-weight: 300; +} + +.display-2 { + font-size: 5.5rem; + font-weight: 300; +} + +.display-3 { + font-size: 4.5rem; + font-weight: 300; +} + +.display-4 { + font-size: 3.5rem; + font-weight: 300; +} + +hr { + margin-top: 1rem; + margin-bottom: 1rem; + border: 0; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +small, +.small { + font-size: 80%; + font-weight: 400; +} + +mark, +.mark { + padding: 0.2em; + background-color: #fcf8e3; +} + +.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: 5px; +} + +.initialism { + font-size: 90%; + text-transform: uppercase; +} + +.blockquote { + padding: 0.5rem 1rem; + margin-bottom: 1rem; + font-size: 1.25rem; + border-left: 0.25rem solid #eceeef; +} + +.blockquote-footer { + display: block; + font-size: 80%; + color: #818a91; +} + +.blockquote-footer::before { + content: '\2014 \00A0'; +} + +.blockquote-reverse { + padding-right: 1rem; + padding-left: 0; + text-align: right; + border-right: 0.25rem solid #eceeef; + border-left: 0; +} + +.blockquote-reverse .blockquote-footer::before { + content: ''; +} + +.blockquote-reverse .blockquote-footer::after { + content: '\00A0 \2014'; +} + +.img-fluid, +.carousel-inner > .carousel-item > img, +.carousel-inner > .carousel-item > a > img { + display: block; + max-width: 100%; + height: auto; +} + +.img-rounded { + border-radius: 0.3rem; +} + +.img-thumbnail { + padding: 0.25rem; + background-color: #211f21; + border: 1px solid #ddd; + border-radius: 0.25rem; + transition: all 0.2s ease-in-out; + display: inline-block; + max-width: 100%; + height: auto; +} + +.img-circle { + border-radius: 50%; +} + +.figure { + display: inline-block; +} + +.figure-img { + margin-bottom: 0.5rem; + line-height: 1; +} + +.figure-caption { + font-size: 90%; + color: #818a91; +} + +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, liberation mono, courier new, monospace; +} + +code { + padding: 0.2rem 0.4rem; + font-size: 90%; + color: #bd4147; + background-color: #f7f7f9; + border-radius: 0.25rem; +} + +kbd { + padding: 0.2rem 0.4rem; + font-size: 90%; + color: #fff; + background-color: #333; + border-radius: 0.2rem; +} + +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: 700; +} + +pre { + display: block; + margin-top: 0; + margin-bottom: 1rem; + font-size: 90%; + color: #fff; +} + +pre code { + padding: 0; + font-size: inherit; + color: inherit; + background-color: transparent; + border-radius: 0; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +.container { + margin-left: auto; + margin-right: auto; + padding-left: 15px; + padding-right: 15px; +} + +@media (min-width: 544px) { + .container { + max-width: 576px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 940px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} + +.container-fluid { + margin-left: auto; + margin-right: auto; + padding-left: 15px; + padding-right: 15px; +} + +.row { + display: flex; + flex-wrap: wrap; + margin-left: -15px; + margin-right: -15px; +} + +.col-xs { + position: relative; + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} + +.col-xs-1 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 8.33333%; + max-width: 8.33333%; +} + +.col-xs-2 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 16.66667%; + max-width: 16.66667%; +} + +.col-xs-3 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 25%; + max-width: 25%; +} + +.col-xs-4 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 33.33333%; + max-width: 33.33333%; +} + +.col-xs-5 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 41.66667%; + max-width: 41.66667%; +} + +.col-xs-6 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 50%; + max-width: 50%; +} + +.col-xs-7 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 58.33333%; + max-width: 58.33333%; +} + +.col-xs-8 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 66.66667%; + max-width: 66.66667%; +} + +.col-xs-9 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 75%; + max-width: 75%; +} + +.col-xs-10 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 83.33333%; + max-width: 83.33333%; +} + +.col-xs-11 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 91.66667%; + max-width: 91.66667%; +} + +.col-xs-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 100%; + max-width: 100%; +} + +.pull-xs-0 { + right: auto; +} + +.pull-xs-1 { + right: 8.33333%; +} + +.pull-xs-2 { + right: 16.66667%; +} + +.pull-xs-3 { + right: 25%; +} + +.pull-xs-4 { + right: 33.33333%; +} + +.pull-xs-5 { + right: 41.66667%; +} + +.pull-xs-6 { + right: 50%; +} + +.pull-xs-7 { + right: 58.33333%; +} + +.pull-xs-8 { + right: 66.66667%; +} + +.pull-xs-9 { + right: 75%; +} + +.pull-xs-10 { + right: 83.33333%; +} + +.pull-xs-11 { + right: 91.66667%; +} + +.pull-xs-12 { + right: 100%; +} + +.push-xs-0 { + left: auto; +} + +.push-xs-1 { + left: 8.33333%; +} + +.push-xs-2 { + left: 16.66667%; +} + +.push-xs-3 { + left: 25%; +} + +.push-xs-4 { + left: 33.33333%; +} + +.push-xs-5 { + left: 41.66667%; +} + +.push-xs-6 { + left: 50%; +} + +.push-xs-7 { + left: 58.33333%; +} + +.push-xs-8 { + left: 66.66667%; +} + +.push-xs-9 { + left: 75%; +} + +.push-xs-10 { + left: 83.33333%; +} + +.push-xs-11 { + left: 91.66667%; +} + +.push-xs-12 { + left: 100%; +} + +.offset-xs-1 { + margin-left: 8.33333%; +} + +.offset-xs-2 { + margin-left: 16.66667%; +} + +.offset-xs-3 { + margin-left: 25%; +} + +.offset-xs-4 { + margin-left: 33.33333%; +} + +.offset-xs-5 { + margin-left: 41.66667%; +} + +.offset-xs-6 { + margin-left: 50%; +} + +.offset-xs-7 { + margin-left: 58.33333%; +} + +.offset-xs-8 { + margin-left: 66.66667%; +} + +.offset-xs-9 { + margin-left: 75%; +} + +.offset-xs-10 { + margin-left: 83.33333%; +} + +.offset-xs-11 { + margin-left: 91.66667%; +} + +@media (min-width: 544px) { + .col-sm { + position: relative; + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + } + .col-sm-1 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 8.33333%; + max-width: 8.33333%; + } + .col-sm-2 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 16.66667%; + max-width: 16.66667%; + } + .col-sm-3 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 25%; + max-width: 25%; + } + .col-sm-4 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 33.33333%; + max-width: 33.33333%; + } + .col-sm-5 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 41.66667%; + max-width: 41.66667%; + } + .col-sm-6 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 50%; + max-width: 50%; + } + .col-sm-7 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 58.33333%; + max-width: 58.33333%; + } + .col-sm-8 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 66.66667%; + max-width: 66.66667%; + } + .col-sm-9 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 75%; + max-width: 75%; + } + .col-sm-10 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 83.33333%; + max-width: 83.33333%; + } + .col-sm-11 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 91.66667%; + max-width: 91.66667%; + } + .col-sm-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 100%; + max-width: 100%; + } + .pull-sm-0 { + right: auto; + } + .pull-sm-1 { + right: 8.33333%; + } + .pull-sm-2 { + right: 16.66667%; + } + .pull-sm-3 { + right: 25%; + } + .pull-sm-4 { + right: 33.33333%; + } + .pull-sm-5 { + right: 41.66667%; + } + .pull-sm-6 { + right: 50%; + } + .pull-sm-7 { + right: 58.33333%; + } + .pull-sm-8 { + right: 66.66667%; + } + .pull-sm-9 { + right: 75%; + } + .pull-sm-10 { + right: 83.33333%; + } + .pull-sm-11 { + right: 91.66667%; + } + .pull-sm-12 { + right: 100%; + } + .push-sm-0 { + left: auto; + } + .push-sm-1 { + left: 8.33333%; + } + .push-sm-2 { + left: 16.66667%; + } + .push-sm-3 { + left: 25%; + } + .push-sm-4 { + left: 33.33333%; + } + .push-sm-5 { + left: 41.66667%; + } + .push-sm-6 { + left: 50%; + } + .push-sm-7 { + left: 58.33333%; + } + .push-sm-8 { + left: 66.66667%; + } + .push-sm-9 { + left: 75%; + } + .push-sm-10 { + left: 83.33333%; + } + .push-sm-11 { + left: 91.66667%; + } + .push-sm-12 { + left: 100%; + } + .offset-sm-0 { + margin-left: 0%; + } + .offset-sm-1 { + margin-left: 8.33333%; + } + .offset-sm-2 { + margin-left: 16.66667%; + } + .offset-sm-3 { + margin-left: 25%; + } + .offset-sm-4 { + margin-left: 33.33333%; + } + .offset-sm-5 { + margin-left: 41.66667%; + } + .offset-sm-6 { + margin-left: 50%; + } + .offset-sm-7 { + margin-left: 58.33333%; + } + .offset-sm-8 { + margin-left: 66.66667%; + } + .offset-sm-9 { + margin-left: 75%; + } + .offset-sm-10 { + margin-left: 83.33333%; + } + .offset-sm-11 { + margin-left: 91.66667%; + } +} + +@media (min-width: 768px) { + .col-md { + position: relative; + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + } + .col-md-1 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 8.33333%; + max-width: 8.33333%; + } + .col-md-2 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 16.66667%; + max-width: 16.66667%; + } + .col-md-3 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 25%; + max-width: 25%; + } + .col-md-4 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 33.33333%; + max-width: 33.33333%; + } + .col-md-5 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 41.66667%; + max-width: 41.66667%; + } + .col-md-6 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 50%; + max-width: 50%; + } + .col-md-7 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 58.33333%; + max-width: 58.33333%; + } + .col-md-8 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 66.66667%; + max-width: 66.66667%; + } + .col-md-9 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 75%; + max-width: 75%; + } + .col-md-10 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 83.33333%; + max-width: 83.33333%; + } + .col-md-11 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 91.66667%; + max-width: 91.66667%; + } + .col-md-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 100%; + max-width: 100%; + } + .pull-md-0 { + right: auto; + } + .pull-md-1 { + right: 8.33333%; + } + .pull-md-2 { + right: 16.66667%; + } + .pull-md-3 { + right: 25%; + } + .pull-md-4 { + right: 33.33333%; + } + .pull-md-5 { + right: 41.66667%; + } + .pull-md-6 { + right: 50%; + } + .pull-md-7 { + right: 58.33333%; + } + .pull-md-8 { + right: 66.66667%; + } + .pull-md-9 { + right: 75%; + } + .pull-md-10 { + right: 83.33333%; + } + .pull-md-11 { + right: 91.66667%; + } + .pull-md-12 { + right: 100%; + } + .push-md-0 { + left: auto; + } + .push-md-1 { + left: 8.33333%; + } + .push-md-2 { + left: 16.66667%; + } + .push-md-3 { + left: 25%; + } + .push-md-4 { + left: 33.33333%; + } + .push-md-5 { + left: 41.66667%; + } + .push-md-6 { + left: 50%; + } + .push-md-7 { + left: 58.33333%; + } + .push-md-8 { + left: 66.66667%; + } + .push-md-9 { + left: 75%; + } + .push-md-10 { + left: 83.33333%; + } + .push-md-11 { + left: 91.66667%; + } + .push-md-12 { + left: 100%; + } + .offset-md-0 { + margin-left: 0%; + } + .offset-md-1 { + margin-left: 8.33333%; + } + .offset-md-2 { + margin-left: 16.66667%; + } + .offset-md-3 { + margin-left: 25%; + } + .offset-md-4 { + margin-left: 33.33333%; + } + .offset-md-5 { + margin-left: 41.66667%; + } + .offset-md-6 { + margin-left: 50%; + } + .offset-md-7 { + margin-left: 58.33333%; + } + .offset-md-8 { + margin-left: 66.66667%; + } + .offset-md-9 { + margin-left: 75%; + } + .offset-md-10 { + margin-left: 83.33333%; + } + .offset-md-11 { + margin-left: 91.66667%; + } +} + +@media (min-width: 992px) { + .col-lg { + position: relative; + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + } + .col-lg-1 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 8.33333%; + max-width: 8.33333%; + } + .col-lg-2 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 16.66667%; + max-width: 16.66667%; + } + .col-lg-3 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 25%; + max-width: 25%; + } + .col-lg-4 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 33.33333%; + max-width: 33.33333%; + } + .col-lg-5 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 41.66667%; + max-width: 41.66667%; + } + .col-lg-6 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 50%; + max-width: 50%; + } + .col-lg-7 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 58.33333%; + max-width: 58.33333%; + } + .col-lg-8 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 66.66667%; + max-width: 66.66667%; + } + .col-lg-9 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 75%; + max-width: 75%; + } + .col-lg-10 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 83.33333%; + max-width: 83.33333%; + } + .col-lg-11 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 91.66667%; + max-width: 91.66667%; + } + .col-lg-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 100%; + max-width: 100%; + } + .pull-lg-0 { + right: auto; + } + .pull-lg-1 { + right: 8.33333%; + } + .pull-lg-2 { + right: 16.66667%; + } + .pull-lg-3 { + right: 25%; + } + .pull-lg-4 { + right: 33.33333%; + } + .pull-lg-5 { + right: 41.66667%; + } + .pull-lg-6 { + right: 50%; + } + .pull-lg-7 { + right: 58.33333%; + } + .pull-lg-8 { + right: 66.66667%; + } + .pull-lg-9 { + right: 75%; + } + .pull-lg-10 { + right: 83.33333%; + } + .pull-lg-11 { + right: 91.66667%; + } + .pull-lg-12 { + right: 100%; + } + .push-lg-0 { + left: auto; + } + .push-lg-1 { + left: 8.33333%; + } + .push-lg-2 { + left: 16.66667%; + } + .push-lg-3 { + left: 25%; + } + .push-lg-4 { + left: 33.33333%; + } + .push-lg-5 { + left: 41.66667%; + } + .push-lg-6 { + left: 50%; + } + .push-lg-7 { + left: 58.33333%; + } + .push-lg-8 { + left: 66.66667%; + } + .push-lg-9 { + left: 75%; + } + .push-lg-10 { + left: 83.33333%; + } + .push-lg-11 { + left: 91.66667%; + } + .push-lg-12 { + left: 100%; + } + .offset-lg-0 { + margin-left: 0%; + } + .offset-lg-1 { + margin-left: 8.33333%; + } + .offset-lg-2 { + margin-left: 16.66667%; + } + .offset-lg-3 { + margin-left: 25%; + } + .offset-lg-4 { + margin-left: 33.33333%; + } + .offset-lg-5 { + margin-left: 41.66667%; + } + .offset-lg-6 { + margin-left: 50%; + } + .offset-lg-7 { + margin-left: 58.33333%; + } + .offset-lg-8 { + margin-left: 66.66667%; + } + .offset-lg-9 { + margin-left: 75%; + } + .offset-lg-10 { + margin-left: 83.33333%; + } + .offset-lg-11 { + margin-left: 91.66667%; + } +} + +@media (min-width: 1200px) { + .col-xl { + position: relative; + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + } + .col-xl-1 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 8.33333%; + max-width: 8.33333%; + } + .col-xl-2 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 16.66667%; + max-width: 16.66667%; + } + .col-xl-3 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 25%; + max-width: 25%; + } + .col-xl-4 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 33.33333%; + max-width: 33.33333%; + } + .col-xl-5 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 41.66667%; + max-width: 41.66667%; + } + .col-xl-6 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 50%; + max-width: 50%; + } + .col-xl-7 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 58.33333%; + max-width: 58.33333%; + } + .col-xl-8 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 66.66667%; + max-width: 66.66667%; + } + .col-xl-9 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 75%; + max-width: 75%; + } + .col-xl-10 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 83.33333%; + max-width: 83.33333%; + } + .col-xl-11 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 91.66667%; + max-width: 91.66667%; + } + .col-xl-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + flex: 0 0 100%; + max-width: 100%; + } + .pull-xl-0 { + right: auto; + } + .pull-xl-1 { + right: 8.33333%; + } + .pull-xl-2 { + right: 16.66667%; + } + .pull-xl-3 { + right: 25%; + } + .pull-xl-4 { + right: 33.33333%; + } + .pull-xl-5 { + right: 41.66667%; + } + .pull-xl-6 { + right: 50%; + } + .pull-xl-7 { + right: 58.33333%; + } + .pull-xl-8 { + right: 66.66667%; + } + .pull-xl-9 { + right: 75%; + } + .pull-xl-10 { + right: 83.33333%; + } + .pull-xl-11 { + right: 91.66667%; + } + .pull-xl-12 { + right: 100%; + } + .push-xl-0 { + left: auto; + } + .push-xl-1 { + left: 8.33333%; + } + .push-xl-2 { + left: 16.66667%; + } + .push-xl-3 { + left: 25%; + } + .push-xl-4 { + left: 33.33333%; + } + .push-xl-5 { + left: 41.66667%; + } + .push-xl-6 { + left: 50%; + } + .push-xl-7 { + left: 58.33333%; + } + .push-xl-8 { + left: 66.66667%; + } + .push-xl-9 { + left: 75%; + } + .push-xl-10 { + left: 83.33333%; + } + .push-xl-11 { + left: 91.66667%; + } + .push-xl-12 { + left: 100%; + } + .offset-xl-0 { + margin-left: 0%; + } + .offset-xl-1 { + margin-left: 8.33333%; + } + .offset-xl-2 { + margin-left: 16.66667%; + } + .offset-xl-3 { + margin-left: 25%; + } + .offset-xl-4 { + margin-left: 33.33333%; + } + .offset-xl-5 { + margin-left: 41.66667%; + } + .offset-xl-6 { + margin-left: 50%; + } + .offset-xl-7 { + margin-left: 58.33333%; + } + .offset-xl-8 { + margin-left: 66.66667%; + } + .offset-xl-9 { + margin-left: 75%; + } + .offset-xl-10 { + margin-left: 83.33333%; + } + .offset-xl-11 { + margin-left: 91.66667%; + } +} + +.table { + width: 100%; + max-width: 100%; + margin-bottom: 1rem; +} + +.table th, +.table td { + padding: 0.75rem; + vertical-align: top; + border-top: 1px solid #eceeef; +} + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid #eceeef; +} + +.table tbody + tbody { + border-top: 2px solid #eceeef; +} + +.table .table { + background-color: #211f21; +} + +.table-sm th, +.table-sm td { + padding: 0.3rem; +} + +.table-bordered { + border: 1px solid #eceeef; +} + +.table-bordered th, +.table-bordered td { + border: 1px solid #eceeef; +} + +.table-bordered thead th, +.table-bordered thead td { + border-bottom-width: 2px; +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: #f9f9f9; +} + +.table-hover tbody tr:hover { + background-color: #f5f5f5; +} + +.table-active, +.table-active > th, +.table-active > td { + background-color: #f5f5f5; +} + +.table-hover .table-active:hover { + background-color: #e8e8e8; +} + +.table-hover .table-active:hover > td, +.table-hover .table-active:hover > th { + background-color: #e8e8e8; +} + +.table-success, +.table-success > th, +.table-success > td { + background-color: #dff0d8; +} + +.table-hover .table-success:hover { + background-color: #d0e9c6; +} + +.table-hover .table-success:hover > td, +.table-hover .table-success:hover > th { + background-color: #d0e9c6; +} + +.table-info, +.table-info > th, +.table-info > td { + background-color: #d9edf7; +} + +.table-hover .table-info:hover { + background-color: #c4e3f3; +} + +.table-hover .table-info:hover > td, +.table-hover .table-info:hover > th { + background-color: #c4e3f3; +} + +.table-warning, +.table-warning > th, +.table-warning > td { + background-color: #fcf8e3; +} + +.table-hover .table-warning:hover { + background-color: #faf2cc; +} + +.table-hover .table-warning:hover > td, +.table-hover .table-warning:hover > th { + background-color: #faf2cc; +} + +.table-danger, +.table-danger > th, +.table-danger > td { + background-color: #f2dede; +} + +.table-hover .table-danger:hover { + background-color: #ebcccc; +} + +.table-hover .table-danger:hover > td, +.table-hover .table-danger:hover > th { + background-color: #ebcccc; +} + +.table-responsive { + display: block; + width: 100%; + min-height: 0.01%; + overflow-x: auto; +} + +.thead-inverse th { + color: #fff; + background-color: #fff; +} + +.thead-default th { + color: #55595c; + background-color: #eceeef; +} + +.table-inverse { + color: #eceeef; + background-color: #fff; +} + +.table-inverse.table-bordered { + border: 0; +} + +.table-inverse th, +.table-inverse td, +.table-inverse thead th { + border-color: #55595c; +} + +.table-reflow thead { + float: left; +} + +.table-reflow tbody { + display: block; + white-space: nowrap; +} + +.table-reflow th, +.table-reflow td { + border-top: 1px solid #eceeef; + border-left: 1px solid #eceeef; +} + +.table-reflow th:last-child, +.table-reflow td:last-child { + border-right: 1px solid #eceeef; +} + +.table-reflow thead:last-child tr:last-child th, +.table-reflow thead:last-child tr:last-child td, +.table-reflow tbody:last-child tr:last-child th, +.table-reflow tbody:last-child tr:last-child td, +.table-reflow tfoot:last-child tr:last-child th, +.table-reflow tfoot:last-child tr:last-child td { + border-bottom: 1px solid #eceeef; +} + +.table-reflow tr { + float: left; +} + +.table-reflow tr th, +.table-reflow tr td { + display: block !important; + border: 1px solid #eceeef; +} + +.form-control { + display: block; + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 1rem; + line-height: 1.25; + background-color: #333; + background-image: none; + background-clip: padding-box; + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 0.25rem; +} + +.form-control::-ms-expand { + background-color: transparent; + border: 0; +} + +.form-control:focus { + border-color: #66afe9; + outline: none; +} + +.form-control::placeholder { + color: #999; + opacity: 1; +} + +.form-control:disabled, +.form-control[readonly] { + background-color: #444; + opacity: 1; +} + +.form-control:disabled { + cursor: not-allowed; +} + +select.form-control:not([size]):not([multiple]) { + height: 2.5rem; +} + +.form-control-file, +.form-control-range { + display: block; +} + +.form-control-label { + padding: 0.5rem 0.75rem; + margin-bottom: 0; +} + +.form-control-legend { + padding: 0.5rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; +} + +_::-webkit-full-page-media.form-control, +input[type='date'].form-control, +input[type='time'].form-control, +input[type='datetime-local'].form-control, +input[type='month'].form-control { + line-height: 2.5rem; +} + +_::-webkit-full-page-media.input-sm, +.input-group-sm _::-webkit-full-page-media.form-control, +input[type='date'].input-sm, +.input-group-sm input[type='date'].form-control, +input[type='time'].input-sm, +.input-group-sm input[type='time'].form-control, +input[type='datetime-local'].input-sm, +.input-group-sm input[type='datetime-local'].form-control, +input[type='month'].input-sm, +.input-group-sm input[type='month'].form-control { + line-height: 1.8125rem; +} + +_::-webkit-full-page-media.input-lg, +.input-group-lg _::-webkit-full-page-media.form-control, +input[type='date'].input-lg, +.input-group-lg input[type='date'].form-control, +input[type='time'].input-lg, +.input-group-lg input[type='time'].form-control, +input[type='datetime-local'].input-lg, +.input-group-lg input[type='datetime-local'].form-control, +input[type='month'].input-lg, +.input-group-lg input[type='month'].form-control { + line-height: 3.16667rem; +} + +.form-control-static { + min-height: 2.5rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + margin-bottom: 0; +} + +.form-control-static.form-control-sm, +.input-group-sm > .form-control-static.form-control, +.input-group-sm > .form-control-static.input-group-addon, +.input-group-sm > .input-group-btn > .form-control-static.btn, +.form-control-static.form-control-lg, +.input-group-lg > .form-control-static.form-control, +.input-group-lg > .form-control-static.input-group-addon, +.input-group-lg > .input-group-btn > .form-control-static.btn { + padding-right: 0; + padding-left: 0; +} + +.form-control-sm, +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 0.2rem; +} + +.form-control-lg, +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; + border-radius: 0.3rem; +} + +.form-group { + margin-bottom: 1rem; +} + +.radio, +.checkbox { + position: relative; + display: block; + margin-bottom: 0.75rem; +} + +.radio label, +.checkbox label { + padding-left: 1.25rem; + margin-bottom: 0; + cursor: pointer; +} + +.radio label input:only-child, +.checkbox label input:only-child { + position: static; +} + +.radio input[type='radio'], +.radio-inline input[type='radio'], +.checkbox input[type='checkbox'], +.checkbox-inline input[type='checkbox'] { + position: absolute; + margin-top: 0.25rem; + margin-left: -1.25rem; +} + +.radio + .radio, +.checkbox + .checkbox { + margin-top: -0.25rem; +} + +.radio-inline, +.checkbox-inline { + position: relative; + display: inline-block; + padding-left: 1.25rem; + margin-bottom: 0; + vertical-align: middle; + cursor: pointer; +} + +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 0.75rem; +} + +input[type='radio']:disabled, +input[type='radio'].disabled, +input[type='checkbox']:disabled, +input[type='checkbox'].disabled { + cursor: not-allowed; +} + +.radio-inline.disabled, +.checkbox-inline.disabled { + cursor: not-allowed; +} + +.radio.disabled label, +.checkbox.disabled label { + cursor: not-allowed; +} + +.form-control-success, +.form-control-warning, +.form-control-danger { + padding-right: 2.25rem; + background-repeat: no-repeat; + background-position: center right 0.625rem; + background-size: 1.25rem 1.25rem; +} + +.has-success .text-help, +.has-success .form-control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline, +.has-success.radio label, +.has-success.checkbox label, +.has-success.radio-inline label, +.has-success.checkbox-inline label, +.has-success .custom-control { + color: #ec8cb5; +} + +.has-success .form-control { + border-color: #ec8cb5; +} + +.has-success .input-group-addon { + color: #ec8cb5; + border-color: #ec8cb5; + background-color: #eaf6ea; +} + +.has-success .form-control-feedback { + color: #ec8cb5; +} + +.has-success .form-control-success { + background-image: url(data:image/svg+xml;charset=utf8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA4IDgnPjxwYXRoIGZpbGw9JyM1Y2I4NWMnIGQ9J00yLjMgNi43M0wuNiA0LjUzYy0uNC0xLjA0LjQ2LTEuNCAxLjEtLjhsMS4xIDEuNCAzLjQtMy44Yy42LS42MyAxLjYtLjI3IDEuMi43bC00IDQuNmMtLjQzLjUtLjguNC0xLjEuMXonLz48L3N2Zz4=); +} + +.has-warning .text-help, +.has-warning .form-control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline, +.has-warning.radio label, +.has-warning.checkbox label, +.has-warning.radio-inline label, +.has-warning.checkbox-inline label, +.has-warning .custom-control { + color: #f0ad4e; +} + +.has-warning .form-control { + border-color: #f0ad4e; +} + +.has-warning .input-group-addon { + color: #f0ad4e; + border-color: #f0ad4e; + background-color: #211f21; +} + +.has-warning .form-control-feedback { + color: #f0ad4e; +} + +.has-warning .form-control-warning { + background-image: url(data:image/svg+xml;charset=utf8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA4IDgnPjxwYXRoIGZpbGw9JyNmMGFkNGUnIGQ9J000LjQgNS4zMjRoLS44di0yLjQ2aC44em0wIDEuNDJoLS44VjUuODloLjh6TTMuNzYuNjNMLjA0IDcuMDc1Yy0uMTE1LjIuMDE2LjQyNS4yNi40MjZoNy4zOTdjLjI0MiAwIC4zNzItLjIyNi4yNTgtLjQyNkM2LjcyNiA0LjkyNCA1LjQ3IDIuNzkgNC4yNTMuNjNjLS4xMTMtLjE3NC0uMzktLjE3NC0uNDk0IDB6Jy8+PC9zdmc+); +} + +.has-danger .text-help, +.has-danger .form-control-label, +.has-danger .radio, +.has-danger .checkbox, +.has-danger .radio-inline, +.has-danger .checkbox-inline, +.has-danger.radio label, +.has-danger.checkbox label, +.has-danger.radio-inline label, +.has-danger.checkbox-inline label, +.has-danger .custom-control { + color: #b85c5c; +} + +.has-danger .form-control { + border-color: #b85c5c; +} + +.has-danger .input-group-addon { + color: #b85c5c; + border-color: #b85c5c; + background-color: #f6eaea; +} + +.has-danger .form-control-feedback { + color: #b85c5c; +} + +.has-danger .form-control-danger { + background-image: url(data:image/svg+xml;charset=utf8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIGZpbGw9JyNkOTUzNGYnIHZpZXdCb3g9Jy0yIC0yIDcgNyc+PHBhdGggc3Ryb2tlPScjZDk1MzRmJyBkPSdNMCAwbDMgM20wLTNMMCAzJy8+PGNpcmNsZSByPScuNScvPjxjaXJjbGUgY3g9JzMnIHI9Jy41Jy8+PGNpcmNsZSBjeT0nMycgcj0nLjUnLz48Y2lyY2xlIGN4PSczJyBjeT0nMycgcj0nLjUnLz48L3N2Zz4=); +} + +@media (min-width: 544px) { + .form-inline .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-static { + display: inline-block; + } + .form-inline .input-group { + display: inline-table; + vertical-align: middle; + } + .form-inline .input-group .input-group-addon, + .form-inline .input-group .input-group-btn, + .form-inline .input-group .form-control { + width: auto; + } + .form-inline .input-group > .form-control { + width: 100%; + } + .form-inline .form-control-label { + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio, + .form-inline .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio label, + .form-inline .checkbox label { + padding-left: 0; + } + .form-inline .radio input[type='radio'], + .form-inline .checkbox input[type='checkbox'] { + position: relative; + margin-left: 0; + } + .form-inline .has-feedback .form-control-feedback { + top: 0; + } +} + +.btn { + display: inline-block; + font-weight: 400; + line-height: 1.25; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + user-select: none; + border: 1px solid transparent; + padding: 0.5rem 1rem; + font-size: 1rem; + border-radius: 0.25rem; +} + +.btn:focus, +.btn.focus, +.btn:active:focus, +.btn:active.focus, +.btn.active:focus, +.btn.active.focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn:focus, +.btn:hover { + text-decoration: none; +} + +.btn.focus { + text-decoration: none; +} + +.btn:active, +.btn.active { + background-image: none; + outline: 0; +} + +.btn.disabled, +.btn:disabled { + cursor: not-allowed; + opacity: 0.65; +} + +a.btn.disabled, +fieldset[disabled] a.btn { + pointer-events: none; +} + +.btn-primary { + color: #fff; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-primary:hover { + color: #fff; + background-color: #7a8992; + border-color: #8ea1ac; +} + +.btn-primary:focus, +.btn-primary.focus { + color: #fff; + background-color: #7a8992; + border-color: #8ea1ac; +} + +.btn-primary:active, +.btn-primary.active, +.open > .btn-primary.dropdown-toggle { + color: #fff; + background-color: #7a8992; + border-color: #8ea1ac; + background-image: none; +} + +.btn-primary:active:hover, +.btn-primary:active:focus, +.btn-primary:active.focus, +.btn-primary.active:hover, +.btn-primary.active:focus, +.btn-primary.active.focus, +.open > .btn-primary.dropdown-toggle:hover, +.open > .btn-primary.dropdown-toggle:focus, +.open > .btn-primary.dropdown-toggle.focus { + color: #fff; + background-color: #ec8cb56d; + border-color: #2d672d; +} + +.btn-primary.disabled:focus, +.btn-primary.disabled.focus, +.btn-primary:disabled:focus, +.btn-primary:disabled.focus { + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-primary.disabled:hover, +.btn-primary:disabled:hover { + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-secondary { + color: #fff; + background-color: #211f21; + border-color: #ccc; +} + +.btn-secondary:hover { + color: #fff; + background-color: #e6e6e6; + border-color: #adadad; +} + +.btn-secondary:focus, +.btn-secondary.focus { + color: #fff; + background-color: #e6e6e6; + border-color: #adadad; +} + +.btn-secondary:active, +.btn-secondary.active, +.open > .btn-secondary.dropdown-toggle { + color: #fff; + background-color: #e6e6e6; + border-color: #adadad; + background-image: none; +} + +.btn-secondary:active:hover, +.btn-secondary:active:focus, +.btn-secondary:active.focus, +.btn-secondary.active:hover, +.btn-secondary.active:focus, +.btn-secondary.active.focus, +.open > .btn-secondary.dropdown-toggle:hover, +.open > .btn-secondary.dropdown-toggle:focus, +.open > .btn-secondary.dropdown-toggle.focus { + color: #fff; + background-color: #d4d4d4; + border-color: #8c8c8c; +} + +.btn-secondary.disabled:focus, +.btn-secondary.disabled.focus, +.btn-secondary:disabled:focus, +.btn-secondary:disabled.focus { + background-color: #211f21; + border-color: #ccc; +} + +.btn-secondary.disabled:hover, +.btn-secondary:disabled:hover { + background-color: #211f21; + border-color: #ccc; +} + +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #5bc0de; +} + +.btn-info:hover { + color: #fff; + background-color: #31b0d5; + border-color: #2aabd2; +} + +.btn-info:focus, +.btn-info.focus { + color: #fff; + background-color: #31b0d5; + border-color: #2aabd2; +} + +.btn-info:active, +.btn-info.active, +.open > .btn-info.dropdown-toggle { + color: #fff; + background-color: #31b0d5; + border-color: #2aabd2; + background-image: none; +} + +.btn-info:active:hover, +.btn-info:active:focus, +.btn-info:active.focus, +.btn-info.active:hover, +.btn-info.active:focus, +.btn-info.active.focus, +.open > .btn-info.dropdown-toggle:hover, +.open > .btn-info.dropdown-toggle:focus, +.open > .btn-info.dropdown-toggle.focus { + color: #fff; + background-color: #269abc; + border-color: #1f7e9a; +} + +.btn-info.disabled:focus, +.btn-info.disabled.focus, +.btn-info:disabled:focus, +.btn-info:disabled.focus { + background-color: #5bc0de; + border-color: #5bc0de; +} + +.btn-info.disabled:hover, +.btn-info:disabled:hover { + background-color: #5bc0de; + border-color: #5bc0de; +} + +.btn-success { + color: #fff; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-success:hover { + color: #fff; + background-color: #7a8992; + border-color: #8ea1ac; +} + +.btn-success:focus, +.btn-success.focus { + color: #fff; + background-color: #7a8992; + border-color: #8ea1ac; +} + +.btn-success:active, +.btn-success.active, +.open > .btn-success.dropdown-toggle { + color: #fff; + background-color: #7a8992; + border-color: #8ea1ac; + background-image: none; +} + +.btn-success:active:hover, +.btn-success:active:focus, +.btn-success:active.focus, +.btn-success.active:hover, +.btn-success.active:focus, +.btn-success.active.focus, +.open > .btn-success.dropdown-toggle:hover, +.open > .btn-success.dropdown-toggle:focus, +.open > .btn-success.dropdown-toggle.focus { + color: #fff; + background-color: #ec8cb56d; + border-color: #2d672d; +} + +.btn-success.disabled:focus, +.btn-success.disabled.focus, +.btn-success:disabled:focus, +.btn-success:disabled.focus { + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-success.disabled:hover, +.btn-success:disabled:hover { + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-warning { + color: #fff; + background-color: #f0ad4e; + border-color: #f0ad4e; +} + +.btn-warning:hover { + color: #fff; + background-color: #ec971f; + border-color: #eb9316; +} + +.btn-warning:focus, +.btn-warning.focus { + color: #fff; + background-color: #ec971f; + border-color: #eb9316; +} + +.btn-warning:active, +.btn-warning.active, +.open > .btn-warning.dropdown-toggle { + color: #fff; + background-color: #ec971f; + border-color: #eb9316; + background-image: none; +} + +.btn-warning:active:hover, +.btn-warning:active:focus, +.btn-warning:active.focus, +.btn-warning.active:hover, +.btn-warning.active:focus, +.btn-warning.active.focus, +.open > .btn-warning.dropdown-toggle:hover, +.open > .btn-warning.dropdown-toggle:focus, +.open > .btn-warning.dropdown-toggle.focus { + color: #fff; + background-color: #d58512; + border-color: #b06d0f; +} + +.btn-warning.disabled:focus, +.btn-warning.disabled.focus, +.btn-warning:disabled:focus, +.btn-warning:disabled.focus { + background-color: #f0ad4e; + border-color: #f0ad4e; +} + +.btn-warning.disabled:hover, +.btn-warning:disabled:hover { + background-color: #f0ad4e; + border-color: #f0ad4e; +} + +.btn-danger { + color: #fff; + background-color: #b85c5c; + border-color: #b85c5c; +} + +.btn-danger:hover { + color: #fff; + background-color: #9d4444; + border-color: #964141; +} + +.btn-danger:focus, +.btn-danger.focus { + color: #fff; + background-color: #9d4444; + border-color: #964141; +} + +.btn-danger:active, +.btn-danger.active, +.open > .btn-danger.dropdown-toggle { + color: #fff; + background-color: #9d4444; + border-color: #964141; + background-image: none; +} + +.btn-danger:active:hover, +.btn-danger:active:focus, +.btn-danger:active.focus, +.btn-danger.active:hover, +.btn-danger.active:focus, +.btn-danger.active.focus, +.open > .btn-danger.dropdown-toggle:hover, +.open > .btn-danger.dropdown-toggle:focus, +.open > .btn-danger.dropdown-toggle.focus { + color: #fff; + background-color: #843939; + border-color: #672d2d; +} + +.btn-danger.disabled:focus, +.btn-danger.disabled.focus, +.btn-danger:disabled:focus, +.btn-danger:disabled.focus { + background-color: #b85c5c; + border-color: #b85c5c; +} + +.btn-danger.disabled:hover, +.btn-danger:disabled:hover { + background-color: #b85c5c; + border-color: #b85c5c; +} + +.btn-outline-primary { + color: #ec8cb5; + background-image: none; + background-color: transparent; + border-color: #ec8cb5; +} + +.btn-outline-primary:hover { + color: #fff; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-outline-primary:focus, +.btn-outline-primary.focus { + color: #fff; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-outline-primary:active, +.btn-outline-primary.active, +.open > .btn-outline-primary.dropdown-toggle { + color: #fff; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-outline-primary:active:hover, +.btn-outline-primary:active:focus, +.btn-outline-primary:active.focus, +.btn-outline-primary.active:hover, +.btn-outline-primary.active:focus, +.btn-outline-primary.active.focus, +.open > .btn-outline-primary.dropdown-toggle:hover, +.open > .btn-outline-primary.dropdown-toggle:focus, +.open > .btn-outline-primary.dropdown-toggle.focus { + color: #fff; + background-color: #ec8cb56d; + border-color: #2d672d; +} + +.btn-outline-primary.disabled:focus, +.btn-outline-primary.disabled.focus, +.btn-outline-primary:disabled:focus, +.btn-outline-primary:disabled.focus { + border-color: #a3d7a3; +} + +.btn-outline-primary.disabled:hover, +.btn-outline-primary:disabled:hover { + border-color: #a3d7a3; +} + +.btn-outline-secondary { + color: #ccc; + background-image: none; + background-color: transparent; + border-color: #ccc; +} + +.btn-outline-secondary:hover { + color: #fff; + background-color: #ccc; + border-color: #ccc; +} + +.btn-outline-secondary:focus, +.btn-outline-secondary.focus { + color: #fff; + background-color: #ccc; + border-color: #ccc; +} + +.btn-outline-secondary:active, +.btn-outline-secondary.active, +.open > .btn-outline-secondary.dropdown-toggle { + color: #fff; + background-color: #ccc; + border-color: #ccc; +} + +.btn-outline-secondary:active:hover, +.btn-outline-secondary:active:focus, +.btn-outline-secondary:active.focus, +.btn-outline-secondary.active:hover, +.btn-outline-secondary.active:focus, +.btn-outline-secondary.active.focus, +.open > .btn-outline-secondary.dropdown-toggle:hover, +.open > .btn-outline-secondary.dropdown-toggle:focus, +.open > .btn-outline-secondary.dropdown-toggle.focus { + color: #fff; + background-color: #a1a1a1; + border-color: #8c8c8c; +} + +.btn-outline-secondary.disabled:focus, +.btn-outline-secondary.disabled.focus, +.btn-outline-secondary:disabled:focus, +.btn-outline-secondary:disabled.focus { + border-color: #fff; +} + +.btn-outline-secondary.disabled:hover, +.btn-outline-secondary:disabled:hover { + border-color: #fff; +} + +.btn-outline-info { + color: #5bc0de; + background-image: none; + background-color: transparent; + border-color: #5bc0de; +} + +.btn-outline-info:hover { + color: #fff; + background-color: #5bc0de; + border-color: #5bc0de; +} + +.btn-outline-info:focus, +.btn-outline-info.focus { + color: #fff; + background-color: #5bc0de; + border-color: #5bc0de; +} + +.btn-outline-info:active, +.btn-outline-info.active, +.open > .btn-outline-info.dropdown-toggle { + color: #fff; + background-color: #5bc0de; + border-color: #5bc0de; +} + +.btn-outline-info:active:hover, +.btn-outline-info:active:focus, +.btn-outline-info:active.focus, +.btn-outline-info.active:hover, +.btn-outline-info.active:focus, +.btn-outline-info.active.focus, +.open > .btn-outline-info.dropdown-toggle:hover, +.open > .btn-outline-info.dropdown-toggle:focus, +.open > .btn-outline-info.dropdown-toggle.focus { + color: #fff; + background-color: #269abc; + border-color: #1f7e9a; +} + +.btn-outline-info.disabled:focus, +.btn-outline-info.disabled.focus, +.btn-outline-info:disabled:focus, +.btn-outline-info:disabled.focus { + border-color: #b0e1ef; +} + +.btn-outline-info.disabled:hover, +.btn-outline-info:disabled:hover { + border-color: #b0e1ef; +} + +.btn-outline-success { + color: #ec8cb5; + background-image: none; + background-color: transparent; + border-color: #ec8cb5; +} + +.btn-outline-success:hover { + color: #fff; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-outline-success:focus, +.btn-outline-success.focus { + color: #fff; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-outline-success:active, +.btn-outline-success.active, +.open > .btn-outline-success.dropdown-toggle { + color: #fff; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.btn-outline-success:active:hover, +.btn-outline-success:active:focus, +.btn-outline-success:active.focus, +.btn-outline-success.active:hover, +.btn-outline-success.active:focus, +.btn-outline-success.active.focus, +.open > .btn-outline-success.dropdown-toggle:hover, +.open > .btn-outline-success.dropdown-toggle:focus, +.open > .btn-outline-success.dropdown-toggle.focus { + color: #fff; + background-color: #ec8cb56d; + border-color: #2d672d; +} + +.btn-outline-success.disabled:focus, +.btn-outline-success.disabled.focus, +.btn-outline-success:disabled:focus, +.btn-outline-success:disabled.focus { + border-color: #a3d7a3; +} + +.btn-outline-success.disabled:hover, +.btn-outline-success:disabled:hover { + border-color: #a3d7a3; +} + +.btn-outline-warning { + color: #f0ad4e; + background-image: none; + background-color: transparent; + border-color: #f0ad4e; +} + +.btn-outline-warning:hover { + color: #fff; + background-color: #f0ad4e; + border-color: #f0ad4e; +} + +.btn-outline-warning:focus, +.btn-outline-warning.focus { + color: #fff; + background-color: #f0ad4e; + border-color: #f0ad4e; +} + +.btn-outline-warning:active, +.btn-outline-warning.active, +.open > .btn-outline-warning.dropdown-toggle { + color: #fff; + background-color: #f0ad4e; + border-color: #f0ad4e; +} + +.btn-outline-warning:active:hover, +.btn-outline-warning:active:focus, +.btn-outline-warning:active.focus, +.btn-outline-warning.active:hover, +.btn-outline-warning.active:focus, +.btn-outline-warning.active.focus, +.open > .btn-outline-warning.dropdown-toggle:hover, +.open > .btn-outline-warning.dropdown-toggle:focus, +.open > .btn-outline-warning.dropdown-toggle.focus { + color: #fff; + background-color: #d58512; + border-color: #b06d0f; +} + +.btn-outline-warning.disabled:focus, +.btn-outline-warning.disabled.focus, +.btn-outline-warning:disabled:focus, +.btn-outline-warning:disabled.focus { + border-color: #f8d9ac; +} + +.btn-outline-warning.disabled:hover, +.btn-outline-warning:disabled:hover { + border-color: #f8d9ac; +} + +.btn-outline-danger { + color: #b85c5c; + background-image: none; + background-color: transparent; + border-color: #b85c5c; +} + +.btn-outline-danger:hover { + color: #fff; + background-color: #b85c5c; + border-color: #b85c5c; +} + +.btn-outline-danger:focus, +.btn-outline-danger.focus { + color: #fff; + background-color: #b85c5c; + border-color: #b85c5c; +} + +.btn-outline-danger:active, +.btn-outline-danger.active, +.open > .btn-outline-danger.dropdown-toggle { + color: #fff; + background-color: #b85c5c; + border-color: #b85c5c; +} + +.btn-outline-danger:active:hover, +.btn-outline-danger:active:focus, +.btn-outline-danger:active.focus, +.btn-outline-danger.active:hover, +.btn-outline-danger.active:focus, +.btn-outline-danger.active.focus, +.open > .btn-outline-danger.dropdown-toggle:hover, +.open > .btn-outline-danger.dropdown-toggle:focus, +.open > .btn-outline-danger.dropdown-toggle.focus { + color: #fff; + background-color: #843939; + border-color: #672d2d; +} + +.btn-outline-danger.disabled:focus, +.btn-outline-danger.disabled.focus, +.btn-outline-danger:disabled:focus, +.btn-outline-danger:disabled.focus { + border-color: #d7a3a3; +} + +.btn-outline-danger.disabled:hover, +.btn-outline-danger:disabled:hover { + border-color: #d7a3a3; +} + +.btn-link { + font-weight: 400; + color: #ec8cb5; + border-radius: 0; +} + +.btn-link, +.btn-link:active, +.btn-link.active, +.btn-link:disabled { + background-color: transparent; +} + +.btn-link, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} + +.btn-link:hover { + border-color: transparent; +} + +.btn-link:focus, +.btn-link:hover { + color: #3d8b3d; + text-decoration: underline; + background-color: transparent; +} + +.btn-link:disabled:focus, +.btn-link:disabled:hover { + color: #818a91; + text-decoration: none; +} + +.btn-lg, +.btn-group-lg > .btn { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; + border-radius: 0.3rem; +} + +.btn-sm, +.btn-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 0.2rem; +} + +.btn-block { + display: block; + width: 100%; +} + +.btn-block + .btn-block { + margin-top: 5px; +} + +input[type='submit'].btn-block, +input[type='reset'].btn-block, +input[type='button'].btn-block { + width: 100%; +} + +.fade { + opacity: 0; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.collapse { + display: none; +} + +.collapse.in { + display: block; +} + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + transition-timing-function: ease; + transition-duration: 0.35s; + transition-property: height; +} + +.dropup, +.dropdown { + position: relative; +} + +.dropdown-toggle::after { + display: inline-block; + width: 0; + height: 0; + margin-right: 0.25rem; + margin-left: 0.25rem; + vertical-align: middle; + content: ''; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-left: 0.3em solid transparent; +} + +.dropdown-toggle:focus { + outline: 0; +} + +.dropup .dropdown-toggle::after { + border-top: 0; + border-bottom: 0.3em solid; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 1rem; + color: #fff; + text-align: left; + list-style: none; + background-color: #211f21; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; +} + +.dropdown-divider { + height: 1px; + margin: 0.5rem 0; + overflow: hidden; + background-color: #e5e5e5; +} + +.dropdown-item { + display: block; + width: 100%; + padding: 3px 20px; + clear: both; + font-weight: 400; + color: #fff; + text-align: inherit; + white-space: nowrap; + background: 0 0; + border: 0; +} + +.dropdown-item:focus, +.dropdown-item:hover { + color: #2b2d2f; + text-decoration: none; + background-color: #f5f5f5; +} + +.dropdown-item.active, +.dropdown-item.active:focus, +.dropdown-item.active:hover { + color: #fff; + text-decoration: none; + background-color: #ec8cb5; + outline: 0; +} + +.dropdown-item.disabled, +.dropdown-item.disabled:focus, +.dropdown-item.disabled:hover { + color: #818a91; +} + +.dropdown-item.disabled:focus, +.dropdown-item.disabled:hover { + text-decoration: none; + cursor: not-allowed; + background-color: transparent; + background-image: none; + filter: 'progid:DXImageTransform.Microsoft.gradient(enabled = false)'; +} + +.open > .dropdown-menu { + display: block; +} + +.open > a { + outline: 0; +} + +.dropdown-menu-right { + right: 0; + left: auto; +} + +.dropdown-menu-left { + right: auto; + left: 0; +} + +.dropdown-header { + display: block; + padding: 5px 20px; + font-size: 0.875rem; + color: #818a91; + white-space: nowrap; +} + +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + content: ''; + border-top: 0; + border-bottom: 0.3em solid; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 2px; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} + +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} + +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn:focus, +.btn-group-vertical > .btn:active, +.btn-group-vertical > .btn.active { + z-index: 2; +} + +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover { + z-index: 2; +} + +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} + +.btn-toolbar { + margin-left: -5px; +} + +.btn-toolbar::after { + content: ''; + display: table; + clear: both; +} + +.btn-toolbar .btn-group, +.btn-toolbar .input-group { + float: left; +} + +.btn-toolbar > .btn, +.btn-toolbar > .btn-group, +.btn-toolbar > .input-group { + margin-left: 5px; +} + +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} + +.btn-group > .btn:first-child { + margin-left: 0; +} + +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} + +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.btn-group > .btn-group { + float: left; +} + +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} + +.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} + +.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group > .btn + .dropdown-toggle { + padding-right: 8px; + padding-left: 8px; +} + +.btn-group > .btn-lg + .dropdown-toggle, +.btn-group-lg.btn-group > .btn + .dropdown-toggle { + padding-right: 12px; + padding-left: 12px; +} + +.btn .caret { + margin-left: 0; +} + +.btn-lg .caret, +.btn-group-lg > .btn .caret { + border-width: 0.3em 0.3em 0; + border-bottom-width: 0; +} + +.dropup .btn-lg .caret, +.dropup .btn-group-lg > .btn .caret { + border-width: 0 0.3em 0.3em; +} + +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; +} + +.btn-group-vertical > .btn-group::after { + content: ''; + display: table; + clear: both; +} + +.btn-group-vertical > .btn-group > .btn { + float: none; +} + +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} + +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} + +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical + > .btn-group:first-child:not(:last-child) + > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical + > .btn-group:last-child:not(:first-child) + > .btn:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +[data-toggle='buttons'] > .btn input[type='radio'], +[data-toggle='buttons'] > .btn input[type='checkbox'], +[data-toggle='buttons'] > .btn-group > .btn input[type='radio'], +[data-toggle='buttons'] > .btn-group > .btn input[type='checkbox'] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} + +.input-group { + position: relative; + width: 100%; + display: flex; +} + +.input-group .form-control { + position: relative; + z-index: 2; + flex: 1; + margin-bottom: 0; +} + +.input-group .form-control:focus, +.input-group .form-control:active, +.input-group .form-control:hover { + z-index: 3; +} + +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} + +.input-group-addon, +.input-group-btn { + white-space: nowrap; + vertical-align: middle; +} + +.input-group-addon { + padding: 0.5rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; + font-weight: 400; + line-height: 1.25; + color: #55595c; + text-align: center; + background-color: #eceeef; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; +} + +.input-group-addon.form-control-sm, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .input-group-addon.btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 0.2rem; +} + +.input-group-addon.form-control-lg, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .input-group-addon.btn { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; + border-radius: 0.3rem; +} + +.input-group-addon input[type='radio'], +.input-group-addon input[type='checkbox'] { + margin-top: 0; +} + +.input-group .form-control:not(:last-child), +.input-group-addon:not(:last-child), +.input-group-btn:not(:last-child) > .btn, +.input-group-btn:not(:last-child) > .btn-group > .btn, +.input-group-btn:not(:last-child) > .dropdown-toggle, +.input-group-btn:not(:first-child) + > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:not(:first-child) > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} + +.input-group-addon:not(:last-child) { + border-right: 0; +} + +.input-group .form-control:not(:first-child), +.input-group-addon:not(:first-child), +.input-group-btn:not(:first-child) > .btn, +.input-group-btn:not(:first-child) > .btn-group > .btn, +.input-group-btn:not(:first-child) > .dropdown-toggle, +.input-group-btn:not(:last-child) > .btn:not(:first-child), +.input-group-btn:not(:last-child) > .btn-group:not(:first-child) > .btn { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.form-control + .input-group-addon:not(:first-child) { + border-left: 0; +} + +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; +} + +.input-group-btn > .btn { + position: relative; +} + +.input-group-btn > .btn + .btn { + margin-left: -1px; +} + +.input-group-btn > .btn:focus, +.input-group-btn > .btn:active, +.input-group-btn > .btn:hover { + z-index: 3; +} + +.input-group-btn:not(:last-child) > .btn, +.input-group-btn:not(:last-child) > .btn-group { + margin-right: -1px; +} + +.input-group-btn:not(:first-child) > .btn, +.input-group-btn:not(:first-child) > .btn-group { + z-index: 2; + margin-left: -1px; +} + +.input-group-btn:not(:first-child) > .btn:focus, +.input-group-btn:not(:first-child) > .btn:active, +.input-group-btn:not(:first-child) > .btn:hover, +.input-group-btn:not(:first-child) > .btn-group:focus, +.input-group-btn:not(:first-child) > .btn-group:active, +.input-group-btn:not(:first-child) > .btn-group:hover { + z-index: 3; +} + +.custom-control { + position: relative; + display: inline; + padding-left: 1.5rem; + cursor: pointer; +} + +.custom-control + .custom-control { + margin-left: 1rem; +} + +.custom-control-input { + position: absolute; + z-index: -1; + opacity: 0; +} + +.custom-control-input:checked ~ .custom-control-indicator { + color: #fff; + background-color: #0074d9; +} + +.custom-control-input:focus ~ .custom-control-indicator { + box-shadow: 0 0 0 0.075rem #fff, 0 0 0 0.2rem #0074d9; +} + +.custom-control-input:active ~ .custom-control-indicator { + color: #fff; + background-color: #84c6ff; +} + +.custom-control-input:disabled ~ .custom-control-indicator { + cursor: not-allowed; + background-color: #eee; +} + +.custom-control-input:disabled ~ .custom-control-description { + color: #767676; + cursor: not-allowed; +} + +.custom-control-indicator { + position: absolute; + top: 0.0625rem; + left: 0; + display: block; + width: 1rem; + height: 1rem; + pointer-events: none; + user-select: none; + background-color: #ddd; + background-repeat: no-repeat; + background-position: center center; + background-size: 50% 50%; +} + +.custom-checkbox .custom-control-indicator { + border-radius: 0.25rem; +} + +.custom-checkbox .custom-control-input:checked ~ .custom-control-indicator { + background-image: url(data:image/svg+xml;charset=utf8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA4IDgnPjxwYXRoIGZpbGw9JyNmZmYnIGQ9J002LjU2NC43NWwtMy41OSAzLjYxMi0xLjUzOC0xLjU1TDAgNC4yNiAyLjk3NCA3LjI1IDggMi4xOTN6Jy8+PC9zdmc+); +} + +.custom-checkbox + .custom-control-input:indeterminate + ~ .custom-control-indicator { + background-color: #0074d9; + background-image: url(data:image/svg+xml;charset=utf8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA0IDQnPjxwYXRoIHN0cm9rZT0nI2ZmZicgZD0nTTAgMmg0Jy8+PC9zdmc+); +} + +.custom-radio .custom-control-indicator { + border-radius: 50%; +} + +.custom-radio .custom-control-input:checked ~ .custom-control-indicator { + background-image: url(data:image/svg+xml;charset=utf8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9Jy00IC00IDggOCc+PGNpcmNsZSByPSczJyBmaWxsPScjZmZmJy8+PC9zdmc+); +} + +.custom-controls-stacked .custom-control { + display: inline; +} + +.custom-controls-stacked .custom-control::after { + display: block; + margin-bottom: 0.25rem; + content: ''; +} + +.custom-controls-stacked .custom-control + .custom-control { + margin-left: 0; +} + +.custom-select { + display: inline-block; + max-width: 100%; + padding: 0.375rem 1.75rem 0.375rem 0.75rem; + padding-right: 0.75rem \9; + color: #55595c; + vertical-align: middle; + background: #211f21 + url(data:image/svg+xml;charset=utf8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA0IDUnPjxwYXRoIGZpbGw9JyMzMzMnIGQ9J00yIDBMMCAyaDR6bTAgNUwwIDNoNHonLz48L3N2Zz4=) + no-repeat right 0.75rem center; + background-image: none \9; + background-size: 8px 10px; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; + -moz-appearance: none; + -webkit-appearance: none; +} + +.custom-select:focus { + border-color: #51a7e8; + outline: none; +} + +.custom-select::-ms-expand { + opacity: 0; +} + +.custom-select-sm { + padding-top: 0.375rem; + padding-bottom: 0.375rem; + font-size: 75%; +} + +.custom-file { + position: relative; + display: inline-block; + max-width: 100%; + height: 2.5rem; + cursor: pointer; +} + +.custom-file-input { + min-width: 14rem; + max-width: 100%; + margin: 0; + filter: alpha(opacity=0); + opacity: 0; +} + +.custom-file-control { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 5; + height: 2.5rem; + padding: 0.5rem 1rem; + line-height: 1.5; + color: #555; + user-select: none; + background-color: #211f21; + border: 1px solid #ddd; + border-radius: 0.25rem; +} + +.custom-file-control:lang(en)::after { + content: 'Choose file...'; +} + +.custom-file-control::before { + position: absolute; + top: -1px; + right: -1px; + bottom: -1px; + z-index: 6; + display: block; + height: 2.5rem; + padding: 0.5rem 1rem; + line-height: 1.5; + color: #555; + background-color: #eee; + border: 1px solid #ddd; + border-radius: 0 0.25rem 0.25rem 0; +} + +.custom-file-control:lang(en)::before { + content: 'Browse'; +} + +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav-link { + display: inline-block; +} + +.nav-link:focus, +.nav-link:hover { + text-decoration: none; +} + +.nav-link.disabled { + color: #818a91; +} + +.nav-link.disabled, +.nav-link.disabled:focus, +.nav-link.disabled:hover { + color: #818a91; + cursor: not-allowed; + background-color: transparent; +} + +.nav-inline .nav-item { + display: inline-block; +} + +.nav-inline .nav-item + .nav-item, +.nav-inline .nav-link + .nav-link { + margin-left: 1rem; +} + +.nav-tabs { + border-bottom: 1px solid #ddd; +} + +.nav-tabs::after { + content: ''; + display: table; + clear: both; +} + +.nav-tabs .nav-item { + float: left; + margin-bottom: -1px; +} + +.nav-tabs .nav-item + .nav-item { + margin-left: 0.2rem; +} + +.nav-tabs .nav-link { + display: block; + padding: 0.5em 1em; + border: 1px solid transparent; + border-radius: 0.25rem 0.25rem 0 0; +} + +.nav-tabs .nav-link:focus, +.nav-tabs .nav-link:hover { + border-color: #eceeef #eceeef #ddd; +} + +.nav-tabs .nav-link.disabled, +.nav-tabs .nav-link.disabled:focus, +.nav-tabs .nav-link.disabled:hover { + color: #818a91; + background-color: transparent; + border-color: transparent; +} + +.nav-tabs .nav-link.active, +.nav-tabs .nav-link.active:focus, +.nav-tabs .nav-link.active:hover, +.nav-tabs .nav-item.open .nav-link, +.nav-tabs .nav-item.open .nav-link:focus, +.nav-tabs .nav-item.open .nav-link:hover { + color: #55595c; + background-color: #211f21; + border-color: #ddd #ddd transparent; +} + +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.nav-pills::after { + content: ''; + display: table; + clear: both; +} + +.nav-pills .nav-item { + float: left; +} + +.nav-pills .nav-item + .nav-item { + margin-left: 0.2rem; +} + +.nav-pills .nav-link { + display: block; + padding: 0.5em 1em; + border-radius: 0.25rem; +} + +.nav-pills .nav-link.active, +.nav-pills .nav-link.active:focus, +.nav-pills .nav-link.active:hover, +.nav-pills .nav-item.open .nav-link, +.nav-pills .nav-item.open .nav-link:focus, +.nav-pills .nav-item.open .nav-link:hover { + color: #fff; + cursor: default; + background-color: #ec8cb5; +} + +.nav-stacked .nav-item { + display: block; + float: none; +} + +.nav-stacked .nav-item + .nav-item { + margin-top: 0.2rem; + margin-left: 0; +} + +.tab-content > .tab-pane { + display: none; +} + +.tab-content > .active { + display: block; +} + +.navbar { + position: relative; + padding: 0.5rem 1rem; +} + +.navbar::after { + content: ''; + display: table; + clear: both; +} + +@media (min-width: 544px) { + .navbar { + border-radius: 0.25rem; + } +} + +.navbar-full { + z-index: 1000; +} + +@media (min-width: 544px) { + .navbar-full { + border-radius: 0; + } +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; +} + +@media (min-width: 544px) { + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; + } +} + +.navbar-fixed-top { + top: 0; +} + +.navbar-fixed-bottom { + bottom: 0; +} + +.navbar-sticky-top { + position: sticky; + top: 0; + z-index: 1030; + width: 100%; +} + +@media (min-width: 544px) { + .navbar-sticky-top { + border-radius: 0; + } +} + +.navbar-brand { + float: left; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + margin-right: 1rem; + font-size: 1.25rem; +} + +.navbar-brand:focus, +.navbar-brand:hover { + text-decoration: none; +} + +.navbar-brand > img { + display: block; +} + +.navbar-divider { + float: left; + width: 1px; + padding-top: 0.425rem; + padding-bottom: 0.425rem; + margin-right: 1rem; + margin-left: 1rem; + overflow: hidden; +} + +.navbar-divider::before { + content: '\00a0'; +} + +.navbar-toggler { + padding: 0.5rem 0.75rem; + font-size: 1.25rem; + line-height: 1; + background: 0 0; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.navbar-toggler:focus, +.navbar-toggler:hover { + text-decoration: none; +} + +@media (min-width: 544px) { + .navbar-toggleable-xs { + display: block !important; + } +} + +@media (min-width: 768px) { + .navbar-toggleable-sm { + display: block !important; + } +} + +@media (min-width: 992px) { + .navbar-toggleable-md { + display: block !important; + } +} + +.navbar-nav .nav-item { + float: left; +} + +.navbar-nav .nav-link { + display: block; + padding-top: 0.425rem; + padding-bottom: 0.425rem; +} + +.navbar-nav .nav-link + .nav-link { + margin-left: 1rem; +} + +.navbar-nav .nav-item + .nav-item { + margin-left: 1rem; +} + +.navbar-light .navbar-brand { + color: rgba(0, 0, 0, 0.8); +} + +.navbar-light .navbar-brand:focus, +.navbar-light .navbar-brand:hover { + color: rgba(0, 0, 0, 0.8); +} + +.navbar-light .navbar-nav .nav-link { + color: #8c8889; +} + +.navbar-light .navbar-nav .nav-link:focus, +.navbar-light .navbar-nav .nav-link:hover { + color: rgba(255, 255, 255, 1); +} + +.navbar-light .navbar-nav .open > .nav-link, +.navbar-light .navbar-nav .open > .nav-link:focus, +.navbar-light .navbar-nav .open > .nav-link:hover, +.navbar-light .navbar-nav .active > .nav-link, +.navbar-light .navbar-nav .active > .nav-link:focus, +.navbar-light .navbar-nav .active > .nav-link:hover, +.navbar-light .navbar-nav .nav-link.open, +.navbar-light .navbar-nav .nav-link.open:focus, +.navbar-light .navbar-nav .nav-link.open:hover, +.navbar-light .navbar-nav .nav-link.active, +.navbar-light .navbar-nav .nav-link.active:focus, +.navbar-light .navbar-nav .nav-link.active:hover { + color: rgba(0, 0, 0, 0.8); +} + +.navbar-light .navbar-divider { + background-color: rgba(0, 0, 0, 0.075); +} + +.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, 0.5); +} + +.navbar-dark .navbar-nav .nav-link:focus, +.navbar-dark .navbar-nav .nav-link:hover { + color: rgba(255, 255, 255, 0.75); +} + +.navbar-dark .navbar-nav .open > .nav-link, +.navbar-dark .navbar-nav .open > .nav-link:focus, +.navbar-dark .navbar-nav .open > .nav-link:hover, +.navbar-dark .navbar-nav .active > .nav-link, +.navbar-dark .navbar-nav .active > .nav-link:focus, +.navbar-dark .navbar-nav .active > .nav-link:hover, +.navbar-dark .navbar-nav .nav-link.open, +.navbar-dark .navbar-nav .nav-link.open:focus, +.navbar-dark .navbar-nav .nav-link.open:hover, +.navbar-dark .navbar-nav .nav-link.active, +.navbar-dark .navbar-nav .nav-link.active:focus, +.navbar-dark .navbar-nav .nav-link.active:hover { + color: #fff; +} + +.navbar-dark .navbar-divider { + background-color: rgba(255, 255, 255, 0.075); +} + +.card { + position: relative; + display: block; + margin-bottom: 0.75rem; + background-color: #211f21; + border-radius: 0.25rem; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.125); +} + +.card-block { + padding: 1.25rem; +} + +.card-block::after { + content: ''; + display: table; + clear: both; +} + +.card-title { + margin-bottom: 0.75rem; +} + +.card-subtitle { + margin-top: -0.375rem; + margin-bottom: 0; +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link:hover { + text-decoration: none; +} + +.card-link + .card-link { + margin-left: 1.25rem; +} + +.card > .list-group:first-child .list-group-item:first-child { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.card > .list-group:last-child .list-group-item:last-child { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.card-header { + padding: 0.75rem 1.25rem; + background-color: #f5f5f5; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.125); +} + +.card-header::after { + content: ''; + display: table; + clear: both; +} + +.card-header:first-child { + border-radius: 0.25rem 0.25rem 0 0; +} + +.card-footer { + padding: 0.75rem 1.25rem; + background-color: #785468; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.125); +} + +.card-footer::after { + content: ''; + display: table; + clear: both; +} + +.card-footer:last-child { + border-radius: 0 0 0.25rem 0.25rem; +} + +.card-header-tabs { + margin-right: -0.625rem; + margin-bottom: -0.75rem; + margin-left: -0.625rem; + border-bottom: 0; +} + +.card-header-tabs .nav-item { + margin-bottom: 0; +} + +.card-header-pills { + margin-right: -0.625rem; + margin-left: -0.625rem; +} + +.card-primary { + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.card-success { + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.card-info { + background-color: #5bc0de; + border-color: #5bc0de; +} + +.card-warning { + background-color: #f0ad4e; + border-color: #f0ad4e; +} + +.card-danger { + background-color: #b85c5c; + border-color: #b85c5c; +} + +.card-outline-primary { + background-color: transparent; + border-color: #ec8cb5; +} + +.card-outline-secondary { + background-color: transparent; + border-color: #ccc; +} + +.card-outline-info { + background-color: transparent; + border-color: #5bc0de; +} + +.card-outline-success { + background-color: transparent; + border-color: #ec8cb5; +} + +.card-outline-warning { + background-color: transparent; + border-color: #f0ad4e; +} + +.card-outline-danger { + background-color: transparent; + border-color: #b85c5c; +} + +.card-inverse .card-header, +.card-inverse .card-footer { + border-bottom: 1px solid rgba(255, 255, 255, 0.2); +} + +.card-inverse .card-header, +.card-inverse .card-footer, +.card-inverse .card-title, +.card-inverse .card-blockquote { + color: #fff; +} + +.card-inverse .card-link, +.card-inverse .card-text, +.card-inverse .card-blockquote > footer { + color: rgba(255, 255, 255, 0.65); +} + +.card-inverse .card-link:focus, +.card-inverse .card-link:hover { + color: #fff; +} + +.card-blockquote { + padding: 0; + margin-bottom: 0; + border-left: 0; +} + +.card-img { + border-radius: 0.25rem; +} + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 1.25rem; +} + +.card-img-top { + border-radius: 0.25rem 0.25rem 0 0; +} + +.card-img-bottom { + border-radius: 0 0 0.25rem 0.25rem; +} + +@media (min-width: 544px) { + .card-deck { + display: flex; + flex-flow: row wrap; + margin-right: -0.625rem; + margin-left: -0.625rem; + } + .card-deck .card { + flex: 1 0 0; + margin-right: 0.625rem; + margin-left: 0.625rem; + } +} + +@media (min-width: 544px) { + .card-group { + display: flex; + flex-flow: row wrap; + } + .card-group .card { + flex: 1 0 0; + } + .card-group .card + .card { + margin-left: 0; + border-left: 0; + } + .card-group .card:first-child { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + } + .card-group .card:first-child .card-img-top { + border-top-right-radius: 0; + } + .card-group .card:first-child .card-img-bottom { + border-bottom-right-radius: 0; + } + .card-group .card:last-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + } + .card-group .card:last-child .card-img-top { + border-top-left-radius: 0; + } + .card-group .card:last-child .card-img-bottom { + border-bottom-left-radius: 0; + } + .card-group .card:not(:first-child):not(:last-child) { + border-radius: 0; + } + .card-group .card:not(:first-child):not(:last-child) .card-img-top, + .card-group .card:not(:first-child):not(:last-child) .card-img-bottom { + border-radius: 0; + } +} + +@media (min-width: 544px) { + .card-columns { + column-count: 3; + column-gap: 1.25rem; + } + .card-columns .card { + display: inline-block; + width: 100%; + } +} + +.breadcrumb { + padding: 0.75rem 1rem; + margin-bottom: 1rem; + list-style: none; + background-color: #eceeef; + border-radius: 0.25rem; +} + +.breadcrumb::after { + content: ''; + display: table; + clear: both; +} + +.breadcrumb-item { + float: left; +} + +.breadcrumb-item + .breadcrumb-item::before { + display: inline-block; + padding-right: 0.5rem; + padding-left: 0.5rem; + color: #818a91; + content: '/'; +} + +.breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: underline; +} + +.breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: none; +} + +.breadcrumb-item.active { + color: #818a91; +} + +.pagination { + display: inline-block; + padding-left: 0; + margin-top: 1rem; + margin-bottom: 1rem; + border-radius: 0.25rem; +} + +.page-item { + display: inline; +} + +.page-item:first-child .page-link { + margin-left: 0; + border-bottom-left-radius: 0.25rem; + border-top-left-radius: 0.25rem; +} + +.page-item:last-child .page-link { + border-bottom-right-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.page-item.active .page-link, +.page-item.active .page-link:focus, +.page-item.active .page-link:hover { + z-index: 2; + color: #fff; + cursor: default; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.page-item.disabled .page-link, +.page-item.disabled .page-link:focus, +.page-item.disabled .page-link:hover { + color: #818a91; + pointer-events: none; + cursor: not-allowed; + background-color: #211f21; + border-color: #ddd; +} + +.page-link { + position: relative; + float: left; + padding: 0.5rem 0.75rem; + margin-left: -1px; + color: #ec8cb5; + text-decoration: none; + background-color: #211f21; + border: 1px solid #ddd; +} + +.page-link:focus, +.page-link:hover { + color: #3d8b3d; + background-color: #eceeef; + border-color: #ddd; +} + +.pagination-lg .page-link { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; +} + +.pagination-lg .page-item:first-child .page-link { + border-bottom-left-radius: 0.3rem; + border-top-left-radius: 0.3rem; +} + +.pagination-lg .page-item:last-child .page-link { + border-bottom-right-radius: 0.3rem; + border-top-right-radius: 0.3rem; +} + +.pagination-sm .page-link { + padding: 0.275rem 0.75rem; + font-size: 0.875rem; +} + +.pagination-sm .page-item:first-child .page-link { + border-bottom-left-radius: 0.2rem; + border-top-left-radius: 0.2rem; +} + +.pagination-sm .page-item:last-child .page-link { + border-bottom-right-radius: 0.2rem; + border-top-right-radius: 0.2rem; +} + +.tag { + display: inline-block; + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; +} + +.tag:empty { + display: none; +} + +.btn .tag { + position: relative; + top: -1px; +} + +a.tag:focus, +a.tag:hover { + color: #fff; + text-decoration: none; + cursor: pointer; +} + +.tag-pill { + border: 1px solid rgba(0, 0, 0, 0.5); + padding-right: 0.6em; + padding-left: 0.6em; + border-radius: 8px; +} + +.tag-default { + background-color: #818a91; +} + +.tag-default[href]:focus, +.tag-default[href]:hover { + background-color: #687077; +} + +.tag-primary { + background-color: #ec8cb5; +} + +.tag-primary[href]:focus, +.tag-primary[href]:hover { + background-color: #7a8992; +} + +.tag-success { + background-color: #ec8cb5; +} + +.tag-success[href]:focus, +.tag-success[href]:hover { + background-color: #7a8992; +} + +.tag-info { + background-color: #5bc0de; +} + +.tag-info[href]:focus, +.tag-info[href]:hover { + background-color: #31b0d5; +} + +.tag-warning { + background-color: #f0ad4e; +} + +.tag-warning[href]:focus, +.tag-warning[href]:hover { + background-color: #ec971f; +} + +.tag-danger { + background-color: #b85c5c; +} + +.tag-danger[href]:focus, +.tag-danger[href]:hover { + background-color: #9d4444; +} + +.jumbotron { + padding: 2rem 1rem; + margin-bottom: 2rem; + background-color: #eceeef; + border-radius: 0.3rem; +} + +@media (min-width: 544px) { + .jumbotron { + padding: 4rem 2rem; + } +} + +.jumbotron-hr { + border-top-color: #d0d5d8; +} + +.jumbotron-fluid { + padding-right: 0; + padding-left: 0; + border-radius: 0; +} + +.alert { + padding: 15px; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.alert-heading { + color: inherit; +} + +.alert-link { + font-weight: 700; +} + +.alert-dismissible { + padding-right: 35px; +} + +.alert-dismissible .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} + +.alert-success { + background-color: #dff0d8; + border-color: #d0e9c6; + color: #3c763d; +} + +.alert-success hr { + border-top-color: #c1e2b3; +} + +.alert-success .alert-link { + color: #2b542c; +} + +.alert-info { + background-color: #d9edf7; + border-color: #bcdff1; + color: #31708f; +} + +.alert-info hr { + border-top-color: #a6d5ec; +} + +.alert-info .alert-link { + color: #245269; +} + +.alert-warning { + background-color: #fcf8e3; + border-color: #faf2cc; + color: #8a6d3b; +} + +.alert-warning hr { + border-top-color: #f7ecb5; +} + +.alert-warning .alert-link { + color: #66512c; +} + +.alert-danger { + background-color: #f2dede; + border-color: #ebcccc; + color: #a94442; +} + +.alert-danger hr { + border-top-color: #e4b9b9; +} + +.alert-danger .alert-link { + color: #843534; +} + +@keyframes progress-bar-stripes { + from { + background-position: 1rem 0; + } + to { + background-position: 0 0; + } +} + +.progress { + display: block; + width: 100%; + height: 1rem; + margin-bottom: 1rem; +} + +.progress[value] { + background-color: #eee; + border: 0; + appearance: none; + border-radius: 0.25rem; +} + +.progress[value]::-ms-fill { + background-color: #0074d9; + border: 0; +} + +.progress[value]::-moz-progress-bar { + background-color: #0074d9; + border-bottom-left-radius: 0.25rem; + border-top-left-radius: 0.25rem; +} + +.progress[value]::-webkit-progress-value { + background-color: #0074d9; + border-bottom-left-radius: 0.25rem; + border-top-left-radius: 0.25rem; +} + +.progress[value='100']::-moz-progress-bar { + border-bottom-right-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.progress[value='100']::-webkit-progress-value { + border-bottom-right-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.progress[value]::-webkit-progress-bar { + background-color: #eee; + border-radius: 0.25rem; +} + +base::-moz-progress-bar, +.progress[value] { + background-color: #eee; + border-radius: 0.25rem; +} + +@media screen and (min-width: 0\0) { + .progress { + background-color: #eee; + border-radius: 0.25rem; + } + .progress-bar { + display: inline-block; + height: 1rem; + text-indent: -999rem; + background-color: #0074d9; + border-bottom-left-radius: 0.25rem; + border-top-left-radius: 0.25rem; + } + .progress[width='100%'] { + border-bottom-right-radius: 0.25rem; + border-top-right-radius: 0.25rem; + } +} + +.progress-striped[value]::-webkit-progress-value { + background-image: linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-size: 1rem 1rem; +} + +.progress-striped[value]::-moz-progress-bar { + background-image: linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-size: 1rem 1rem; +} + +.progress-striped[value]::-ms-fill { + background-image: linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-size: 1rem 1rem; +} + +@media screen and (min-width: 0\0) { + .progress-bar-striped { + background-image: linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-size: 1rem 1rem; + } +} + +.progress-animated[value]::-webkit-progress-value { + animation: progress-bar-stripes 2s linear infinite; +} + +.progress-animated[value]::-moz-progress-bar { + animation: progress-bar-stripes 2s linear infinite; +} + +@media screen and (min-width: 0\0) { + .progress-animated .progress-bar-striped { + animation: progress-bar-stripes 2s linear infinite; + } +} + +.progress-success[value]::-webkit-progress-value { + background-color: #ec8cb5; +} + +.progress-success[value]::-moz-progress-bar { + background-color: #ec8cb5; +} + +.progress-success[value]::-ms-fill { + background-color: #ec8cb5; +} + +@media screen and (min-width: 0\0) { + .progress-success .progress-bar { + background-color: #ec8cb5; + } +} + +.progress-info[value]::-webkit-progress-value { + background-color: #5bc0de; +} + +.progress-info[value]::-moz-progress-bar { + background-color: #5bc0de; +} + +.progress-info[value]::-ms-fill { + background-color: #5bc0de; +} + +@media screen and (min-width: 0\0) { + .progress-info .progress-bar { + background-color: #5bc0de; + } +} + +.progress-warning[value]::-webkit-progress-value { + background-color: #f0ad4e; +} + +.progress-warning[value]::-moz-progress-bar { + background-color: #f0ad4e; +} + +.progress-warning[value]::-ms-fill { + background-color: #f0ad4e; +} + +@media screen and (min-width: 0\0) { + .progress-warning .progress-bar { + background-color: #f0ad4e; + } +} + +.progress-danger[value]::-webkit-progress-value { + background-color: #b85c5c; +} + +.progress-danger[value]::-moz-progress-bar { + background-color: #b85c5c; +} + +.progress-danger[value]::-ms-fill { + background-color: #b85c5c; +} + +@media screen and (min-width: 0\0) { + .progress-danger .progress-bar { + background-color: #b85c5c; + } +} + +.media { + display: flex; + margin-bottom: 1rem; +} + +.media-body { + flex: 1; +} + +.media-middle { + align-self: center; +} + +.media-bottom { + align-self: flex-end; +} + +.media-object { + display: block; +} + +.media-object.img-thumbnail { + max-width: none; +} + +.media-right { + padding-left: 10px; +} + +.media-left { + padding-right: 10px; +} + +.media-heading { + margin-top: 0; + margin-bottom: 5px; +} + +.media-list { + padding-left: 0; + list-style: none; +} + +.list-group { + padding-left: 0; + margin-bottom: 0; +} + +.list-group-item { + position: relative; + display: block; + padding: 0.75rem 1.25rem; + margin-bottom: -1px; + background-color: #211f21; + border: 1px solid #ddd; +} + +.list-group-item:first-child { + border-top-right-radius: 0.25rem; + border-top-left-radius: 0.25rem; +} + +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.list-group-item.disabled, +.list-group-item.disabled:focus, +.list-group-item.disabled:hover { + color: #818a91; + cursor: not-allowed; + background-color: #eceeef; +} + +.list-group-item.disabled .list-group-item-heading, +.list-group-item.disabled:focus .list-group-item-heading, +.list-group-item.disabled:hover .list-group-item-heading { + color: inherit; +} + +.list-group-item.disabled .list-group-item-text, +.list-group-item.disabled:focus .list-group-item-text, +.list-group-item.disabled:hover .list-group-item-text { + color: #818a91; +} + +.list-group-item.active, +.list-group-item.active:focus, +.list-group-item.active:hover { + z-index: 2; + color: #fff; + text-decoration: none; + background-color: #ec8cb5; + border-color: #ec8cb5; +} + +.list-group-item.active .list-group-item-heading, +.list-group-item.active .list-group-item-heading > small, +.list-group-item.active .list-group-item-heading > .small, +.list-group-item.active:focus .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading > small, +.list-group-item.active:focus .list-group-item-heading > .small, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading > small, +.list-group-item.active:hover .list-group-item-heading > .small { + color: inherit; +} + +.list-group-item.active .list-group-item-text, +.list-group-item.active:focus .list-group-item-text, +.list-group-item.active:hover .list-group-item-text { + color: #eaf6ea; +} + +.list-group-flush .list-group-item { + border-radius: 0; +} + +.list-group-item-action { + width: 100%; + color: #555; + text-align: inherit; +} + +.list-group-item-action .list-group-item-heading { + color: #333; +} + +.list-group-item-action:focus, +.list-group-item-action:hover { + color: #555; + text-decoration: none; + background-color: #f5f5f5; +} + +.list-group-item-success { + color: #3c763d; + background-color: #dff0d8; +} + +a.list-group-item-success, +button.list-group-item-success { + color: #3c763d; +} + +a.list-group-item-success .list-group-item-heading, +button.list-group-item-success .list-group-item-heading { + color: inherit; +} + +a.list-group-item-success:focus, +a.list-group-item-success:hover, +button.list-group-item-success:focus, +button.list-group-item-success:hover { + color: #3c763d; + background-color: #d0e9c6; +} + +a.list-group-item-success.active, +a.list-group-item-success.active:focus, +a.list-group-item-success.active:hover, +button.list-group-item-success.active, +button.list-group-item-success.active:focus, +button.list-group-item-success.active:hover { + color: #fff; + background-color: #3c763d; + border-color: #3c763d; +} + +.list-group-item-info { + color: #31708f; + background-color: #d9edf7; +} + +a.list-group-item-info, +button.list-group-item-info { + color: #31708f; +} + +a.list-group-item-info .list-group-item-heading, +button.list-group-item-info .list-group-item-heading { + color: inherit; +} + +a.list-group-item-info:focus, +a.list-group-item-info:hover, +button.list-group-item-info:focus, +button.list-group-item-info:hover { + color: #31708f; + background-color: #c4e3f3; +} + +a.list-group-item-info.active, +a.list-group-item-info.active:focus, +a.list-group-item-info.active:hover, +button.list-group-item-info.active, +button.list-group-item-info.active:focus, +button.list-group-item-info.active:hover { + color: #fff; + background-color: #31708f; + border-color: #31708f; +} + +.list-group-item-warning { + color: #8a6d3b; + background-color: #fcf8e3; +} + +a.list-group-item-warning, +button.list-group-item-warning { + color: #8a6d3b; +} + +a.list-group-item-warning .list-group-item-heading, +button.list-group-item-warning .list-group-item-heading { + color: inherit; +} + +a.list-group-item-warning:focus, +a.list-group-item-warning:hover, +button.list-group-item-warning:focus, +button.list-group-item-warning:hover { + color: #8a6d3b; + background-color: #faf2cc; +} + +a.list-group-item-warning.active, +a.list-group-item-warning.active:focus, +a.list-group-item-warning.active:hover, +button.list-group-item-warning.active, +button.list-group-item-warning.active:focus, +button.list-group-item-warning.active:hover { + color: #fff; + background-color: #8a6d3b; + border-color: #8a6d3b; +} + +.list-group-item-danger { + color: #a94442; + background-color: #f2dede; +} + +a.list-group-item-danger, +button.list-group-item-danger { + color: #a94442; +} + +a.list-group-item-danger .list-group-item-heading, +button.list-group-item-danger .list-group-item-heading { + color: inherit; +} + +a.list-group-item-danger:focus, +a.list-group-item-danger:hover, +button.list-group-item-danger:focus, +button.list-group-item-danger:hover { + color: #a94442; + background-color: #ebcccc; +} + +a.list-group-item-danger.active, +a.list-group-item-danger.active:focus, +a.list-group-item-danger.active:hover, +button.list-group-item-danger.active, +button.list-group-item-danger.active:focus, +button.list-group-item-danger.active:hover { + color: #fff; + background-color: #a94442; + border-color: #a94442; +} + +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} + +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} + +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; +} + +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} + +.embed-responsive-21by9 { + padding-bottom: 42.85714%; +} + +.embed-responsive-16by9 { + padding-bottom: 56.25%; +} + +.embed-responsive-4by3 { + padding-bottom: 75%; +} + +.embed-responsive-1by1 { + padding-bottom: 100%; +} + +.close { + float: right; + font-size: 1.5rem; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: 0.2; +} + +.close:focus, +.close:hover { + color: #000; + text-decoration: none; + cursor: pointer; + opacity: 0.5; +} + +button.close { + padding: 0; + cursor: pointer; + background: 0 0; + border: 0; + -webkit-appearance: none; +} + +.modal-open { + overflow: hidden; +} + +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + display: none; + overflow: hidden; + outline: 0; + -webkit-overflow-scrolling: touch; +} + +.modal.fade .modal-dialog { + transition: transform 0.3s ease-out; + transform: translate(0, -25%); +} + +.modal.in .modal-dialog { + transform: translate(0, 0); +} + +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} + +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} + +.modal-content { + position: relative; + background-color: #211f21; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; + outline: 0; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop.in { + opacity: 0.5; +} + +.modal-header { + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} + +.modal-header::after { + content: ''; + display: table; + clear: both; +} + +.modal-header .close { + margin-top: -2px; +} + +.modal-title { + margin: 0; + line-height: 1.5; +} + +.modal-body { + position: relative; + padding: 15px; +} + +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} + +.modal-footer::after { + content: ''; + display: table; + clear: both; +} + +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} + +@media (min-width: 544px) { + .modal-dialog { + max-width: 600px; + margin: 30px auto; + } + .modal-sm { + max-width: 300px; + } +} + +@media (min-width: 992px) { + .modal-lg { + max-width: 900px; + } +} + +.tooltip { + position: absolute; + z-index: 1070; + display: block; + font-family: source sans pro, sans-serif; + font-style: normal; + font-weight: 400; + letter-spacing: normal; + line-break: auto; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + white-space: normal; + word-break: normal; + word-spacing: normal; + font-size: 0.875rem; + word-wrap: break-word; + opacity: 0; +} + +.tooltip.in { + opacity: 0.9; +} + +.tooltip.tooltip-top, +.tooltip.bs-tether-element-attached-bottom { + padding: 5px 0; + margin-top: -3px; +} + +.tooltip.tooltip-top .tooltip-arrow, +.tooltip.bs-tether-element-attached-bottom .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} + +.tooltip.tooltip-right, +.tooltip.bs-tether-element-attached-left { + padding: 0 5px; + margin-left: 3px; +} + +.tooltip.tooltip-right .tooltip-arrow, +.tooltip.bs-tether-element-attached-left .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #000; +} + +.tooltip.tooltip-bottom, +.tooltip.bs-tether-element-attached-top { + padding: 5px 0; + margin-top: 3px; +} + +.tooltip.tooltip-bottom .tooltip-arrow, +.tooltip.bs-tether-element-attached-top .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} + +.tooltip.tooltip-left, +.tooltip.bs-tether-element-attached-right { + padding: 0 5px; + margin-left: -3px; +} + +.tooltip.tooltip-left .tooltip-arrow, +.tooltip.bs-tether-element-attached-right .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: #000; +} + +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 0.25rem; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: block; + max-width: 276px; + padding: 1px; + font-family: source sans pro, sans-serif; + font-style: normal; + font-weight: 400; + letter-spacing: normal; + line-break: auto; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + white-space: normal; + word-break: normal; + word-spacing: normal; + font-size: 0.875rem; + word-wrap: break-word; + background-color: #211f21; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; +} + +.popover.popover-top, +.popover.bs-tether-element-attached-bottom { + margin-top: -10px; +} + +.popover.popover-top .popover-arrow, +.popover.bs-tether-element-attached-bottom .popover-arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: rgba(0, 0, 0, 0.25); + border-bottom-width: 0; +} + +.popover.popover-top .popover-arrow::after, +.popover.bs-tether-element-attached-bottom .popover-arrow::after { + bottom: 1px; + margin-left: -10px; + content: ''; + border-top-color: #fff; + border-bottom-width: 0; +} + +.popover.popover-right, +.popover.bs-tether-element-attached-left { + margin-left: 10px; +} + +.popover.popover-right .popover-arrow, +.popover.bs-tether-element-attached-left .popover-arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: rgba(0, 0, 0, 0.25); + border-left-width: 0; +} + +.popover.popover-right .popover-arrow::after, +.popover.bs-tether-element-attached-left .popover-arrow::after { + bottom: -10px; + left: 1px; + content: ''; + border-right-color: #fff; + border-left-width: 0; +} + +.popover.popover-bottom, +.popover.bs-tether-element-attached-top { + margin-top: 10px; +} + +.popover.popover-bottom .popover-arrow, +.popover.bs-tether-element-attached-top .popover-arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: rgba(0, 0, 0, 0.25); +} + +.popover.popover-bottom .popover-arrow::after, +.popover.bs-tether-element-attached-top .popover-arrow::after { + top: 1px; + margin-left: -10px; + content: ''; + border-top-width: 0; + border-bottom-color: #fff; +} + +.popover.popover-left, +.popover.bs-tether-element-attached-right { + margin-left: -10px; +} + +.popover.popover-left .popover-arrow, +.popover.bs-tether-element-attached-right .popover-arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: rgba(0, 0, 0, 0.25); +} + +.popover.popover-left .popover-arrow::after, +.popover.bs-tether-element-attached-right .popover-arrow::after { + right: 1px; + bottom: -10px; + content: ''; + border-right-width: 0; + border-left-color: #fff; +} + +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 1rem; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 0.2375rem 0.2375rem 0 0; +} + +.popover-content { + padding: 9px 14px; +} + +.popover-arrow, +.popover-arrow::after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover-arrow { + border-width: 11px; +} + +.popover-arrow::after { + content: ''; + border-width: 10px; +} + +.carousel { + position: relative; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel-inner > .carousel-item { + position: relative; + display: none; + transition: 0.6s ease-in-out left; +} + +.carousel-inner > .carousel-item > img, +.carousel-inner > .carousel-item > a > img { + line-height: 1; +} + +@media all and (transform-3d), (-webkit-transform-3d) { + .carousel-inner > .carousel-item { + transition: transform 0.6s ease-in-out; + backface-visibility: hidden; + perspective: 1000px; + } + .carousel-inner > .carousel-item.next, + .carousel-inner > .carousel-item.active.right { + left: 0; + transform: translate3d(100%, 0, 0); + } + .carousel-inner > .carousel-item.prev, + .carousel-inner > .carousel-item.active.left { + left: 0; + transform: translate3d(-100%, 0, 0); + } + .carousel-inner > .carousel-item.next.left, + .carousel-inner > .carousel-item.prev.right, + .carousel-inner > .carousel-item.active { + left: 0; + transform: translate3d(0, 0, 0); + } +} + +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} + +.carousel-inner > .active { + left: 0; +} + +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} + +.carousel-inner > .next { + left: 100%; +} + +.carousel-inner > .prev { + left: -100%; +} + +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} + +.carousel-inner > .active.left { + left: -100%; +} + +.carousel-inner > .active.right { + left: 100%; +} + +.carousel-control { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 15%; + font-size: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); + opacity: 0.5; +} + +.carousel-control.left { + background-image: linear-gradient( + to right, + rgba(0, 0, 0, 0.5) 0%, + rgba(0, 0, 0, 0.0001) 100% + ); + background-repeat: repeat-x; + filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); +} + +.carousel-control.right { + right: 0; + left: auto; + background-image: linear-gradient( + to right, + rgba(0, 0, 0, 0.0001) 0%, + rgba(0, 0, 0, 0.5) 100% + ); + background-repeat: repeat-x; + filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); +} + +.carousel-control:focus, +.carousel-control:hover { + color: #fff; + text-decoration: none; + outline: 0; + opacity: 0.9; +} + +.carousel-control .icon-prev, +.carousel-control .icon-next { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; + width: 20px; + height: 20px; + margin-top: -10px; + font-family: serif; + line-height: 1; +} + +.carousel-control .icon-prev { + left: 50%; + margin-left: -10px; +} + +.carousel-control .icon-next { + right: 50%; + margin-right: -10px; +} + +.carousel-control .icon-prev::before { + content: '\2039'; +} + +.carousel-control .icon-next::before { + content: '\203a'; +} + +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + padding-left: 0; + margin-left: -30%; + text-align: center; + list-style: none; +} + +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + cursor: pointer; + background-color: transparent; + border: 1px solid #fff; + border-radius: 10px; +} + +.carousel-indicators .active { + width: 12px; + height: 12px; + margin: 0; + background-color: #211f21; +} + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); +} + +.carousel-caption .btn { + text-shadow: none; +} + +@media (min-width: 544px) { + .carousel-control .icon-prev, + .carousel-control .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + font-size: 30px; + } + .carousel-control .icon-prev { + margin-left: -15px; + } + .carousel-control .icon-next { + margin-right: -15px; + } + .carousel-caption { + right: 20%; + left: 20%; + padding-bottom: 30px; + } + .carousel-indicators { + bottom: 20px; + } +} + +.bg-inverse { + color: #eceeef; + background-color: #fff; +} + +.bg-faded { + background-color: #f7f7f9; +} + +.bg-primary { + color: #fff !important; + background-color: #ec8cb5 !important; +} + +a.bg-primary:focus, +a.bg-primary:hover { + background-color: #7a8992 !important; +} + +.bg-success { + color: #fff !important; + background-color: #ec8cb5 !important; +} + +a.bg-success:focus, +a.bg-success:hover { + background-color: #7a8992 !important; +} + +.bg-info { + color: #fff !important; + background-color: #5bc0de !important; +} + +a.bg-info:focus, +a.bg-info:hover { + background-color: #31b0d5 !important; +} + +.bg-warning { + color: #fff !important; + background-color: #f0ad4e !important; +} + +a.bg-warning:focus, +a.bg-warning:hover { + background-color: #ec971f !important; +} + +.bg-danger { + color: #fff !important; + background-color: #b85c5c !important; +} + +a.bg-danger:focus, +a.bg-danger:hover { + background-color: #9d4444 !important; +} + +.clearfix::after { + content: ''; + display: table; + clear: both; +} + +.pull-xs-left { + float: left !important; +} + +.pull-xs-right { + float: right !important; +} + +.pull-xs-none { + float: none !important; +} + +@media (min-width: 544px) { + .pull-sm-left { + float: left !important; + } + .pull-sm-right { + float: right !important; + } + .pull-sm-none { + float: none !important; + } +} + +@media (min-width: 768px) { + .pull-md-left { + float: left !important; + } + .pull-md-right { + float: right !important; + } + .pull-md-none { + float: none !important; + } +} + +@media (min-width: 992px) { + .pull-lg-left { + float: left !important; + } + .pull-lg-right { + float: right !important; + } + .pull-lg-none { + float: none !important; + } +} + +@media (min-width: 1200px) { + .pull-xl-left { + float: left !important; + } + .pull-xl-right { + float: right !important; + } + .pull-xl-none { + float: none !important; + } +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} + +.m-x-auto { + margin-right: auto !important; + margin-left: auto !important; +} + +.m-a-0 { + margin: 0 !important; +} + +.m-t-0 { + margin-top: 0 !important; +} + +.m-r-0 { + margin-right: 0 !important; +} + +.m-b-0 { + margin-bottom: 0 !important; +} + +.m-l-0 { + margin-left: 0 !important; +} + +.m-x-0 { + margin-right: 0 !important; + margin-left: 0 !important; +} + +.m-y-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +.m-a-1 { + margin: 1rem !important; +} + +.m-t-1 { + margin-top: 1rem !important; +} + +.m-r-1 { + margin-right: 1rem !important; +} + +.m-b-1 { + margin-bottom: 1rem !important; +} + +.m-l-1 { + margin-left: 1rem !important; +} + +.m-x-1 { + margin-right: 1rem !important; + margin-left: 1rem !important; +} + +.m-y-1 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; +} + +.m-a-2 { + margin: 1.5rem !important; +} + +.m-t-2 { + margin-top: 1.5rem !important; +} + +.m-r-2 { + margin-right: 1.5rem !important; +} + +.m-b-2 { + margin-bottom: 1.5rem !important; +} + +.m-l-2 { + margin-left: 1.5rem !important; +} + +.m-x-2 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; +} + +.m-y-2 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; +} + +.m-a-3 { + margin: 3rem !important; +} + +.m-t-3 { + margin-top: 3rem !important; +} + +.m-r-3 { + margin-right: 3rem !important; +} + +.m-b-3 { + margin-bottom: 3rem !important; +} + +.m-l-3 { + margin-left: 3rem !important; +} + +.m-x-3 { + margin-right: 3rem !important; + margin-left: 3rem !important; +} + +.m-y-3 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; +} + +.p-a-0 { + padding: 0 !important; +} + +.p-t-0 { + padding-top: 0 !important; +} + +.p-r-0 { + padding-right: 0 !important; +} + +.p-b-0 { + padding-bottom: 0 !important; +} + +.p-l-0 { + padding-left: 0 !important; +} + +.p-x-0 { + padding-right: 0 !important; + padding-left: 0 !important; +} + +.p-y-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.p-a-1 { + padding: 1rem !important; +} + +.p-t-1 { + padding-top: 1rem !important; +} + +.p-r-1 { + padding-right: 1rem !important; +} + +.p-b-1 { + padding-bottom: 1rem !important; +} + +.p-l-1 { + padding-left: 1rem !important; +} + +.p-x-1 { + padding-right: 1rem !important; + padding-left: 1rem !important; +} + +.p-y-1 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; +} + +.p-a-2 { + padding: 1.5rem !important; +} + +.p-t-2 { + padding-top: 1.5rem !important; +} + +.p-r-2 { + padding-right: 1.5rem !important; +} + +.p-b-2 { + padding-bottom: 1.5rem !important; +} + +.p-l-2 { + padding-left: 1.5rem !important; +} + +.p-x-2 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; +} + +.p-y-2 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; +} + +.p-a-3 { + padding: 3rem !important; +} + +.p-t-3 { + padding-top: 3rem !important; +} + +.p-r-3 { + padding-right: 3rem !important; +} + +.p-b-3 { + padding-bottom: 3rem !important; +} + +.p-l-3 { + padding-left: 3rem !important; +} + +.p-x-3 { + padding-right: 3rem !important; + padding-left: 3rem !important; +} + +.p-y-3 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; +} + +.pos-f-t { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; +} + +.text-justify { + text-align: justify !important; +} + +.text-nowrap { + white-space: nowrap !important; +} + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.text-xs-left { + text-align: left !important; +} + +.text-xs-right { + text-align: right !important; +} + +.text-xs-center { + text-align: center !important; +} + +@media (min-width: 544px) { + .text-sm-left { + text-align: left !important; + } + .text-sm-right { + text-align: right !important; + } + .text-sm-center { + text-align: center !important; + } +} + +@media (min-width: 768px) { + .text-md-left { + text-align: left !important; + } + .text-md-right { + text-align: right !important; + } + .text-md-center { + text-align: center !important; + } +} + +@media (min-width: 992px) { + .text-lg-left { + text-align: left !important; + } + .text-lg-right { + text-align: right !important; + } + .text-lg-center { + text-align: center !important; + } +} + +@media (min-width: 1200px) { + .text-xl-left { + text-align: left !important; + } + .text-xl-right { + text-align: right !important; + } + .text-xl-center { + text-align: center !important; + } +} + +.text-lowercase { + text-transform: lowercase !important; +} + +.text-uppercase { + text-transform: uppercase !important; +} + +.text-capitalize { + text-transform: capitalize !important; +} + +.font-weight-normal { + font-weight: 400; +} + +.font-weight-bold { + font-weight: 700; +} + +.font-italic { + font-style: italic; +} + +.text-muted { + color: #818a91 !important; +} + +a.text-muted:focus, +a.text-muted:hover { + color: #687077; +} + +.text-primary { + color: #ec8cb5 !important; +} + +a.text-primary:focus, +a.text-primary:hover { + color: #7a8992; +} + +.text-success { + color: #ec8cb5 !important; +} + +a.text-success:focus, +a.text-success:hover { + color: #7a8992; +} + +.text-info { + color: #5bc0de !important; +} + +a.text-info:focus, +a.text-info:hover { + color: #31b0d5; +} + +.text-warning { + color: #f0ad4e !important; +} + +a.text-warning:focus, +a.text-warning:hover { + color: #ec971f; +} + +.text-danger { + color: #b85c5c !important; +} + +a.text-danger:focus, +a.text-danger:hover { + color: #9d4444; +} + +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.invisible { + visibility: hidden !important; +} + +.hidden-xs-up { + display: none !important; +} + +@media (max-width: 543px) { + .hidden-xs-down { + display: none !important; + } +} + +@media (min-width: 544px) { + .hidden-sm-up { + display: none !important; + } +} + +@media (max-width: 767px) { + .hidden-sm-down { + display: none !important; + } +} + +@media (min-width: 768px) { + .hidden-md-up { + display: none !important; + } +} + +@media (max-width: 991px) { + .hidden-md-down { + display: none !important; + } +} + +@media (min-width: 992px) { + .hidden-lg-up { + display: none !important; + } +} + +@media (max-width: 1199px) { + .hidden-lg-down { + display: none !important; + } +} + +@media (min-width: 1200px) { + .hidden-xl-up { + display: none !important; + } +} + +.hidden-xl-down { + display: none !important; +} + +.visible-print-block { + display: none !important; +} + +@media print { + .visible-print-block { + display: block !important; + } +} + +.visible-print-inline { + display: none !important; +} + +@media print { + .visible-print-inline { + display: inline !important; + } +} + +.visible-print-inline-block { + display: none !important; +} + +@media print { + .visible-print-inline-block { + display: inline-block !important; + } +} + +@media print { + .hidden-print { + display: none !important; + } +} + +.flex-xs-first { + order: -1; +} + +.flex-xs-last { + order: 1; +} + +.flex-items-xs-top { + align-items: flex-start; +} + +.flex-items-xs-middle { + align-items: center; +} + +.flex-items-xs-bottom { + align-items: flex-end; +} + +.flex-xs-top { + align-self: flex-start; +} + +.flex-xs-middle { + align-self: center; +} + +.flex-xs-bottom { + align-self: flex-end; +} + +.flex-items-xs-left { + justify-content: flex-start; +} + +.flex-items-xs-center { + justify-content: center; +} + +.flex-items-xs-right { + justify-content: flex-end; +} + +.flex-items-xs-around { + justify-content: space-around; +} + +.flex-items-xs-between { + justify-content: space-between; +} + +@media (min-width: 544px) { + .flex-sm-first { + order: -1; + } + .flex-sm-last { + order: 1; + } +} + +@media (min-width: 544px) { + .flex-items-sm-top { + align-items: flex-start; + } + .flex-items-sm-middle { + align-items: center; + } + .flex-items-sm-bottom { + align-items: flex-end; + } +} + +@media (min-width: 544px) { + .flex-sm-top { + align-self: flex-start; + } + .flex-sm-middle { + align-self: center; + } + .flex-sm-bottom { + align-self: flex-end; + } +} + +@media (min-width: 544px) { + .flex-items-sm-left { + justify-content: flex-start; + } + .flex-items-sm-center { + justify-content: center; + } + .flex-items-sm-right { + justify-content: flex-end; + } + .flex-items-sm-around { + justify-content: space-around; + } + .flex-items-sm-between { + justify-content: space-between; + } +} + +@media (min-width: 768px) { + .flex-md-first { + order: -1; + } + .flex-md-last { + order: 1; + } +} + +@media (min-width: 768px) { + .flex-items-md-top { + align-items: flex-start; + } + .flex-items-md-middle { + align-items: center; + } + .flex-items-md-bottom { + align-items: flex-end; + } +} + +@media (min-width: 768px) { + .flex-md-top { + align-self: flex-start; + } + .flex-md-middle { + align-self: center; + } + .flex-md-bottom { + align-self: flex-end; + } +} + +@media (min-width: 768px) { + .flex-items-md-left { + justify-content: flex-start; + } + .flex-items-md-center { + justify-content: center; + } + .flex-items-md-right { + justify-content: flex-end; + } + .flex-items-md-around { + justify-content: space-around; + } + .flex-items-md-between { + justify-content: space-between; + } +} + +@media (min-width: 992px) { + .flex-lg-first { + order: -1; + } + .flex-lg-last { + order: 1; + } +} + +@media (min-width: 992px) { + .flex-items-lg-top { + align-items: flex-start; + } + .flex-items-lg-middle { + align-items: center; + } + .flex-items-lg-bottom { + align-items: flex-end; + } +} + +@media (min-width: 992px) { + .flex-lg-top { + align-self: flex-start; + } + .flex-lg-middle { + align-self: center; + } + .flex-lg-bottom { + align-self: flex-end; + } +} + +@media (min-width: 992px) { + .flex-items-lg-left { + justify-content: flex-start; + } + .flex-items-lg-center { + justify-content: center; + } + .flex-items-lg-right { + justify-content: flex-end; + } + .flex-items-lg-around { + justify-content: space-around; + } + .flex-items-lg-between { + justify-content: space-between; + } +} + +@media (min-width: 1200px) { + .flex-xl-first { + order: -1; + } + .flex-xl-last { + order: 1; + } +} + +@media (min-width: 1200px) { + .flex-items-xl-top { + align-items: flex-start; + } + .flex-items-xl-middle { + align-items: center; + } + .flex-items-xl-bottom { + align-items: flex-end; + } +} + +@media (min-width: 1200px) { + .flex-xl-top { + align-self: flex-start; + } + .flex-xl-middle { + align-self: center; + } + .flex-xl-bottom { + align-self: flex-end; + } +} + +@media (min-width: 1200px) { + .flex-items-xl-left { + justify-content: flex-start; + } + .flex-items-xl-center { + justify-content: center; + } + .flex-items-xl-right { + justify-content: flex-end; + } + .flex-items-xl-around { + justify-content: space-around; + } + .flex-items-xl-between { + justify-content: space-between; + } +} + +.tag-default { + color: #fff !important; + font-size: 0.8rem; + padding-top: 0.1rem; + padding-bottom: 0.1rem; + white-space: nowrap; + margin-right: 3px; + margin-bottom: 0.2rem; + display: inline-block; +} + +.tag-default:hover { + text-decoration: none; +} + +.tag-default.tag-outline { + border: 1px solid #ddd; + color: #aaa !important; + background: 0 0 !important; +} + +ul.tag-list { + padding-left: 0 !important; + display: inline-block; + list-style: none !important; +} + +ul.tag-list li { + display: inline-block !important; +} + +.navbar-brand { + font-family: titillium web, sans-serif; + font-size: 1.5rem !important; + padding-top: 0 !important; + margin-right: 2rem !important; + color: #ec8cb5 !important; +} + +.nav-link .user-pic { + height: 26px; + border-radius: 50px; + float: left; + margin-right: 5px; +} + +.nav-link:hover { + transition: 0.1s all; +} + +.nav-pills.outline-active .nav-link { + border-radius: 0; + border: none; + border-bottom: 2px solid transparent; + background: 0 0; + color: #aaa; +} + +.nav-pills.outline-active .nav-link:hover { + color: #ffff; +} + +.nav-pills.outline-active .nav-link.active { + background: #211f21 !important; + border-bottom: 2px solid #ec8cb5 !important; + color: #ec8cb5 !important; +} + +footer { + text-align: center; + font-size: 0.875em; + color: rgb(255, 255, 255, 0.3); + margin-top: 3rem; + padding: 1rem 0; + position: absolute; + bottom: 0; + width: 100%; +} + +footer .logo-font { + vertical-align: middle; +} + +footer .attribution { + vertical-align: middle; + margin-left: 10px; + font-size: 0.8rem; + color: #bbb; + font-weight: 300; +} + +.error-messages { + color: #b85c5c !important; + font-weight: 700; +} + +.banner { + color: #fff; + background: #ec8cb578 !important; + padding: 2rem; + margin-bottom: 2rem; +} + +.banner h1 { + text-shadow: 0 1px 3px #8c8889; + margin-bottom: 0; +} + +.container.page { + margin-top: 1.5rem; +} + +.preview-link { + color: inherit !important; +} + +.preview-link:hover { + text-decoration: inherit !important; +} + +.article-meta { + display: block; + position: relative; + font-weight: 300; +} + +.article-meta img { + display: inline-block; + vertical-align: middle; + height: 32px; + width: 32px; + border-radius: 30px; +} + +.article-meta .info { + margin: 0 1.5rem 0 0.3rem; + display: inline-block; + vertical-align: middle; + line-height: 1rem; +} + +.article-meta .info .author { + display: block; + font-weight: 500 !important; +} + +.article-meta .info .date { + color: #bbb; + font-size: 0.8rem; + display: block; +} + +.article-preview { + background-color: rgb(13, 10, 11); + border-radius: 20px; + border-top: 10px solid rgba(255, 255, 255, 0.1); + padding: 1.5rem; +} + +.article-preview .article-meta { + margin: 0 0 1rem; +} + +.article-preview .preview-link h1 { + font-weight: 600 !important; + font-size: 1.5rem !important; + margin-bottom: 3px; +} + +.article-preview .preview-link p { + font-weight: 300; + font-size: 24px; + color: #999; + margin-bottom: 15px; + font-size: 1rem; + line-height: 1.3rem; +} + +.article-preview .preview-link span { + max-width: 30%; + font-size: 0.8rem; + font-weight: 300; + color: #bbb; + vertical-align: middle; +} + +.article-preview .preview-link ul { + float: right; + max-width: 50%; + vertical-align: top; +} + +.article-preview .preview-link ul li { + font-weight: 300; + font-size: 0.8rem !important; + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.btn .counter { + font-size: 0.8rem !important; +} + +.home-page .banner { + background: #ec8cb5; + box-shadow: inset 0 8px 8px -8px #8c8889, inset 0 -8px 8px -8px #8c8889; +} + +.home-page .banner p { + color: #fff; + text-align: center; + font-size: 1.5rem; + font-weight: 300 !important; + margin-bottom: 0; +} + +.home-page .banner h1 { + text-shadow: 0 1px 3px #8c8889; + font-weight: 700 !important; + text-align: center; + font-size: 3.5rem; + padding-bottom: 0.5rem; +} + +.home-page .feed-toggle { + margin-bottom: -1px; +} + +.home-page .sidebar { + padding: 5px 10px 10px; + background: #6a7287; + border-radius: 10px; +} + +.home-page .sidebar p { + margin-bottom: 0.2rem; +} + +.article-page .banner { + padding: 2rem 0; +} + +.article-page .banner h1 { + font-size: 2.8rem; + font-weight: 600; +} + +.article-page .banner .btn { + opacity: 0.8; +} + +.article-page .banner .btn:hover { + transition: 0.1s all; + opacity: 1; +} + +.article-page .banner .article-meta { + margin: 2rem 0 0; +} + +.article-page .banner .article-meta .author { + color: #fff; +} + +.article-page .article-content p { + font-family: 'source serif pro', serif; + font-size: 1.2rem; + line-height: 1.8rem; + margin-bottom: 2rem; +} + +.article-page .article-content div { + font-size: 1.2rem; +} + +.article-page .article-content h1, +.article-page .article-content h2, +.article-page .article-content h3, +.article-page .article-content h4, +.article-page .article-content h5, +.article-page .article-content h6 { + font-weight: 500 !important; + margin: 1.6rem 0 1rem; +} + +.article-page .article-actions { + text-align: center; + margin: 1.5rem 0 3rem; +} + +.article-page .article-actions .article-meta .info { + text-align: left; +} + +.article-page .comment-form .card-block { + padding: 0; +} + +.article-page .comment-form .card-block textarea { + border: 0; + padding: 1.25rem; +} + +.article-page .comment-form .card-footer .btn { + font-weight: 700; + float: right; +} + +.article-page .comment-form .card-footer .comment-author-img { + height: 30px; + width: 30px; +} + +.article-page .card { + border: 1px solid #e5e5e5; + box-shadow: none !important; +} + +.article-page .card .card-footer { + border-top: 1px solid #e5e5e5; + box-shadow: none !important; + font-size: 0.8rem; + font-weight: 300; +} + +.article-page .card .comment-author-img { + display: inline-block; + vertical-align: middle; + height: 20px; + width: 20px; + border-radius: 30px; +} + +.article-page .card .comment-author { + display: inline-block; + vertical-align: middle; +} + +.article-page .card .date-posted { + display: inline-block; + vertical-align: middle; + margin-left: 5px; + color: #bbb; +} + +.article-page .card .mod-options { + float: right; + color: #fff; + font-size: 1rem; +} + +.article-page .card .mod-options i { + margin-left: 5px; + opacity: 0.6; + cursor: pointer; +} + +.article-page .card .mod-options i:hover { + opacity: 1; +} + +.profile-page .user-info { + text-align: center; + background: #6a7287; + padding: 2rem 0 1rem; +} + +.profile-page .user-info .user-img { + width: 100px; + height: 100px; + border-radius: 100px; + margin-bottom: 1rem; +} + +.profile-page .user-info h4 { + font-weight: 700; +} + +.profile-page .user-info p { + margin: 0 auto 0.5rem; + color: #aaa; + max-width: 450px; + font-weight: 300; +} + +.profile-page .user-info .action-btn { + float: right; + color: #999; + border: 1px solid #999; +} + +.profile-page .articles-toggle { + margin: 1.5rem 0 -1px; +} + +.editor-page .tag-list i { + font-size: 0.6rem; + margin-right: 5px; + cursor: pointer; +} diff --git a/python/capital/frontend/src/agent.js b/python/capital/frontend/src/agent.js new file mode 100644 index 000000000..a21ab1ce2 --- /dev/null +++ b/python/capital/frontend/src/agent.js @@ -0,0 +1,378 @@ +const API_ROOT = + process.env.REACT_APP_BACKEND_URL ?? 'https://conduit.productionready.io/api'; + +/** + * Serialize object to URL params + * + * @param {Record} object + * @returns {String} + */ +function serialize(object) { + const params = []; + + for (const param in object) { + if (Object.hasOwnProperty.call(object, param) && object[param] != null) { + params.push(`${param}=${encodeURIComponent(object[param])}`); + } + } + + return params.join('&'); +} + +let token = null; + +/** + * + * @typedef {Object} ApiError + * @property {{[property: string]: string}} errors + */ + +/** + * @typedef {Object} UserAuth + * @property {Object} user + * @property {String} user.email + * @property {String} user.username + * @property {String} user.bio + * @property {String} user.image + * @property {String} user.token + * + * @typedef {Object} Profile + * @property {String} username + * @property {String} bio + * @property {String} image + * @property {Boolean} following + * + * @typedef {Object} Tags + * @property {String[]} tags + * + * @typedef {Object} Article + * @property {String} title + * @property {String} slug + * @property {String} body + * @property {String} description + * @property {String[]} tagList + * @property {Profile} author + * @property {Boolean} favorited + * @property {Number} favoritesCount + * @property {String} createdAt + * @property {String} updatedAt + * + * @typedef {Object} ArticleResponse + * @property {Article} article + * + * @typedef {Object} ArticlesResponse + * @property {Article[]} articles + * @property {Number} articlesCount + * + * @typedef {Object} Comment + * @property {String} id + * @property {String} body + * @property {Profile} author + * @property {String} createdAt + * @property {String} updatedAt + * + * @typedef {Object} CommentResponse + * @property {Comment} comment + * + * @typedef {Object} CommentsResponse + * @property {Comment[]} comments + * + * @typedef {Object} ProfileResponse + * @property {Profile} profile + */ + +/** + * API client + * + * @param {String} url The endpoint + * @param {Object} body The request's body + * @param {('GET'|'DELETE'|'PUT'|'POST')} [method='GET'] The request's method + * + * @throws {@link ApiError API Error} + * + * @returns {Promise} API response's body + */ +const agent = async (url, body, method = 'GET') => { + const headers = new Headers(); + + if (body) { + headers.set('Content-Type', 'application/json'); + } + + if (token) { + headers.set('Authorization', `Token ${token}`); + } + + const response = await fetch(`${API_ROOT}${url}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + let result; + + try { + result = await response.json(); + } catch (error) { + result = { errors: { [response.status]: [response.statusText] } }; + } + + if (!response.ok) throw result; + + return result; +}; + +const requests = { + /** + * Send a DELETE request + * + * @param {String} url The endpoint + * @returns {Promise} + */ + del: (url) => agent(url, undefined, 'DELETE'), + /** + * Send a GET request + * + * @param {String} url The endpoint + * @param {Object} [query={}] URL parameters + * @param {Number} [query.limit=10] + * @param {Number} [query.page] + * @param {String} [query.author] + * @param {String} [query.tag] + * @param {String} [query.favorited] + * @returns {Promise} + */ + get: (url, query = {}) => { + if (Number.isSafeInteger(query?.page)) { + query.limit = query.limit ? query.limit : 10; + query.offset = query.page * query.limit; + } + delete query.page; + const isEmptyQuery = query == null || Object.keys(query).length === 0; + + return agent(isEmptyQuery ? url : `${url}?${serialize(query)}`); + }, + /** + * Send a PUT request + * + * @param {String} url The endpoint + * @param {Record} body The request's body + * @returns {Promise} + */ + put: (url, body) => agent(url, body, 'PUT'), + /** + * Send a POST request + * + * @param {String} url The endpoint + * @param {Record} body The request's body + * @returns {Promise} + */ + post: (url, body) => agent(url, body, 'POST'), +}; + +const Auth = { + /** + * Get current user + * + * @returns {Promise} + */ + current: () => requests.get('/user'), + /** + * Login with email and password + * + * @param {String} email + * @param {String} password + * @returns {Promise} + */ + login: (email, password) => + requests.post('/v2/users/login', { user: { email, password } }), + /** + * Register with username, email and password + * + * @param {String} username + * @param {String} email + * @param {String} password + * @returns {Promise} + */ + register: (username, email, password) => + requests.post('/users', { user: { username, email, password } }), + /** + * Update user + * + * @param {Object} user + * @param {String} [user.email] + * @param {String} [user.username] + * @param {String} [user.bio] + * @param {String} [user.image] + * @param {String} [user.password] + * @returns {Promise} + */ + save: (user) => requests.put('/user', { user }), +}; + +const Tags = { + /** + * Get all tags + * + * @returns {Promise} + */ + getAll: () => requests.get('/tags'), +}; + +const Articles = { + /** + * Get all articles + * + * @param {Object} query Article's query parameters + * @param {Number} [query.limit=10] + * @param {Number} [query.page] + * @param {String} [query.author] + * @param {String} [query.tag] + * @param {String} [query.favorited] + * @returns {Promise} + */ + all: (query) => requests.get(`/articles`, query), + /** + * Get all articles from author + * + * @param {String} author Article's author + * @param {Number} [page] + * @returns {Promise} + */ + byAuthor: (author, page) => + requests.get(`/articles`, { author, limit: 5, page }), + /** + * Get all articles by tag + * + * @param {String} tag Article's tag + * @param {Number} page + * @returns {Promise} + */ + byTag: (tag, page) => requests.get(`/articles`, { tag, page }), + /** + * Remove one article + * + * @param {String} slug Article's slug + * @returns {Promise<{}>} + */ + del: (slug) => requests.del(`/articles/${slug}`), + /** + * Favorite one article + * + * @param {String} slug Article's slug + * @returns {Promise} + */ + favorite: (slug) => requests.post(`/articles/${slug}/favorite`), + /** + * Get article favorited by author + * + * @param {String} username Username + * @param {Number} [page] + * @returns {Promise} + */ + favoritedBy: (username, page) => + requests.get(`/articles`, { favorited: username, limit: 5, page }), + /** + * Get all articles in the user's feed + * + * @param {Number} [page] + * @returns {Promise} + */ + feed: (page) => requests.get('/articles/feed', { page }), + /** + * Get one article by slug + * + * @param {String} slug Article's slug + * @returns {Promise} + */ + get: (slug) => requests.get(`/articles/${slug}`), + /** + * Unfavorite one article + * + * @param {String} slug Article's slug + * @returns {Promise} + */ + unfavorite: (slug) => requests.del(`/articles/${slug}/favorite`), + /** + * Update one article + * + * @param {Partial
} article + * @returns {Promise} + */ + update: ({ slug, ...article }) => + requests.put(`/articles/${slug}`, { article }), + /** + * Create a new article + * + * @param {Object} article + * @param {String} article.title + * @param {String} article.description + * @param {String} article.body + * @param {String[]} article.tagList + * @returns {Promise} + */ + create: (article) => requests.post('/articles', { article }), +}; + +const Comments = { + /** + * Create a new comment for article + * + * @param {String} slug Article's slug + * @param {Object} comment + * @param {String} comment.body + * @returns {Promise} + */ + create: (slug, comment) => + requests.post(`/articles/${slug}/comments`, { comment }), + /** + * Remove one comment + * + * @param {String} slug Article's slug + * @param {String} commentId Comment's id + * @returns {Promise<{}>} + */ + delete: (slug, commentId) => + requests.del(`/articles/${slug}/comments/${commentId}`), + /** + * Get all comments for one article + * + * @param {String} slug Article's slug + * @returns {Promise} + */ + forArticle: (slug) => requests.get(`/articles/${slug}/comments`), +}; + +const Profile = { + /** + * Follow another user + * + * @param {String} username User's username + * @returns {Profile} + */ + follow: (username) => requests.post(`/profiles/${username}/follow`), + /** + * Get the profile of an user + * + * @param {String} username User's username + * @returns {Profile} + */ + get: (username) => requests.get(`/profiles/${username}`), + /** + * Unfollow another user + * + * @param {String} username User's username + * @returns {Profile} + */ + unfollow: (username) => requests.del(`/profiles/${username}/follow`), +}; + +export default { + Articles, + Auth, + Comments, + Profile, + Tags, + setToken: (_token) => { + token = _token; + }, +}; diff --git a/python/capital/frontend/src/app/history.js b/python/capital/frontend/src/app/history.js new file mode 100644 index 000000000..9937105a6 --- /dev/null +++ b/python/capital/frontend/src/app/history.js @@ -0,0 +1,3 @@ +import { createBrowserHistory } from 'history'; + +export default createBrowserHistory(); diff --git a/python/capital/frontend/src/app/middleware.js b/python/capital/frontend/src/app/middleware.js new file mode 100644 index 000000000..3984f1bc8 --- /dev/null +++ b/python/capital/frontend/src/app/middleware.js @@ -0,0 +1,31 @@ +import agent from '../agent'; +import { + login, + logout, + register, + updateUser, +} from '../features/auth/authSlice'; + +const localStorageMiddleware = (store) => (next) => (action) => { + switch (action.type) { + case register.fulfilled.type: + case login.fulfilled.type: + window.localStorage.setItem('jwt', action.payload.token); + agent.setToken(action.payload.token); + break; + + case logout.type: + window.localStorage.removeItem('jwt'); + agent.setToken(undefined); + break; + + case updateUser.fulfilled.type: + window.localStorage.setItem('jwt', action.payload.token); + agent.setToken(action.payload.token); + break; + } + + return next(action); +}; + +export { localStorageMiddleware }; diff --git a/python/capital/frontend/src/app/store.js b/python/capital/frontend/src/app/store.js new file mode 100644 index 000000000..5fd6311d3 --- /dev/null +++ b/python/capital/frontend/src/app/store.js @@ -0,0 +1,38 @@ +import { configureStore } from '@reduxjs/toolkit'; +// import { connectRouter, routerMiddleware } from 'connected-react-router'; + +import authReducer from '../features/auth/authSlice'; +import commentsReducer from '../features/comments/commentsSlice'; +import tagsReducer from '../features/tags/tagsSlice'; +import history from './history'; +import { localStorageMiddleware } from './middleware'; +import articleReducer from '../reducers/article'; +import articlesReducer from '../reducers/articleList'; +import commonReducer from '../reducers/common'; +import profileReducer from '../reducers/profile'; + +export function makeStore(preloadedState) { + return configureStore({ + reducer: { + article: articleReducer, + articleList: articlesReducer, + auth: authReducer, + comments: commentsReducer, + common: commonReducer, + profile: profileReducer, + tags: tagsReducer, + // router: connectRouter(history), + }, + devTools: true, + preloadedState, + middleware: (getDefaultMiddleware) => [ + ...getDefaultMiddleware(), + // routerMiddleware(history), + localStorageMiddleware, + ], + }); +} + +const store = makeStore(); + +export default store; diff --git a/python/capital/frontend/src/common/utils.js b/python/capital/frontend/src/common/utils.js new file mode 100644 index 000000000..8eda2772b --- /dev/null +++ b/python/capital/frontend/src/common/utils.js @@ -0,0 +1,43 @@ +/** + * States of the slice + * @readonly + * @enum {string} + */ +export const Status = { + /** The initial state */ + IDLE: 'idle', + /** The loading state */ + LOADING: 'loading', + /** The success state */ + SUCCESS: 'success', + /** The error state */ + FAILURE: 'failure', +}; + +/** + * Check if error is an ApiError + * + * @param {object} error + * @returns {boolean} error is ApiError + */ +export function isApiError(error) { + return typeof error === 'object' && error !== null && 'errors' in error; +} + +/** + * Set state as loading + * + * @param {import('@reduxjs/toolkit').Draft} state + */ +export function loadingReducer(state) { + state.status = Status.LOADING; +} + +/** + * @param {import('@reduxjs/toolkit').Draft} state + * @param {import('@reduxjs/toolkit').PayloadAction<{errors: Record} action + */ +export function failureReducer(state, action) { + state.status = Status.FAILURE; + state.errors = action.payload.errors; +} diff --git a/python/capital/frontend/src/components/AdminPage.js b/python/capital/frontend/src/components/AdminPage.js new file mode 100644 index 000000000..9f0494c57 --- /dev/null +++ b/python/capital/frontend/src/components/AdminPage.js @@ -0,0 +1,20 @@ +import React, { memo } from 'react'; + +function AdminPage() { + return ( + <> +
+ Deprecated +
+ + ); +} + +export default memo(AdminPage); diff --git a/python/capital/frontend/src/components/App.js b/python/capital/frontend/src/components/App.js new file mode 100644 index 000000000..ed2a31a47 --- /dev/null +++ b/python/capital/frontend/src/components/App.js @@ -0,0 +1,101 @@ +import React, { lazy, Suspense, useEffect, memo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Route, Routes } from 'react-router-dom'; + +import Home from '../components/Home'; +import { appLoad, clearRedirect } from '../reducers/common'; +import Header from './Header'; +import Footer from './Footer'; +import Membership from './Membership/Membership'; + +const Article = lazy(() => + import( + /* webpackChunkName: "Article", webpackPrefetch: true */ '../components/Article' + ) +); +const Editor = lazy(() => + import( + /* webpackChunkName: "Editor", webpackPrefetch: true */ '../components/Editor' + ) +); +const AuthScreen = lazy(() => + import( + /* webpackChunkName: "AuthScreen", webpackPrefetch: true */ '../features/auth/AuthScreen' + ) +); +const Profile = lazy(() => + import( + /* webpackChunkName: "Profile", webpackPrefetch: true */ '../components/Profile' + ) +); +const SettingsScreen = lazy(() => + import( + /* webpackChunkName: "SettingsScreen", webpackPrefetch: true */ '../features/auth/SettingsScreen' + ) +); +const AdminPage = lazy(() => + import( + /* webpackChunkName: "SettingsScreen", webpackPrefetch: true */ '../components/AdminPage' + ) +); + +const Logging = lazy(() => + import( + /* webpackChunkName: "SettingsScreen", webpackPrefetch: true */ '../components/Logging' + ) +); + +function App() { + const dispatch = useDispatch(); + const redirectTo = useSelector((state) => state.common.redirectTo); + const appLoaded = useSelector((state) => state.common.appLoaded); + + useEffect(() => { + if (redirectTo) { + // dispatch(push(redirectTo)); + dispatch(clearRedirect()); + } + }, [redirectTo]); + + useEffect(() => { + const token = window.localStorage.getItem('jwt'); + dispatch(appLoad(token)); + }, []); + + if (appLoaded) { + return ( + <> +
+ Loading...

}> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } + /> + } /> + +
+