├── .jshintrc
├── demo.gif
├── docs
├── favicon.ico
├── apple-icon.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon-96x96.png
├── ms-icon-70x70.png
├── ms-icon-144x144.png
├── ms-icon-150x150.png
├── ms-icon-310x310.png
├── LibrarianOGImage.png
├── android-icon-36x36.png
├── android-icon-48x48.png
├── android-icon-72x72.png
├── android-icon-96x96.png
├── apple-icon-114x114.png
├── apple-icon-120x120.png
├── apple-icon-144x144.png
├── apple-icon-152x152.png
├── apple-icon-180x180.png
├── apple-icon-57x57.png
├── apple-icon-60x60.png
├── apple-icon-72x72.png
├── apple-icon-76x76.png
├── android-icon-144x144.png
├── android-icon-192x192.png
├── apple-icon-precomposed.png
├── 93f4cef4-6746-4c2b-b623-9eb31fd86c16.png
├── adaf322b-9ca8-48cc-ac15-9c63009da3fe.png
├── ae663e7c-1d75-45d3-b47c-4f9356303a69.gif
├── e48ae75b-2c22-4673-9a70-fec50cfcbf6c.png
├── browserconfig.xml
├── manifest.json
├── css.css
├── index.html
└── AllTemplates.min.css
├── analytics.js
├── LICENSE
├── .gitignore
├── package.json
├── webBridge.js
├── README.md
├── setup.js
└── librarian.js
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "esversion": 6
3 | }
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/demo.gif
--------------------------------------------------------------------------------
/docs/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/favicon.ico
--------------------------------------------------------------------------------
/docs/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/apple-icon.png
--------------------------------------------------------------------------------
/docs/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/favicon-32x32.png
--------------------------------------------------------------------------------
/docs/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/favicon-96x96.png
--------------------------------------------------------------------------------
/docs/ms-icon-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/ms-icon-70x70.png
--------------------------------------------------------------------------------
/docs/ms-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/ms-icon-144x144.png
--------------------------------------------------------------------------------
/docs/ms-icon-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/ms-icon-150x150.png
--------------------------------------------------------------------------------
/docs/ms-icon-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/ms-icon-310x310.png
--------------------------------------------------------------------------------
/docs/LibrarianOGImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/LibrarianOGImage.png
--------------------------------------------------------------------------------
/docs/android-icon-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/android-icon-36x36.png
--------------------------------------------------------------------------------
/docs/android-icon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/android-icon-48x48.png
--------------------------------------------------------------------------------
/docs/android-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/android-icon-72x72.png
--------------------------------------------------------------------------------
/docs/android-icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/android-icon-96x96.png
--------------------------------------------------------------------------------
/docs/apple-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/apple-icon-114x114.png
--------------------------------------------------------------------------------
/docs/apple-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/apple-icon-120x120.png
--------------------------------------------------------------------------------
/docs/apple-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/apple-icon-144x144.png
--------------------------------------------------------------------------------
/docs/apple-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/apple-icon-152x152.png
--------------------------------------------------------------------------------
/docs/apple-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/apple-icon-180x180.png
--------------------------------------------------------------------------------
/docs/apple-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/apple-icon-57x57.png
--------------------------------------------------------------------------------
/docs/apple-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/apple-icon-60x60.png
--------------------------------------------------------------------------------
/docs/apple-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/apple-icon-72x72.png
--------------------------------------------------------------------------------
/docs/apple-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/apple-icon-76x76.png
--------------------------------------------------------------------------------
/docs/android-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/android-icon-144x144.png
--------------------------------------------------------------------------------
/docs/android-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/android-icon-192x192.png
--------------------------------------------------------------------------------
/docs/apple-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/apple-icon-precomposed.png
--------------------------------------------------------------------------------
/docs/93f4cef4-6746-4c2b-b623-9eb31fd86c16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/93f4cef4-6746-4c2b-b623-9eb31fd86c16.png
--------------------------------------------------------------------------------
/docs/adaf322b-9ca8-48cc-ac15-9c63009da3fe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/adaf322b-9ca8-48cc-ac15-9c63009da3fe.png
--------------------------------------------------------------------------------
/docs/ae663e7c-1d75-45d3-b47c-4f9356303a69.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/ae663e7c-1d75-45d3-b47c-4f9356303a69.gif
--------------------------------------------------------------------------------
/docs/e48ae75b-2c22-4673-9a70-fec50cfcbf6c.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biocross/Librarian/HEAD/docs/e48ae75b-2c22-4673-9a70-fec50cfcbf6c.png
--------------------------------------------------------------------------------
/docs/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 | #ffffff
--------------------------------------------------------------------------------
/analytics.js:
--------------------------------------------------------------------------------
1 | const https = require('https');
2 |
3 | // These analytics ONLY send event names for understanding usage of the librarian.
4 | // Absolutely NO system metadata or personal information is ever collected or sent.
5 |
6 | ///// Simple, Privary Aware Analytics: https://curl.press
7 |
8 | const sendEvent = async (event) => {
9 | if (event && event.length && event.length > 0) {
10 | try {
11 | const req = https.get(`https://curl.press/api/librarian/add?event=${event}`).on('error', function(err) {})
12 | } catch (error) {}
13 | }
14 | }
15 |
16 | const LibrarianEvents = {
17 | SetupStarted: "setup.start",
18 | SetupComplete: "setup.finish",
19 | SetupError: "setup.error",
20 | ServerStarted: "server.start",
21 | ServerError: "server.error",
22 | BuildSubmitted: "build.submit"
23 | }
24 |
25 | module.exports = { sendEvent, LibrarianEvents };
--------------------------------------------------------------------------------
/docs/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "App",
3 | "icons": [
4 | {
5 | "src": "\/android-icon-36x36.png",
6 | "sizes": "36x36",
7 | "type": "image\/png",
8 | "density": "0.75"
9 | },
10 | {
11 | "src": "\/android-icon-48x48.png",
12 | "sizes": "48x48",
13 | "type": "image\/png",
14 | "density": "1.0"
15 | },
16 | {
17 | "src": "\/android-icon-72x72.png",
18 | "sizes": "72x72",
19 | "type": "image\/png",
20 | "density": "1.5"
21 | },
22 | {
23 | "src": "\/android-icon-96x96.png",
24 | "sizes": "96x96",
25 | "type": "image\/png",
26 | "density": "2.0"
27 | },
28 | {
29 | "src": "\/android-icon-144x144.png",
30 | "sizes": "144x144",
31 | "type": "image\/png",
32 | "density": "3.0"
33 | },
34 | {
35 | "src": "\/android-icon-192x192.png",
36 | "sizes": "192x192",
37 | "type": "image\/png",
38 | "density": "4.0"
39 | }
40 | ]
41 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Siddharth Gupta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Mac OS X
2 | .DS_Store
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 |
17 | # Directory for instrumented libs generated by jscoverage/JSCover
18 | lib-cov
19 |
20 | # Coverage directory used by tools like istanbul
21 | coverage
22 |
23 | # nyc test coverage
24 | .nyc_output
25 |
26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
27 | .grunt
28 |
29 | # Bower dependency directory (https://bower.io/)
30 | bower_components
31 |
32 | # node-waf configuration
33 | .lock-wscript
34 |
35 | # Compiled binary addons (https://nodejs.org/api/addons.html)
36 | build/Release
37 |
38 | # Dependency directories
39 | node_modules/
40 | jspm_packages/
41 |
42 | # TypeScript v1 declaration files
43 | typings/
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional eslint cache
49 | .eslintcache
50 |
51 | # Optional REPL history
52 | .node_repl_history
53 |
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # dotenv environment variables file
61 | .env
62 |
63 | # next.js build output
64 | .next
65 |
66 | # vuepress build output
67 | .vuepress/dist
68 |
69 | # Serverless directories
70 | .serverless
71 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "librarian-server",
3 | "version": "1.4.1",
4 | "description": "Librarian is a local server for your iOS & Android builds, cause local is best!",
5 | "homepage": "https://github.com/biocross/Librarian",
6 | "repository": "https://github.com/biocross/Librarian",
7 | "main": "librarian.js",
8 | "bin": {
9 | "librarian": "./librarian.js"
10 | },
11 | "scripts": {
12 | "test": "echo \"Error: no test specified\" && exit 1"
13 | },
14 | "files": [
15 | "librarian.js",
16 | "setup.js",
17 | "webBridge.js",
18 | "analytics.js"
19 | ],
20 | "keywords": [
21 | "cli-app",
22 | "local",
23 | "build",
24 | "server",
25 | "fastlane",
26 | "ci",
27 | "ios",
28 | "android",
29 | "enterprise",
30 | "host",
31 | "ngrok"
32 | ],
33 | "author": "Siddharth Gupta",
34 | "license": "MIT",
35 | "dependencies": {
36 | "app-metadata": "^0.1.26",
37 | "chalk": "^2.4.2",
38 | "commander": "^3.0.2",
39 | "fs-extra": "8.1.0",
40 | "inquirer": "^7.0.0",
41 | "js-yaml": "^3.13.1",
42 | "lodash": "4.17.15",
43 | "ngrok": "^3.2.5",
44 | "node-persist": "^3.0.5",
45 | "plist": "^3.0.1",
46 | "qrcode-terminal": "^0.12.0",
47 | "simple-git": "^1.126.0",
48 | "update-notifier": "^3.0.1"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/webBridge.js:
--------------------------------------------------------------------------------
1 | /*jshint esversion: 6 */
2 | const { configurationKey } = require('./setup.js');
3 | const fs = require('fs-extra');
4 | const yaml = require('js-yaml');
5 | const webConfigurationPath = 'web/_data/config.json';
6 | const assetServerConfigurationPath = 'asset_server/_data/config.json';
7 | const buildsDataPath = 'web/_builds/';
8 |
9 | const setWebConfiguration = async (preferences, configuration) => {
10 | try {
11 | const prefs = await preferences.getItem(configurationKey);
12 | const webConfigPath = prefs.working_directory + webConfigurationPath;
13 | const webConfiguration = JSON.parse(fs.readFileSync(webConfigPath, 'utf8'));
14 | Object.assign(webConfiguration, configuration);
15 | fs.writeFileSync(webConfigPath, JSON.stringify(webConfiguration));
16 | if (prefs.assets_web) {
17 | const assetServerConfigPath = prefs.working_directory + assetServerConfigurationPath;
18 | fs.copySync(webConfigPath, assetServerConfigPath);
19 | }
20 | } catch (error) {
21 | console.log(error);
22 | }
23 | }
24 |
25 | const addBuild = async (preferences, build) => {
26 | try {
27 | const prefs = await preferences.getItem(configurationKey);
28 | const webConfigPath = prefs.working_directory + webConfigurationPath;
29 | const webConfiguration = JSON.parse(fs.readFileSync(webConfigPath, 'utf8'));
30 | const buildPath = prefs.working_directory + buildsDataPath + build.folderPath + '.md';
31 | const contents = `---\nlayout: build\n${yaml.safeDump(build)}---\n`;
32 | fs.writeFileSync(buildPath, contents);
33 |
34 | if (!webConfiguration.initialized) {
35 | await setWebConfiguration(preferences, { "initialized": true });
36 | }
37 | } catch (error) {
38 | console.log(error);
39 | }
40 | }
41 |
42 | module.exports = { setWebConfiguration, addBuild };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | > Librarian is an easy way to serve your iOS & Android builds on your local network, and make testing internal versions of your app effortless.
8 |
9 | [](https://www.npmjs.org/package/librarian-server)
10 |
11 |
12 |
13 | ## Highlights
14 |
15 | - [x] Support for iOS `IPA` & Android `APK`
16 | - [x] Simple & Quick Setup
17 | - [x] Clean Web Interface
18 | - [x] Easily add builds
19 | - [x] Ability to have Internet accessible Public URLs for builds
20 | - [x] Instant app installs on the local network, your testers don't have to wait!
21 | - [x] No more dependency on `Crashlytics Beta` / `Testflight`
22 |
23 | ## Install
24 |
25 | ```console
26 | $ sudo npm i -g librarian-server
27 | $ librarian setup
28 | $ sudo npm link librarian-server # If you can get an `EACCESS / Permissions` error
29 | ```
30 |
31 | The setup will ask you a few questions to configure Librarian on your system. You can just press enter throughout the process to choose the default values.
32 |
33 | ## Usage
34 |
35 | ### Starting Librarian
36 |
37 | Run the following command to start the Librarian server.
38 |
39 | ```console
40 | $ librarian start
41 | ```
42 | This will start the web interface, and will print the URL to it on the console, along with a QR code to the URL for quick access 😁
43 |
44 | > Librarian uses [ngrok](https://ngrok.com/product) tunneling to serve your localhost over the Internet using a secure `HTTPS` tunnel. Also, `HTTPS` is mandatory for iOS Builds to work.
45 |
46 | ### Submitting Builds
47 |
48 | Submit builds to Librarian using:
49 |
50 | ```console
51 | $ librarian submit [options]
52 | ```
53 | The `pathToFile` must be the full path to the `IPA` or `APK` file. Example: `/Users/jenkins/MyApp.ipa`, and should be accessible by Librarian.
54 |
55 | You can pass in the following additional options along with the path of the build file.
56 |
57 | Option | Short | Example | Description
58 | --- | --- | --- | ---
59 | `--branch ` | `-b` | `--branch master` | git branch the build is from
60 | `--notes ` | `-n` | `--notes "Release Candidate Build"` | release notes for the build
61 | `--public` | `-p` | Just add the flag `--public` | allow the build to be downloaded over the HTTPs tunnel (by default, builds can only be downloaded on the local network)
62 |
63 | Librarian will autodetect the type of build `iOS / Android` using the file extension, will create a copy of the build in it's assets, and make it available for download on it's web interface.
64 |
65 | > The Librarian server should be running while submitting a build.
66 |
67 | ## Updating Librarian
68 |
69 | Librarian follows [semantic versioning](https://semver.org/). You can update by running:
70 |
71 | ```console
72 | $ npm i -g librarian-server
73 | $ librarian update
74 | ```
75 |
76 | ## Contributing
77 |
78 | Librarian is built up of two parts:
79 |
80 | - [Librarian](https://github.com/biocross/Librarian) - The Command Line tool, written in NodeJS (this repository).
81 | - [Librarian Web](https://github.com/biocross/Librarian-Web) - The Web Interface of Librarian, built in Jekyll.
82 |
83 |
84 | ## Maintainers
85 |
86 | Developed by [biocross](https://twitter.com/sids7) & designed by [madebytushar](https://twitter.com/madebytushar)
87 |
88 | ## License
89 |
90 | MIT
91 |
--------------------------------------------------------------------------------
/docs/css.css:
--------------------------------------------------------------------------------
1 | /* cyrillic-ext */
2 | @font-face {
3 | font-family: 'Roboto';
4 | font-style: normal;
5 | font-weight: 900;
6 | src: local('Roboto Black'), local('Roboto-Black'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmYUtfCRc4AMP6lbBP.woff2) format('woff2');
7 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
8 | }
9 | /* cyrillic */
10 | @font-face {
11 | font-family: 'Roboto';
12 | font-style: normal;
13 | font-weight: 900;
14 | src: local('Roboto Black'), local('Roboto-Black'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmYUtfABc4AMP6lbBP.woff2) format('woff2');
15 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
16 | }
17 | /* greek-ext */
18 | @font-face {
19 | font-family: 'Roboto';
20 | font-style: normal;
21 | font-weight: 900;
22 | src: local('Roboto Black'), local('Roboto-Black'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmYUtfCBc4AMP6lbBP.woff2) format('woff2');
23 | unicode-range: U+1F00-1FFF;
24 | }
25 | /* greek */
26 | @font-face {
27 | font-family: 'Roboto';
28 | font-style: normal;
29 | font-weight: 900;
30 | src: local('Roboto Black'), local('Roboto-Black'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmYUtfBxc4AMP6lbBP.woff2) format('woff2');
31 | unicode-range: U+0370-03FF;
32 | }
33 | /* vietnamese */
34 | @font-face {
35 | font-family: 'Roboto';
36 | font-style: normal;
37 | font-weight: 900;
38 | src: local('Roboto Black'), local('Roboto-Black'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmYUtfCxc4AMP6lbBP.woff2) format('woff2');
39 | unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
40 | }
41 | /* latin-ext */
42 | @font-face {
43 | font-family: 'Roboto';
44 | font-style: normal;
45 | font-weight: 900;
46 | src: local('Roboto Black'), local('Roboto-Black'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmYUtfChc4AMP6lbBP.woff2) format('woff2');
47 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
48 | }
49 | /* latin */
50 | @font-face {
51 | font-family: 'Roboto';
52 | font-style: normal;
53 | font-weight: 900;
54 | src: local('Roboto Black'), local('Roboto-Black'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmYUtfBBc4AMP6lQ.woff2) format('woff2');
55 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
56 | }
57 | /* cyrillic-ext */
58 | @font-face {
59 | font-family: 'Roboto Mono';
60 | font-style: normal;
61 | font-weight: 400;
62 | src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://fonts.gstatic.com/s/robotomono/v5/L0x5DF4xlVMF-BfR8bXMIjhGq3-cXbKDO1w.woff2) format('woff2');
63 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
64 | }
65 | /* cyrillic */
66 | @font-face {
67 | font-family: 'Roboto Mono';
68 | font-style: normal;
69 | font-weight: 400;
70 | src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://fonts.gstatic.com/s/robotomono/v5/L0x5DF4xlVMF-BfR8bXMIjhPq3-cXbKDO1w.woff2) format('woff2');
71 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
72 | }
73 | /* greek-ext */
74 | @font-face {
75 | font-family: 'Roboto Mono';
76 | font-style: normal;
77 | font-weight: 400;
78 | src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://fonts.gstatic.com/s/robotomono/v5/L0x5DF4xlVMF-BfR8bXMIjhHq3-cXbKDO1w.woff2) format('woff2');
79 | unicode-range: U+1F00-1FFF;
80 | }
81 | /* greek */
82 | @font-face {
83 | font-family: 'Roboto Mono';
84 | font-style: normal;
85 | font-weight: 400;
86 | src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://fonts.gstatic.com/s/robotomono/v5/L0x5DF4xlVMF-BfR8bXMIjhIq3-cXbKDO1w.woff2) format('woff2');
87 | unicode-range: U+0370-03FF;
88 | }
89 | /* vietnamese */
90 | @font-face {
91 | font-family: 'Roboto Mono';
92 | font-style: normal;
93 | font-weight: 400;
94 | src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://fonts.gstatic.com/s/robotomono/v5/L0x5DF4xlVMF-BfR8bXMIjhEq3-cXbKDO1w.woff2) format('woff2');
95 | unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
96 | }
97 | /* latin-ext */
98 | @font-face {
99 | font-family: 'Roboto Mono';
100 | font-style: normal;
101 | font-weight: 400;
102 | src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://fonts.gstatic.com/s/robotomono/v5/L0x5DF4xlVMF-BfR8bXMIjhFq3-cXbKDO1w.woff2) format('woff2');
103 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
104 | }
105 | /* latin */
106 | @font-face {
107 | font-family: 'Roboto Mono';
108 | font-style: normal;
109 | font-weight: 400;
110 | src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://fonts.gstatic.com/s/robotomono/v5/L0x5DF4xlVMF-BfR8bXMIjhLq3-cXbKD.woff2) format('woff2');
111 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
112 | }
113 |
--------------------------------------------------------------------------------
/setup.js:
--------------------------------------------------------------------------------
1 | /*jshint esversion: 6 */
2 | const { prompt } = require('inquirer');
3 | const chalk = require('chalk');
4 | const os = require('os');
5 | const log = console.log;
6 | const fs = require('fs-extra');
7 | const git = require('simple-git/promise');
8 | const home = os.homedir();
9 | const { spawn } = require('child_process');
10 | const { sendEvent, LibrarianEvents } = require('./analytics.js');
11 |
12 | const configurationKey = 'librarian_config';
13 | const librarianWebRepo = 'https://github.com/biocross/Librarian-Web.git';
14 |
15 | const existingConfigurationConfirmation = [
16 | {
17 | type: 'confirm',
18 | name: 'existing_configuration',
19 | message: 'Librarian has already been configured on this system. Do you want to reconfigure?',
20 | default: false
21 | }
22 | ];
23 |
24 | const setupQuestions = [
25 | {
26 | type: 'input',
27 | name: 'working_directory',
28 | message: 'Where should Librarian store builds & it\'s website? Press enter for default:',
29 | default: `${home}/librarian/`
30 | },
31 | {
32 | type: 'input',
33 | name: 'local_ip',
34 | message: 'What is the local IP Librarian Website should be running at? (Enter for autodetected default)',
35 | default: os.networkInterfaces().en0.find(elm => elm.family == 'IPv4').address
36 | },
37 | {
38 | type: 'confirm',
39 | name: 'assets_web',
40 | message: 'Should Librarian\'s web interface be accessible over the internet?'
41 | },
42 | {
43 | type: 'input',
44 | name: 'jekyll_port',
45 | message: 'Which port should the Librarian Website run at? (Default: 5000)',
46 | default: '5000'
47 | },
48 | {
49 | type: 'input',
50 | name: 'assets_port',
51 | message: 'Which port should the Librarian Assets Server run at? (Default: 5001)',
52 | default: '5001',
53 | when: (answers) => { return answers.assets_web === false; }
54 | },
55 | {
56 | type: 'confirm',
57 | name: 'existing_token',
58 | message: 'Do you want to you use a custom ngrok token? [This is required if you want to password protect Librarian\'s web interface]\n\n Press n if you\'re unsure (Know More: github.com/biocross/Librarian/wiki/Custom-ngrok-Tokens) ',
59 | default: false
60 | },
61 | {
62 | type: 'input',
63 | name: 'ngrok_token',
64 | message: 'Please enter your ngRok token:',
65 | when: (answers) => { return answers.existing_token === true; }
66 | },
67 | {
68 | type: 'confirm',
69 | name: 'private_web',
70 | message: 'Do you want Librarian\'s Website to be password protected (when accessed over the internet)?',
71 | default: false,
72 | when: (answers) => { return answers.existing_token === true; }
73 | },
74 | {
75 | type: 'input',
76 | name: 'web_username',
77 | message: 'Please enter the username for the web interface:',
78 | when: (answers) => { return answers.existing_token === true && answers.private_web === true; }
79 | },
80 | {
81 | type: 'input',
82 | name: 'web_password',
83 | message: 'Please enter the password for the web interface:',
84 | when: (answers) => { return answers.existing_token === true && answers.private_web === true; }
85 | }
86 | ];
87 |
88 | const beginSetup = async (preferences) => {
89 | sendEvent(LibrarianEvents.SetupStarted);
90 | const configuration = await prompt(setupQuestions);
91 |
92 | if (configuration.local_ip.indexOf('http') == -1) {
93 | configuration.local_ip = 'http://' + configuration.local_ip + ':' + configuration.jekyll_port;
94 | configuration.assets_web = !configuration.assets_web;
95 | }
96 |
97 | console.log(chalk.green('\nUsing Configuration: \n'));
98 | console.log(configuration);
99 |
100 | console.log(chalk.green('\nCloning the Librarian WebServer...'));
101 | const localPath = `${configuration.working_directory}/web`;
102 | const assetServerPath = `${configuration.working_directory}/asset_server`;
103 | await git(configuration.working_directory).clone(librarianWebRepo, localPath, ['--depth', 1]);
104 | console.log(chalk.green('Cloning Complete!'));
105 |
106 | if (configuration.assets_web) {
107 | console.log(chalk.green('\nCloning the Librarian Assets Server...'));
108 | await git(configuration.working_directory).clone(librarianWebRepo, assetServerPath, ['--depth', 1, '-b', 'asset_server']);
109 | console.log(chalk.green('Cloning Complete!'));
110 | }
111 |
112 | console.log(chalk.green('\nInstalling required ruby gems...'));
113 | const bundler = spawn('bundle install --path ./localgems', {
114 | shell: true,
115 | cwd: localPath
116 | });
117 |
118 | bundler.stdout.on('data', (data) => {
119 | if (String(data).indexOf('Bundle complete') > -1) {
120 | if (configuration.assets_web) {
121 | const assets_bundler = spawn('bundle install --path ./localgems', {
122 | shell: true,
123 | cwd: assetServerPath
124 | });
125 | assets_bundler.stdout.on('data', (data) => {
126 | if (String(data).indexOf('Bundle complete') > -1) {
127 | log(chalk.green('Installation Complete!'));
128 | log(chalk.bold('\nAll set! Run Librarian using: ') + chalk.yellow.bold('librarian start'));
129 | }
130 | if (String(data).toLowerCase().indexOf('error') > -1) {
131 | sendEvent(LibrarianEvents.SetupError);
132 | log(String(data));
133 | }
134 | });
135 | } else {
136 | sendEvent(LibrarianEvents.SetupComplete);
137 | log(chalk.green('Installation Complete!'));
138 | log(chalk.bold('\nAll set! Run Librarian using: ') + chalk.yellow.bold('librarian start'));
139 | }
140 | }
141 | if (String(data).toLowerCase().indexOf('error') > -1) {
142 | log(String(data));
143 | }
144 | });
145 |
146 | bundler.on('exit', function (code, signal) {
147 | if(code != 0) { sendEvent(LibrarianEvents.SetupError); }
148 | if (code == 127) {
149 | fatalError('Librarian requires bundler to work. Please install bundler by running ' + chalk.bold.yellow('gem install bundler') + ' and run librarian setup again.')
150 | }
151 | });
152 |
153 | await preferences.setItem(configurationKey, configuration);
154 | }
155 |
156 | const purgeExistingInstallation = async (preferences) => {
157 | const prefs = await preferences.getItem(configurationKey);
158 | console.log("Purging the Existing Installation at: " + prefs.working_directory);
159 | await fs.emptyDir(prefs.working_directory);
160 | await fs.removeSync(prefs.working_directory)
161 | console.log("Purge Complete!\n");
162 | }
163 |
164 | const isSetup = async (preferences) => {
165 | const isSetup = await preferences.getItem(configurationKey);
166 | return isSetup !== undefined;
167 | };
168 |
169 | const shouldOverwriteConfiguration = async () => {
170 | const answer = await prompt(existingConfigurationConfirmation);
171 | return answer.existing_configuration == true;
172 | };
173 |
174 | const fatalError = (message) => {
175 | log(chalk.red.bold('🚨 Error: ' + message + ' 🚨'));
176 | process.exit(1);
177 | };
178 |
179 | module.exports = { beginSetup, isSetup, shouldOverwriteConfiguration, purgeExistingInstallation, configurationKey };
--------------------------------------------------------------------------------
/librarian.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /*jshint esversion: 6 */
3 | const program = require('commander');
4 | const ngrok = require('ngrok');
5 | const chalk = require('chalk');
6 | const preferences = require('node-persist');
7 | const os = require('os');
8 | const fs = require('fs-extra');
9 | const plist = require('plist');
10 | const qrcode = require('qrcode-terminal');
11 | const log = console.log;
12 | const home = os.homedir();
13 | const updateNotifier = require('update-notifier');
14 | const pkg = require('./package.json');
15 | const gitP = require('simple-git/promise');
16 | const git = gitP();
17 | const { Extract } = require('app-metadata');
18 | const { spawn } = require('child_process');
19 | const { sendEvent, LibrarianEvents } = require('./analytics.js');
20 | const { beginSetup, isSetup, shouldOverwriteConfiguration, purgeExistingInstallation, configurationKey } = require('./setup.js');
21 | const { setWebConfiguration, addBuild } = require('./webBridge.js');
22 | const storageOptions = {
23 | dir: `${home}/librarian/configuration`,
24 | stringify: JSON.stringify,
25 | parse: JSON.parse,
26 | encoding: 'utf8',
27 | forgiveParseErrors: true
28 | };
29 | const JEYLL_FRONT_MATTER_CHARACTER = "---\n---\n\n";
30 | const noUpdateConfiguration = {
31 | "update": {
32 | "available": false,
33 | "notes": ""
34 | }
35 | };
36 |
37 | program
38 | .version(pkg.version)
39 | .description('Librarian is a local server for your iOS & Android builds, cause local is best!')
40 |
41 | program
42 | .command('setup')
43 | .alias('s')
44 | .description('Setup Librarian to Run on your machine')
45 | .action(async () => {
46 | printHeader('Welcome to Librarian!');
47 | await preferences.init(storageOptions);
48 |
49 | if (await isSetup(preferences)) {
50 | if (await shouldOverwriteConfiguration()) {
51 | await purgeExistingInstallation(preferences);
52 | await preferences.init(storageOptions);
53 | await beginSetup(preferences);
54 | }
55 | } else {
56 | await beginSetup(preferences);
57 | }
58 |
59 | await checkForUpdate(preferences);
60 | });
61 |
62 | program
63 | .command('start')
64 | .alias('st')
65 | .description('Start the Librarian Server')
66 | .action(async () => {
67 | await preferences.init(storageOptions);
68 |
69 | if (!await isSetup(preferences)) {
70 | fatalError('Librarian has not been setup yet! Run ' + chalk.yellow('librarian setup') + ' to begin')
71 | }
72 |
73 | sendEvent(LibrarianEvents.ServerStarted);
74 |
75 | printHeader('Starting Librarian...');
76 |
77 | const prefs = await preferences.getItem(configurationKey);
78 | const webPath = prefs.working_directory + 'web';
79 | const webPort = prefs.jekyll_port;
80 | const webCommand = `JEKYLL_ENV=production bundle exec jekyll serve --port ${webPort}`;
81 |
82 | // Start the Jekyll Web Server
83 | const web = spawn(webCommand, {
84 | shell: true,
85 | cwd: webPath
86 | });
87 |
88 | web.stdout.on('data', (data) => {
89 | if (String(data).indexOf('Server address:') > -1) {
90 | log('Jekyll Server Started');
91 | }
92 | if (String(data).toLowerCase().indexOf('error') > -1) {
93 | log(String(data));
94 | }
95 | });
96 |
97 | web.stderr.on('data', (data) => {
98 | log('Error:');
99 | log(String(data));
100 | });
101 |
102 | web.on('exit', function (code, signal) {
103 | if(code != 0) { sendEvent(LibrarianEvents.ServerError); }
104 | if(code == 1) { fatalError("Do you have another instance of Librarian running?") }
105 | fatalError('The Jekyll Server has quit unexpectedly. Librarian is now exiting.');
106 | });
107 |
108 | if (prefs.assets_web) {
109 | const assetsPath = prefs.working_directory + 'asset_server';
110 | const assetsPort = prefs.assets_port;
111 | const webCommand = `JEKYLL_ENV=production bundle exec jekyll serve --port ${assetsPort}`;
112 |
113 | const asset_server = spawn(webCommand, {
114 | shell: true,
115 | cwd: assetsPath
116 | });
117 |
118 | asset_server.stdout.on('data', (data) => {
119 | if (String(data).indexOf('Server address:') > -1) {
120 | log('Assets Server Started');
121 | }
122 | if (String(data).toLowerCase().indexOf('error') > -1) {
123 | log(String(data));
124 | }
125 | });
126 |
127 | asset_server.stderr.on('data', (data) => {
128 | log('Error:');
129 | log(String(data));
130 | });
131 |
132 | asset_server.on('exit', function (code, signal) {
133 | if(code == 1) { fatalError("Do you have another instance of Librarian running?") }
134 | if(code != 0) { sendEvent(LibrarianEvents.ServerError); }
135 | fatalError('The Assets Server has quit unexpectedly. Librarian is now exiting.');
136 | });
137 | }
138 |
139 | // Start the ngrok tunnel to the webserver
140 | let tunnelURL;
141 |
142 | try {
143 | const port = prefs.assets_web ? prefs.assets_port : prefs.jekyll_port;
144 | let options = { addr: port, region: 'ap' };
145 |
146 | if (prefs.ngrok_token && prefs.ngrok_token !== "") {
147 | options.authtoken = prefs.ngrok_token;
148 | }
149 |
150 | if (prefs.private_web) {
151 | options.auth = `${prefs.web_username}:${prefs.web_password}`
152 | }
153 |
154 | tunnelURL = await ngrok.connect(options);
155 |
156 | } catch (error) {
157 | sendEvent(LibrarianEvents.ServerError);
158 | log(JSON.stringify(error));
159 | fatalError("\nFailed to start the ngrok tunnel.\nPlease make sure your ngRok token is valid.");
160 | }
161 |
162 | if (tunnelURL == undefined || tunnelURL === '') {
163 | fatalError('Failed to start the ngrok tunnel.')
164 | }
165 |
166 | prefs.currentURL = tunnelURL;
167 |
168 | const currentIP = os.networkInterfaces().en0.find(elm => elm.family == 'IPv4').address;
169 | if (currentIP !== prefs.local_ip) {
170 | prefs.local_ip = 'http://' + currentIP + ':' + prefs.jekyll_port;
171 | }
172 |
173 | await preferences.setItem(configurationKey, prefs);
174 |
175 | let webConfiguration = {
176 | "webBaseURL": prefs.currentURL,
177 | "localBaseURL": prefs.local_ip
178 | };
179 | await setWebConfiguration(preferences, webConfiguration);
180 |
181 | const webURL = prefs.assets_web ? prefs.local_ip : tunnelURL;
182 |
183 | log('\nLibrarian is up at: ');
184 | log(chalk.yellow.bold(webURL));
185 |
186 | log('\nScan the QR code to jump to Librarian\'s web interface:');
187 | qrcode.generate(webURL);
188 |
189 | await checkForUpdate(preferences);
190 | });
191 |
192 |
193 | program
194 | .command('submit ')
195 | .alias('a')
196 | .option('-b, --branch ', 'The branch the build is from')
197 | .option('-n, --notes ', 'Release Notes for the build')
198 | .option('-p, --public', 'Allow the build to be downloaded via the Internet using Librarian\'s HTTPS Tunnel')
199 | .description('Submit a build to librarian')
200 | .action(async (pathToFile, options) => {
201 |
202 | sendEvent(LibrarianEvents.BuildSubmitted);
203 |
204 | await preferences.init(storageOptions);
205 |
206 | if (!await isSetup(preferences)) {
207 | fatalError('Librarian has not been setup yet! Run ' + chalk.yellow('librarian setup') + ' to begin')
208 | }
209 |
210 | const prefs = await preferences.getItem(configurationKey);
211 |
212 | if (prefs.currentURL === undefined) {
213 | fatalError("Please start the librarian server with " + chalk.yellow('librarian start') + " before trying to submit a build");
214 | }
215 |
216 | if (!fs.existsSync(pathToFile)) {
217 | fatalError('Couldn\'t find or access the file in the given path: ' + pathToFile);
218 | }
219 |
220 | const metadata = await Extract.run(pathToFile);
221 | const bundleIdentifier = metadata.uniqueIdentifier;
222 | const version = metadata.version;
223 | const build = metadata.buildVersion;
224 | const platform = metadata.deviceFamily.indexOf("Android") > -1 ? "android" : "ios";
225 | let buildInfo;
226 |
227 | if (platform == "ios") {
228 | const appName = metadata.displayName;
229 |
230 | if (bundleIdentifier === undefined || appName === undefined || version === undefined || build === undefined) {
231 | fatalError("The IPA is missing critical information.");
232 | }
233 |
234 | const buildTime = new Date();
235 | const folderName = buildTime.getTime();
236 | const templatePath = prefs.working_directory + 'web/templates/manifest.plist';
237 | const localManifestPath = prefs.working_directory + (prefs.assets_web ? 'asset_server' : 'web') + '/assets/b/' + folderName + '/local/manifest.plist';
238 | const webManifestPath = prefs.working_directory + 'web/assets/b/' + folderName + '/web/manifest.plist';
239 | const ipaPath = prefs.working_directory + 'web/assets/b/' + folderName + '/' + appName + '.ipa';
240 |
241 | try {
242 | fs.copySync(templatePath, localManifestPath);
243 | fs.copySync(pathToFile, ipaPath);
244 | const manifest = fs.readFileSync(localManifestPath, 'utf8');
245 | let editablePlist = plist.parse(manifest);
246 | editablePlist.items[0].metadata["bundle-version"] = version;
247 | editablePlist.items[0].metadata["bundle-identifier"] = bundleIdentifier;
248 | editablePlist.items[0].metadata["title"] = appName;
249 | editablePlist.items[0].assets[0].url = '{{site.data.config.localBaseURL}}/assets/b/' + folderName + '/' + appName + '.ipa';
250 | fs.writeFileSync(localManifestPath, JEYLL_FRONT_MATTER_CHARACTER + plist.build(editablePlist));
251 | if (options.public && !prefs.assets_web) {
252 | fs.copySync(templatePath, webManifestPath);
253 | editablePlist.items[0].assets[0].url = '{{site.data.config.webBaseURL}}/assets/b/' + folderName + '/' + appName + '.ipa';
254 | fs.writeFileSync(webManifestPath, JEYLL_FRONT_MATTER_CHARACTER + plist.build(editablePlist));
255 | }
256 | } catch (error) {
257 | fatalError(error);
258 | }
259 |
260 | buildInfo = {
261 | "version": version,
262 | "buildNumber": build,
263 | "bundle": bundleIdentifier,
264 | "folderPath": folderName,
265 | "date": buildTime.toISOString()
266 | };
267 | } else {
268 | const appName = metadata.originalFileName;
269 | const buildTime = new Date();
270 | const folderName = buildTime.getTime();
271 | const apkPath = prefs.working_directory + 'web/assets/b/' + folderName + '/' + appName;
272 |
273 | if (bundleIdentifier === undefined || appName === undefined || version === undefined || build === undefined) {
274 | fatalError("The APK is missing critical information.");
275 | }
276 |
277 | try {
278 | fs.copySync(pathToFile, apkPath);
279 | } catch (error) {
280 | fatalError(error);
281 | }
282 |
283 | buildInfo = {
284 | "version": version,
285 | "buildNumber": build,
286 | "bundle": bundleIdentifier,
287 | "folderPath": folderName,
288 | "fileName": appName,
289 | "date": buildTime.toISOString()
290 | };
291 | }
292 |
293 | buildInfo.notes = options.notes ? options.notes : "";
294 | buildInfo.branch = options.branch ? options.branch : "";
295 | buildInfo.public = options.public ? true : false;
296 | buildInfo.platform = platform;
297 |
298 | await addBuild(preferences, buildInfo);
299 | printHeader("Build Added Successfully!")
300 | await checkForUpdate(preferences);
301 | process.exit(0);
302 | });
303 |
304 | program
305 | .command('update')
306 | .description('Update Librarian to be the latest and greatest!')
307 | .action(async () => {
308 | printHeader('Updating Librarian...');
309 | await preferences.init(storageOptions);
310 |
311 | if (!await isSetup(preferences)) {
312 | fatalError('Librarian has not been setup yet! Run ' + chalk.yellow('librarian setup') + ' to begin')
313 | }
314 |
315 | const configuration = await preferences.getItem(configurationKey);
316 |
317 | const localPath = `${configuration.working_directory}web`;
318 | const assetServerPath = `${configuration.working_directory}/asset_server`;
319 |
320 | try {
321 | await updateServer(localPath);
322 | if (configuration.assets_web) {
323 | await updateServer(assetServerPath);
324 | }
325 |
326 | await setWebConfiguration(preferences, noUpdateConfiguration);
327 |
328 | log(chalk.bold("Update Complete!"));
329 | log(chalk.bold('\nAll set! Run Librarian using: ') + chalk.yellow.bold('librarian start'));
330 | } catch (error) {
331 | log(error);
332 | log("Failed to update");
333 | }
334 | });
335 |
336 | const updateServer = async (path) => {
337 | return new Promise(async (resolve, reject) => {
338 | git.cwd(path).then(() => git.add('./*')).then(() => git.commit(`Snapshot before Librarian Update at ${new Date()}`)).then(() => {
339 | console.log(`Updating Librarian Web Server at ${path}...`)
340 | git.pull((err, update) => {
341 | if (update && update.summary.changes) {
342 | console.log(update.summary.changes);
343 | }
344 | }).then(async () => {
345 | try {
346 | console.log(`Updating bundle for ${path}`);
347 | await installBundle(path);
348 | resolve(true);
349 | } catch (error) {
350 | reject(error);
351 | }
352 | })
353 | });
354 | })
355 | }
356 |
357 | const installBundle = async (path) => {
358 | return new Promise(async (resolve, reject) => {
359 | const bundler = spawn('bundle install --path ./localgems', {
360 | shell: true,
361 | cwd: path
362 | });
363 |
364 | bundler.stdout.on('data', (data) => {
365 | if (String(data).toLowerCase().indexOf('error') > -1) {
366 | log(String(data));
367 | }
368 | });
369 |
370 | bundler.on('exit', function (code, signal) {
371 | if (code === 0) {
372 | log(chalk.green('Bundle Installation Complete!'));
373 | resolve(true);
374 | return;
375 | }
376 |
377 | if (code == 127) {
378 | console.log('Librarian requires bundler to work. Please install bundler by running ' + chalk.bold.yellow('gem install bundler') + ' and run librarian setup again.');
379 | reject(false);
380 | } else {
381 | reject(false)
382 | }
383 | });
384 | });
385 | }
386 |
387 | const checkForUpdate = async (preferences) => {
388 | const notifier = updateNotifier({ pkg });
389 | notifier.notify();
390 | if (notifier.update) {
391 | const configuration = {
392 | "update": {
393 | "available": true,
394 | "notes": `An Update to Librarian is available! The new version is ${notifier.update.latest} (You have ${notifier.update.current})`
395 | }
396 | }
397 | await setWebConfiguration(preferences, configuration);
398 | } else {
399 | await setWebConfiguration(preferences, noUpdateConfiguration);
400 | }
401 | }
402 |
403 | const printHeader = (message) => {
404 | log('---------------------');
405 | log(chalk.black.bgCyan.bold(message));
406 | log('---------------------');
407 | };
408 |
409 | const fatalError = (message) => {
410 | log(chalk.red.bold('🚨 Error: ' + message + ' 🚨'));
411 | process.exit(1);
412 | };
413 |
414 | program.parse(process.argv);
415 |
416 | process.on('SIGINT', async function () {
417 | log("\nExiting...")
418 | await preferences.init(storageOptions);
419 | const prefs = await preferences.getItem(configurationKey);
420 | prefs.currentURL = undefined;
421 | await preferences.setItem(configurationKey, prefs);
422 | printHeader("Thanks for using Librarian!");
423 | process.exit(0);
424 | });
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Librarian – Easily host your iOS and Android builds locally!
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
110 |
112 |
162 |
163 |
169 |
170 |
171 |
172 |
182 |
183 |
184 |
185 |
Managing app builds has never been easier.
186 |
Serve iOS & Android Builds on your local network, make dogfooding effortless.
187 |
195 |
196 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
Quick Setup
213 |
Open Terminal and run:
214 |
240 |
241 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
Locally Hosted
257 |
Never worry about third party services disrupting your release schedule.
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
Public URLs
266 |
Accessible by your entire team through localhost tunneling.
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
iOS & Android Support
275 |
Cross platform support makes it a great choice for all teams.
276 |
277 |
278 |
279 |
280 |
292 |
293 |
--------------------------------------------------------------------------------
/docs/AllTemplates.min.css:
--------------------------------------------------------------------------------
1 | .font-elegant *{font-family:Montserrat,sans-serif}.font-elegant .bold,.font-elegant .heading,.font-elegant .heading-lrg,.font-elegant .heading-sml{font-family:Spectral,serif}.font-friendly *{font-family:Montserrat,sans-serif}.font-hacker *{font-family:'Roboto Mono',monospace}.font-hacker .heading,.font-hacker .heading-lrg,.font-hacker .heading-sml{font-family:Roboto,sans-serif}.font-modern *{font-family:Poppins,sans-serif}.font-modern .heading,.font-modern .heading-lrg,.font-modern .heading-sml{font-weight:300}.font-neutral *{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"}.font-serious *{font-family:Montserrat,sans-serif}.font-serious .bold,.font-serious .heading,.font-serious .heading-lrg,.font-serious .heading-sml{font-family:Eczar,serif}*,:after,:before{margin:0;padding:0;box-sizing:border-box;font-smoothing:antialiased;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}.header{padding-top:40px}.logo{font-size:calc(120% + 1vw);line-height:44px;font-weight:700}.nav-link{font-size:19px;line-height:44px;text-decoration:none;color:inherit;display:inline-block;margin:0 15px}.logo img{height:44px;display:inherit}.heading{font-size:42px;line-height:1.5}.heading-sml{font-size:36px;line-height:1.38;font-weight:700}.heading-lrg{font-size:56px;line-height:1.38}.subheading{font-size:22px;line-height:1.8;font-weight:400}.paragraph{font-size:19px;line-height:1.8;font-weight:400}a{color:inherit;text-decoration:none}.span{font-size:15px;line-height:1.5}.bold{font-size:22px;line-height:1.5}.text-center{text-align:center}img{max-width:100%}.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9{padding:0 20px}.col-1{flex-basis:8.333333%;max-width:8.333333%}.col-2{flex-basis:16.666667%;max-width:16.666667%}.col-3{flex-basis:25%;max-width:25%}.col-4{flex-basis:33.333333%;max-width:33.333333%}.col-5{flex-basis:41.666667%;max-width:41.666667%}.col-6{flex-basis:50%;max-width:50%}.col-7{flex-basis:58.333333%;max-width:58.333333%}.col-8{flex-basis:66.666667%;max-width:66.666667%}.col-9{flex-basis:75%;max-width:75%}.col-10{flex-basis:83.333333%;max-width:83.333333%}.col-11{flex-basis:91.666667%;max-width:91.666667%}.col-12{flex-basis:100%;max-width:100%}.flex{display:flex}.flex-column{flex-direction:column}.spread{justify-content:space-between}.center-horizontal{align-items:center}.center-vertical{justify-content:center}.start{justify-content:flex-start}.end{justify-content:flex-end}.noshrink{flex-shrink:0}.wrap{flex-flow:wrap}.container,.container-lrg,.container-sml{margin:auto;position:relative;width:100%}.container-lrg{max-width:1020px}.container{max-width:780px}.container-sml{max-width:470px}.section{padding: 100px 0;overflow:hidden;position:relative;}.button{display:inline-block;padding:15px 20px;border-radius:3px;font-size:15px;box-shadow:0 4px 6px rgba(50,50,93,.11),0 1px 3px rgba(0,0,0,.08);text-decoration:none;background:#fff;color:#000;white-space:nowrap;border:none;-webkit-appearance:none;cursor:pointer}.button__full{width:100%;margin:0;text-align:center}.no-max-width{max-width:initial!important}.button.accent-bg span,.white-color{color:#fff!important}.button span,.button svg{display:inline-block;vertical-align:middle}.button svg{height:18px;max-width:21px;margin-right:5px}.mask{overflow:hidden;position:relative;background:#fff}.mailchimp{border:15px solid #fff;border-radius:3px;position:relative;background:#fff;z-index:2;box-shadow:0 4px 6px rgba(50,50,93,.11),0 1px 3px rgba(0,0,0,.08);margin:auto}.mailchimp-input{font-size:15px;outline:0;border:none;width:100%;padding:15px}.fill-white{fill:#fff}.user-image{width:64px;height:64px;border-radius:50%;display:inline-block;background-position:center;background-size:cover}.socialicons{width:30px;height:30px;border-radius:50%;display:inline-block;position:relative}.socialicons:not(:last-of-type){margin-right:10px}.mb10:first-of-type,.mb10:not(:last-of-type){margin-bottom:10px}.mb20:first-of-type,.mb20:not(:last-of-type){margin-bottom:20px}.mb35:first-of-type,.mb35:not(:last-of-type){margin-bottom:35px}.mb40:first-of-type,.mb40:not(:last-of-type){margin-bottom:40px}.mb50:first-of-type,.mb50:not(:last-of-type){margin-bottom:50px}.mb75:first-of-type,.mb75:not(:last-of-type){margin-bottom:75px}.m0{margin:0}.mt10{margin-top:10px}.mt20{margin-top:20px}.mt30{margin-top:30px}.mt40{margin-top:40px}.mt50{margin-top:50px}.mt75{margin-top:75px}.mr0{margin-right:0}.mr10{margin-right:10px}.mr20{margin-right:20px}.pl0{padding-left:0}.pr0{padding-right:0}.pad10{padding:10px}.pad20{padding:20px}.pad30{padding:30px}@media screen and (max-width:820px){.container-lrg.flex,.container-sml.flex,.container.flex,.mobile-flex-wrap{flex-flow:wrap}.col-1,.col-10,.col-11,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9{flex-basis:100%;max-width:100%;margin-bottom:20px}.mobile-col-6{flex-basis:50%;max-width:50%}.heading-sml{font-size:calc(110% + 2vw)}.heading{font-size:calc(107.5% + 3.7vw)}.heading-lrg{font-size:calc(105% + 5.3vw)}.section{padding:60px 0}.mobile-text-center{text-align:center}.mobile-center-icon{margin:0 auto 20px}.card>div:not(:first-of-type){border-top:1px solid rgba(0,0,0,.1);border-left:none}.mailchimp .button{width:100%}}@media screen and (max-width:460px){.button{max-width:280px;width:100%;margin-right:0!important}}.logo__white{color:#fff!important}.logo__black{color:#000!important}.button__black,.button__black.accent-bg span{color:#000!important}.button__black svg{fill:#000!important}.button__white,.button__white.accent-bg span{color:#fff!important}.button__white{fill:#fff!important}.email,.facebook,.github,.instagram,.linkedin,.medium,.twitter,.youtube{position:relative}.email:after,.facebook:after,.github:after,.instagram:after,.linkedin:after,.medium:after,.twitter:after,.youtube:after{content:"";position:absolute;width:100%;height:100%;display:block;background-position:center;background-size:16px auto;background-repeat:no-repeat;top:0;left:0}.twitter:after{background-image:url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjZmZmIiB3aWR0aD0iMTIyIiBoZWlnaHQ9Ijk5IiB2aWV3Qm94PSIwIDAgMTIyIDk5IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xMjEuNTY4IDExLjY5NWE0OS44NjkgNDkuODY5IDAgMCAxLTE0LjMyNSAzLjkyOGM1LjE1LTMuMDg3IDkuMTA0LTcuOTc1IDEwLjk2Ny0xMy43OTlhNDkuOTQ5IDQ5Ljk0OSAwIDAgMS0xNS44MzggNi4wNTJBMjQuOTEgMjQuOTEgMCAwIDAgODQuMTY3IDBDNzAuMzkyIDAgNTkuMjI1IDExLjE2NyA1OS4yMjUgMjQuOTRjMCAxLjk1NS4yMiAzLjg1OS42NDYgNS42ODQtMjAuNzI5LTEuMDQtMzkuMTA3LTEwLjk3LTUxLjQwOC0yNi4wNTlhMjQuODI0IDI0LjgyNCAwIDAgMC0zLjM3NyAxMi41NGMwIDguNjUyIDQuNDA0IDE2LjI4NiAxMS4wOTYgMjAuNzU5YTI0Ljg0MSAyNC44NDEgMCAwIDEtMTEuMjk3LTMuMTJjLS4wMDIuMTA0LS4wMDIuMjA5LS4wMDIuMzE0IDAgMTIuMDg0IDguNTk3IDIyLjE2NSAyMC4wMDcgMjQuNDU2YTI0Ljk3NSAyNC45NzUgMCAwIDEtMTEuMjY0LjQyN2MzLjE3NCA5LjkwOSAxMi4zODUgMTcuMTIgMjMuMjk5IDE3LjMyMUE1MC4wNDQgNTAuMDQ0IDAgMCAxIDUuOTUgODcuOTRjLTIuMDEzIDAtMy45OTktLjExOC01Ljk1LS4zNDggMTEuMDM4IDcuMDc2IDI0LjE0OCAxMS4yMDUgMzguMjMzIDExLjIwNSA0NS44NzUgMCA3MC45NjItMzguMDA1IDcwLjk2Mi03MC45NjNhNzEuOTIgNzEuOTIgMCAwIDAtLjA3Mi0zLjIyNyA1MC42OTIgNTAuNjkyIDAgMCAwIDEyLjQ0NS0xMi45MSIvPjwvc3ZnPg==)}.medium:after{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAyMCAxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTguMTA1IDE0LjM1N2wxLjQ4MiAxLjQxNXYuMDQ3SDEyLjA3di0uMDQ3bDEuNTA4LTEuNDE1YS41NjguNTY4IDAgMCAwIC4yMDEtLjU0M1Y0Ljc2N2MwLS4zODIgMC0uODk4LjA2Ny0xLjM0TDguNzc3IDE2aC0uMDZMMy40NjcgNC40MzJjLS4xMTQtLjI4OC0uMTQ3LS4zMDgtLjIyMi0uNTAzdjcuNTg1YTIuNjM2IDIuNjM2IDAgMCAwIC4yNDggMS40ODJsMi4xMTMgMi43NzV2LjA0OEgwdi0uMDQ3bDIuMTEyLTIuNzgzYy4yMjMtLjQ1OC4zMS0uOTcuMjQ5LTEuNDc1VjMuMTMyYTEuNjIzIDEuNjIzIDAgMCAwLS4zNS0xLjE0N0wuNTE2LjA0N1YwaDUuMzY1bDQuNDkzIDkuNzk3TDE0LjMzIDBoNS4yNTF2LjA0N0wxOC4xMDUgMS42OWEuNTc4LjU3OCAwIDAgMC0uMjI4LjU2M3YxMS41NmEuNTQxLjU0MSAwIDAgMCAuMjI4LjU0NHoiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI0ZGRiIvPjwvc3ZnPg==)}.facebook:after{background-image:url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjZmZmIiB3aWR0aD0iNTIiIGhlaWdodD0iMTAwIiB2aWV3Qm94PSIwIDAgNTIgMTAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0zMy42NjcgOTkuNDlWNTQuMTA4SDQ4Ljk2bDIuMjktMTcuNjg2SDMzLjY2NlYyNS4xM2MwLTUuMTIgMS40MjgtOC42MSA4LjgtOC42MWw5LjQwMi0uMDA1Vi42OTdDNTAuMjQyLjQ4MiA0NC42NiAwIDM4LjE2OSAwIDI0LjYxMSAwIDE1LjMzMSA4LjI0MiAxNS4zMzEgMjMuMzc5djEzLjA0M0gwdjE3LjY4NmgxNS4zMzJWOTkuNDloMTguMzM1Ii8+PC9zdmc+);background-size:auto 16px}.github:after{background-image:url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjZmZmIiB3aWR0aD0iMTA1IiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwNSAxMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTYzLjg3OSA5Ni42MzZWODIuNjhjMC0zLjQ5LS45OTctNi40OC0zLjQ4OS04Ljk3Mi45OTcgMCAzLjQ4OS0uNDk4IDYuNDgtLjk5NyA2Ljk3Ny0xLjQ5NSAxMS45Ni00Ljk4NCAxNC40NTMtMTAuNDY2IDIuOTktNS45OCAzLjQ4OS0xMi40NiAxLjk5NC0xOC45NGExNi42MzMgMTYuNjMzIDAgMCAwLTIuNDkzLTUuOThjLTEuNDk1LTEuNDk1LTEuOTkzLTIuNDkyLTEuNDk1LTIuOTkuOTk3LTMuNDkuOTk3LTcuNDc3LS40OTgtMTEuOTYyLS40OTktLjk5Ny0uOTk3LS45OTctMS40OTUtLjk5Ny0yLjk5IDAtNy40NzYgMS40OTUtMTIuNDYgNC40ODYtLjk5Ny40OTgtMS40OTYuNDk4LTEuNDk2LjQ5OGE1MC42MDkgNTAuNjA5IDAgMCAwLTI0LjQyMSAwYy0uNDk5IDAtLjk5NyAwLTEuNDk2LS40OTgtNC40ODUtMi40OTItNy45NzQtNC40ODYtMTAuOTY0LTQuNDg2LTEuNDk2LS40OTgtMS45OTQtLjQ5OC0yLjQ5MiAwLS40OTkuNDk4LS40OTkuOTk3LS45OTcgMS45OTQtLjk5NyAzLjk4Ny0uOTk3IDcuNDc2IDAgMTAuOTY0IDAgLjQ5OSAwIC45OTctLjQ5OS45OTctMy45ODcgNC45ODQtNS40ODIgMTAuOTY1LTQuNDg1IDE4LjQ0MSAxLjQ5NSA5Ljk2OCA2Ljk3NyAxNi40NDcgMTYuOTQ1IDE4Ljk0LjQ5OSAwIDIuNDkyLjQ5OCA2LjQ4IDEuNDk1LTEuNDk2IDEuNDk1LTIuNDkyIDMuNDg4LTIuOTkgNS45OCAwIC40OTktLjUuOTk3LS41Ljk5Ny02LjQ3OCAyLjQ5Mi0xMC45NjQuOTk3LTE0LjQ1My0zLjk4Ny0xLjk5My0yLjk5LTQuNDg1LTQuOTg0LTYuOTc3LTUuNDgyaC0yLjQ5MmMtLjk5NyAwLS45OTcuNDk4LS40OTkgMS40OTUuNDk5LjQ5OC45OTcuOTk3IDEuNDk1Ljk5NyAyLjQ5MiAxLjQ5NSA0LjQ4NiAzLjk4NyA1Ljk4MSA2Ljk3NyAxLjk5NCA0LjQ4NiA1LjQ4MyA2Ljk3OCAxMC45NjUgNy40NzYuNDk4IDAgMi45OSAwIDYuOTc4LS40OTh2OS40N2MwIC45OTYtLjQ5OSAxLjQ5NS0uOTk3IDEuOTkzLS45OTcuNDk5LTEuNDk1LjQ5OS0yLjQ5MiAwLTQuOTg0LTEuNDk1LTguOTcxLTMuNDg5LTEyLjQ2LTUuOThDNy4wNiA4Mi42OC0uOTE0IDY3LjcyNy4wODMgNDguNzg4Yy40OTktMTEuOTYyIDQuNDg2LTIxLjkzIDEyLjQ2LTMwLjlDMjAuNTE4IDguOTE2IDMwLjQ4NiAzLjQzMyA0My40NDQuOTQxYzEyLjk1OS0yLjQ5MiAyNC45MiAwIDM2LjM4NCA2Ljk3OCAxMS40NjMgNi45NzggMTkuNDM3IDE3LjQ0NCAyMi45MjYgMzAuNDAzIDMuNDg5IDEyLjk1OCAxLjk5NCAyNS40MTgtNC40ODUgMzcuMzgtNi40OCAxMS45NjItMTYuNDQ4IDE5LjkzNi0yOC45MDggMjMuOTIzLTMuOTg3LjQ5OS01LjQ4Mi0uNDk4LTUuNDgyLTIuOTl6Ii8+PC9zdmc+)}.instagram:after{background-image:url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjZmZmIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTUwIDBDMzYuNDIgMCAzNC43MTguMDU3IDI5LjM4NS4zYy01LjMyMi4yNDQtOC45NTcgMS4wOS0xMi4xMzcgMi4zMjUtMy4yODggMS4yNzgtNi4wNzYgMi45ODctOC44NTYgNS43NjctMi43OCAyLjc4LTQuNDkgNS41NjgtNS43NjcgOC44NTZDMS4zOSAyMC40MjguNTQ0IDI0LjA2My4zMDEgMjkuMzg1LjA1NyAzNC43MTggMCAzNi40MjEgMCA1MGMwIDEzLjU4LjA1NyAxNS4yODIuMyAyMC42MTUuMjQ0IDUuMzIyIDEuMDg5IDguOTU2IDIuMzI1IDEyLjEzNyAxLjI3OCAzLjI4OCAyLjk4NyA2LjA3NiA1Ljc2NyA4Ljg1NiAyLjc4IDIuNzggNS41NjggNC40OSA4Ljg1NiA1Ljc2NyAzLjE4IDEuMjM2IDYuODE1IDIuMDgxIDEyLjEzNyAyLjMyNCA1LjMzMy4yNDMgNy4wMzYuMzAxIDIwLjYxNS4zMDEgMTMuNTggMCAxNS4yODItLjA1OCAyMC42MTUtLjMgNS4zMjItLjI0NCA4Ljk1Ny0xLjA5IDEyLjEzNy0yLjMyNSAzLjI4OC0xLjI3OCA2LjA3Ni0yLjk4NyA4Ljg1Ni01Ljc2NyAyLjc4LTIuNzggNC40OS01LjU2OCA1Ljc2Ny04Ljg1NiAxLjIzNi0zLjE4IDIuMDgxLTYuODE1IDIuMzI0LTEyLjEzNy4yNDQtNS4zMzMuMzAxLTcuMDM2LjMwMS0yMC42MTUgMC0xMy41OC0uMDU3LTE1LjI4Mi0uMy0yMC42MTUtLjI0NC01LjMyMi0xLjA4OS04Ljk1Ni0yLjMyNS0xMi4xMzctMS4yNzgtMy4yODgtMi45ODctNi4wNzYtNS43NjctOC44NTYtMi43OC0yLjc4LTUuNTY4LTQuNDktOC44NTYtNS43NjdDNzkuNTcyIDEuMzg5IDc1LjkzNy41NDQgNzAuNjE1LjMgNjUuMjgyLjA1NyA2My41OCAwIDUwIDB6bTAgOS4wMDljMTMuMzUgMCAxNC45MzIuMDUgMjAuMjA0LjI5MSA0Ljg3NS4yMjMgNy41MjMgMS4wMzggOS4yODUgMS43MjIgMi4zMzQuOTA3IDQgMS45OSA1Ljc0OSAzLjc0IDEuNzUgMS43NSAyLjgzMyAzLjQxNSAzLjc0IDUuNzUuNjg0IDEuNzYxIDEuNSA0LjQwOSAxLjcyMiA5LjI4NC4yNCA1LjI3Mi4yOTEgNi44NTMuMjkxIDIwLjIwNCAwIDEzLjM1LS4wNSAxNC45MzItLjI5MSAyMC4yMDQtLjIyMyA0Ljg3NS0xLjAzOCA3LjUyMy0xLjcyMiA5LjI4NS0uOTA3IDIuMzMzLTEuOTkgMy45OTktMy43NCA1Ljc0OS0xLjc1IDEuNzUtMy40MTUgMi44MzMtNS43NSAzLjc0LTEuNzYxLjY4NC00LjQwOSAxLjUtOS4yODQgMS43MjEtNS4yNzEuMjQxLTYuODUyLjI5Mi0yMC4yMDQuMjkycy0xNC45MzMtLjA1LTIwLjIwNC0uMjkyYy00Ljg3NS0uMjIyLTcuNTIzLTEuMDM3LTkuMjg1LTEuNzIxLTIuMzMzLS45MDctNC0xLjk5LTUuNzQ5LTMuNzQtMS43NS0xLjc1LTIuODMzLTMuNDE2LTMuNzQtNS43NS0uNjg0LTEuNzYxLTEuNS00LjQwOS0xLjcyMi05LjI4NEM5LjA2IDY0LjkzMiA5LjAxIDYzLjM1MSA5LjAxIDUwYzAtMTMuMzUuMDUxLTE0LjkzMi4yOTEtMjAuMjA0LjIyMy00Ljg3NSAxLjAzOC03LjUyMyAxLjcyMi05LjI4NS45MDctMi4zMzQgMS45OS0zLjk5OSAzLjc0LTUuNzQ5IDEuNzUtMS43NSAzLjQxNi0yLjgzMyA1Ljc1LTMuNzQgMS43NjEtLjY4NCA0LjQwOS0xLjUgOS4yODQtMS43MjJDMzUuMDY4IDkuMDYgMzYuNjQ5IDkuMDEgNTAgOS4wMXptMCAxNS4zMTVDMzUuODIgMjQuMzI0IDI0LjMyNCAzNS44MiAyNC4zMjQgNTBjMCAxNC4xOCAxMS40OTYgMjUuNjc1IDI1LjY3NiAyNS42NzUgMTQuMTggMCAyNS42NzYtMTEuNDk1IDI1LjY3Ni0yNS42NzUgMC0xNC4xOC0xMS40OTYtMjUuNjc2LTI1LjY3Ni0yNS42NzZ6bTAgNDIuMzQzYy05LjIwNSAwLTE2LjY2Ny03LjQ2Mi0xNi42NjctMTYuNjY3IDAtOS4yMDUgNy40NjItMTYuNjY3IDE2LjY2Ny0xNi42NjcgOS4yMDUgMCAxNi42NjcgNy40NjIgMTYuNjY3IDE2LjY2NyAwIDkuMjA1LTcuNDYyIDE2LjY2Ny0xNi42NjcgMTYuNjY3ek04Mi42OSAyMy4zMWE2IDYgMCAxIDAtMTIgMCA2IDYgMCAwIDAgMTIgMHoiLz48L3N2Zz4=)}.linkedin:after{background-image:url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjZmZmIiB3aWR0aD0iOTkiIGhlaWdodD0iOTkiIHZpZXdCb3g9IjAgMCA5OSA5OSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMjIuNjE3IDk4LjkxNkgyLjIyNVYzMy4zOGgyMC4zOTJ2NjUuNTUtLjAxNHpNMTIuNDI0IDI0LjQwM0ExMS44MTMgMTEuODEzIDAgMSAxIDI0LjIzMSAxMi41OWExMS44MDYgMTEuODA2IDAgMCAxLTExLjgwNyAxMS44MTN6bTg2LjQ4NCA3NC41MTNINzguNTUxVjY3LjAzNGMwLTcuNjAzLS4xNTEtMTcuMzgzLTEwLjU5LTE3LjM4My0xMC41OTggMC0xMi4yMiA4LjI3Ni0xMi4yMiAxNi44MzNWOTguOTFIMzUuMzk5di02NS41M2gxOS41MjZ2OC45M2guMjgyYTIxLjQwOCAyMS40MDggMCAwIDEgMTkuMjc5LTEwLjU5MWMyMC42MDQgMCAyNC40MjMgMTMuNTcxIDI0LjQyMyAzMS4yM3YzNS45Njh6Ii8+PC9zdmc+)}.youtube:after{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjU2IiBoZWlnaHQ9IjE4MCIgdmlld0JveD0iMCAwIDI1NiAxODAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTI1My4zMDEgMzguOHMtMi40OTktMTcuNi0xMC4xOTYtMjUuNGMtOS42OTYtMTAuMi0yMC41OTItMTAuMi0yNS42OS0xMC44QzE4MS43MyAwIDEyOC4wNSAwIDEyOC4wNSAwaC0uMVM3NC4yNzEgMCAzOC4zODUgMi42Yy00Ljk5OC42LTE1Ljg5NC42LTI1LjY5IDEwLjhDNS4wOTggMjEuMiAyLjU5OSAzOC44IDIuNTk5IDM4LjhTMCA1OS42IDAgODAuM3YxOS40YzAgMjAuNyAyLjU5OSA0MS40IDIuNTk5IDQxLjRzMi40OTkgMTcuNiAxMC4xOTYgMjUuNGM5LjY5NiAxMC4yIDIyLjQ5MSA5LjkgMjguMTg5IDEwLjkgMjAuNDkyIDIgODYuOTY2IDIuNiA4Ni45NjYgMi42czUzLjc3OS0uMSA4OS41NjUtMi43YzQuOTk4LS42IDE1Ljg5NC0uNiAyNS42OS0xMC44IDcuNjk3LTcuOCAxMC4xOTYtMjUuNCAxMC4xOTYtMjUuNFMyNTYgMTIwLjQgMjU2IDk5LjdWODAuM2MtLjEtMjAuNy0yLjY5OS00MS41LTIuNjk5LTQxLjV6bS0xNTEuNzQgODQuNFY1MS4zbDY5LjE3MiAzNi4xLTY5LjE3MyAzNS44aC4wMDF6IiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNGRkYiLz48L3N2Zz4=)}.email:after{background-image:url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjZmZmIiB3aWR0aD0iMTI0IiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEyNCAxMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTExMC43ODQuOThIMTIuNzQ1QzYuMDA1Ljk4LjU1MSA2LjQ5NS41NTEgMTMuMjM1bC0uMDYgNzMuNTNjMCA2Ljc0IDUuNTE0IDEyLjI1NSAxMi4yNTQgMTIuMjU1aDk4LjA0YzYuNzQgMCAxMi4yNTQtNS41MTUgMTIuMjU0LTEyLjI1NXYtNzMuNTNjMC02Ljc0LTUuNTE0LTEyLjI1NS0xMi4yNTUtMTIuMjU1em0wIDI0LjUxbC00OS4wMiAzMC42MzdMMTIuNzQ2IDI1LjQ5VjEzLjIzNWw0OS4wMiAzMC42MzggNDkuMDItMzAuNjM4VjI1LjQ5eiIvPjwvc3ZnPg==)}.grey{background:#f5f5f5}.blue-flat{background-color:#0065ff}.blue-flat .secondary-bg{background-color:#041029}.blue-flat .accent-bg{background-color:#00cc47}.blue-flat .primary-color{color:#fff}.blue-flat .secondary-color{color:#d8e8ff}.blue-flat .svg-fill{fill:#fff}.blue-gradient{background:linear-gradient(#00c3ff,#0065ff)}.blue-gradient .secondary-bg{background-color:#041029}.blue-gradient .accent-bg{background-color:#00cc47}.blue-gradient .primary-color{color:#fff}.blue-gradient .secondary-color{color:#d8e8ff}.blue-gradient .svg-fill{fill:#fff}.blue-accent{background-color:#00cc47}.blue-accent .secondary-bg{background-color:#041029}.blue-accent .accent-bg{background-color:#0065ff}.blue-accent .primary-color{color:#fff}.blue-accent .secondary-color{color:#dcf8e6}.blue-accent .svg-fill{fill:#fff}.blue-black{background-color:#041029}.blue-black .secondary-bg{background-color:#0065ff}.blue-black .accent-bg{background-color:#00cc47}.blue-black .primary-color{color:#fff}.blue-black .secondary-color{color:#dadbdf}.blue-black .svg-fill{fill:#fff}.blue-white{background-color:#fff}.blue-white .secondary-bg{background-color:#d8e8ff}.blue-white .accent-bg{background-color:#00cc47}.blue-white .primary-color{color:#000}.blue-white .secondary-color{color:#8f8f8f}.blue-white .svg-fill{fill:#0065ff}.purple-flat{background-color:#b061d0}.purple-flat .secondary-bg{background-color:#252780}.purple-flat .accent-bg{background-color:#422db1}.purple-flat .primary-color{color:#fff}.purple-flat .secondary-color{color:#f3e6f8}.purple-flat .svg-fill{fill:#fff}.purple-gradient{background:linear-gradient(#252780,#b061d0)}.purple-gradient .secondary-bg{background-color:#252780}.purple-gradient .accent-bg{background-color:#422db1}.purple-gradient .primary-color{color:#fff}.purple-gradient .secondary-color{color:#f3e6f8}.purple-gradient .svg-fill{fill:#fff}.purple-accent{background-color:#422db1}.purple-accent .secondary-bg{background-color:#252780}.purple-accent .accent-bg{background-color:#b061d0}.purple-accent .primary-color{color:#fff}.purple-accent .secondary-color{color:#eceaf7}.purple-accent .svg-fill{fill:#fff}.purple-black{background-color:#252780}.purple-black .secondary-bg{background-color:#b061d0}.purple-black .accent-bg{background-color:#422db1}.purple-black .primary-color{color:#fff}.purple-black .secondary-color{color:#e4e4ef}.purple-black .svg-fill{fill:#fff}.purple-white{background-color:#fff}.purple-white .secondary-bg{background-color:#f3e6f8}.purple-white .accent-bg{background-color:#422db1}.purple-white .primary-color{color:#000}.purple-white .secondary-color{color:#8f8f8f}.purple-white .svg-fill{fill:#b061d0}.coral-flat{background-color:#fe7a6d}.coral-flat .secondary-bg{background-color:#ff4c68}.coral-flat .accent-bg{background-color:#391718}.coral-flat .primary-color{color:#fff}.coral-flat .secondary-color{color:#ffeae7}.coral-flat .svg-fill{fill:#fff}.coral-gradient{background:linear-gradient(#fe985f,#fe7a6d)}.coral-gradient .secondary-bg{background-color:#ff4c68}.coral-gradient .accent-bg{background-color:#391718}.coral-gradient .primary-color{color:#fff}.coral-gradient .secondary-color{color:#ffeae7}.coral-gradient .svg-fill{fill:#fff}.coral-accent{background-color:#391718}.coral-accent .secondary-bg{background-color:#ff4c68}.coral-accent .accent-bg{background-color:#fe7a6d}.coral-accent .primary-color{color:#fff}.coral-accent .secondary-color{color:#ebe7e7}.coral-accent .svg-fill{fill:#fff}.coral-black{background-color:#ff4c68}.coral-black .secondary-bg{background-color:#fe7a6d}.coral-black .accent-bg{background-color:#391718}.coral-black .primary-color{color:#fff}.coral-black .secondary-color{color:#ffe5e9}.coral-black .svg-fill{fill:#fff}.coral-white{background-color:#fff}.coral-white .secondary-bg{background-color:#ffeae7}.coral-white .accent-bg{background-color:#391718}.coral-white .primary-color{color:#000}.coral-white .secondary-color{color:#8f8f8f}.coral-white .svg-fill{fill:#fe7a6d}.ocher-flat{background-color:#ccaa3f}.ocher-flat .secondary-bg{background-color:#957a3e}.ocher-flat .accent-bg{background-color:#1a0f2f}.ocher-flat .primary-color{color:#fff}.ocher-flat .secondary-color{color:#f7f1df}.ocher-flat .svg-fill{fill:#fff}.ocher-gradient{background:linear-gradient(#867039,#ccaa3f)}.ocher-gradient .secondary-bg{background-color:#957a3e}.ocher-gradient .accent-bg{background-color:#1a0f2f}.ocher-gradient .primary-color{color:#fff}.ocher-gradient .secondary-color{color:#f7f1df}.ocher-gradient .svg-fill{fill:#fff}.ocher-accent{background-color:#1a0f2f}.ocher-accent .secondary-bg{background-color:#957a3e}.ocher-accent .accent-bg{background-color:#ccaa3f}.ocher-accent .primary-color{color:#fff}.ocher-accent .secondary-color{color:#e8e7ea}.ocher-accent .svg-fill{fill:#fff}.ocher-black{background-color:#957a3e}.ocher-black .secondary-bg{background-color:#ccaa3f}.ocher-black .accent-bg{background-color:#1a0f2f}.ocher-black .primary-color{color:#fff}.ocher-black .secondary-color{color:#f0ece3}.ocher-black .svg-fill{fill:#fff}.ocher-white{background-color:#fff}.ocher-white .secondary-bg{background-color:#f7f1df}.ocher-white .accent-bg{background-color:#1a0f2f}.ocher-white .primary-color{color:#000}.ocher-white .secondary-color{color:#8f8f8f}.ocher-white .svg-fill{fill:#ccaa3f}.fuschia-flat{background-color:#bb3385}.fuschia-flat .secondary-bg{background-color:#4831b8}.fuschia-flat .accent-bg{background-color:#56afd5}.fuschia-flat .primary-color{color:#fff}.fuschia-flat .secondary-color{color:#f3dcea}.fuschia-flat .svg-fill{fill:#fff}.fuschia-gradient{background:linear-gradient(#f93268,#4731b9)}.fuschia-gradient .secondary-bg{background-color:#4831b8}.fuschia-gradient .accent-bg{background-color:#56afd5}.fuschia-gradient .primary-color{color:#fff}.fuschia-gradient .secondary-color{color:#f3dcea}.fuschia-gradient .svg-fill{fill:#fff}.fuschia-accent{background-color:#56afd5}.fuschia-accent .secondary-bg{background-color:#4831b8}.fuschia-accent .accent-bg{background-color:#bb3385}.fuschia-accent .primary-color{color:#fff}.fuschia-accent .secondary-color{color:#e4f2f8}.fuschia-accent .svg-fill{fill:#fff}.fuschia-black{background-color:#4831b8}.fuschia-black .secondary-bg{background-color:#bb3385}.fuschia-black .accent-bg{background-color:#56afd5}.fuschia-black .primary-color{color:#fff}.fuschia-black .secondary-color{color:#e7e4f6}.fuschia-black .svg-fill{fill:#fff}.fuschia-white{background-color:#fff}.fuschia-white .secondary-bg{background-color:#f3dcea}.fuschia-white .accent-bg{background-color:#56afd5}.fuschia-white .primary-color{color:#000}.fuschia-white .secondary-color{color:#8f8f8f}.fuschia-white .svg-fill{fill:#bb3385}.pink-flat{background-color:#e5548b}.pink-flat .secondary-bg{background-color:#994e83}.pink-flat .accent-bg{background-color:#5c5fbe}.pink-flat .primary-color{color:#fff}.pink-flat .secondary-color{color:#fbe6ee}.pink-flat .svg-fill{fill:#fff}.pink-gradient{background:linear-gradient(#fe7a6d,#e5548b)}.pink-gradient .secondary-bg{background-color:#994e83}.pink-gradient .accent-bg{background-color:#5c5fbe}.pink-gradient .primary-color{color:#fff}.pink-gradient .secondary-color{color:#fbe6ee}.pink-gradient .svg-fill{fill:#fff}.pink-accent{background-color:#5c5fbe}.pink-accent .secondary-bg{background-color:#994e83}.pink-accent .accent-bg{background-color:#e5548b}.pink-accent .primary-color{color:#fff}.pink-accent .secondary-color{color:#e7e8f6}.pink-accent .svg-fill{fill:#fff}.pink-black{background-color:#994e83}.pink-black .secondary-bg{background-color:#e5548b}.pink-black .accent-bg{background-color:#5c5fbe}.pink-black .primary-color{color:#fff}.pink-black .secondary-color{color:#eee2ea}.pink-black .svg-fill{fill:#fff}.pink-white{background-color:#fff}.pink-white .secondary-bg{background-color:#fbe6ee}.pink-white .accent-bg{background-color:#5c5fbe}.pink-white .primary-color{color:#000}.pink-white .secondary-color{color:#8f8f8f}.pink-white .svg-fill{fill:#e5548b}.gray-flat{background-color:#c4c8d4}.gray-flat .secondary-bg{background-color:#e7e8e9}.gray-flat .accent-bg{background-color:#443b55}.gray-flat .primary-color{color:#000}.gray-flat .secondary-color{color:#72747b}.gray-flat .svg-fill{fill:#fff}.gray-gradient{background:linear-gradient(#e7e8e9,#c4c8d4)}.gray-gradient .secondary-bg{background-color:#e7e8e9}.gray-gradient .accent-bg{background-color:#443b55}.gray-gradient .primary-color{color:#000}.gray-gradient .secondary-color{color:#72747b}.gray-gradient .svg-fill{fill:#fff}.gray-accent{background-color:#443b55}.gray-accent .secondary-bg{background-color:#b6bac7}.gray-accent .accent-bg{background-color:#c4c8d4}.gray-accent .primary-color{color:#fff}.gray-accent .secondary-color{color:#e8e7ea}.gray-accent .svg-fill{fill:#fff}.gray-black{background-color:#e7e8e9}.gray-black .secondary-bg{background-color:#c4c8d4}.gray-black .accent-bg{background-color:#443b55}.gray-black .primary-color{color:#000}.gray-black .secondary-color{color:#5b5d5d}.gray-black .svg-fill{fill:#5b5d5d}.gray-white{background-color:#fff}.gray-white .secondary-bg{background-color:#f8f9fa}.gray-white .accent-bg{background-color:#443b55}.gray-white .primary-color{color:#000}.gray-white .secondary-color{color:#8f8f8f}.gray-white .svg-fill{fill:#b6bac7}.brown-flat{background-color:#5b1200}.brown-flat .secondary-bg{background-color:#911c00}.brown-flat .accent-bg{background-color:#fe7040}.brown-flat .primary-color{color:#fff}.brown-flat .secondary-color{color:#e5dad7}.brown-flat .svg-fill{fill:#fff}.brown-gradient{background:linear-gradient(#911c00,#5b1200)}.brown-gradient .secondary-bg{background-color:#911c00}.brown-gradient .accent-bg{background-color:#fe7040}.brown-gradient .primary-color{color:#fff}.brown-gradient .secondary-color{color:#e5dad7}.brown-gradient .svg-fill{fill:#fff}.brown-accent{background-color:#fe7040}.brown-accent .secondary-bg{background-color:#911c00}.brown-accent .accent-bg{background-color:#5b1200}.brown-accent .primary-color{color:#fff}.brown-accent .secondary-color{color:#ffeae3}.brown-accent .svg-fill{fill:#fff}.brown-black{background-color:#911c00}.brown-black .secondary-bg{background-color:#5b1200}.brown-black .accent-bg{background-color:#fe7040}.brown-black .primary-color{color:#fff}.brown-black .secondary-color{color:#eedcd8}.brown-black .svg-fill{fill:#fff}.brown-white{background-color:#fff}.brown-white .secondary-bg{background-color:#e5dad7}.brown-white .accent-bg{background-color:#fe7040}.brown-white .primary-color{color:#000}.brown-white .secondary-color{color:#8f8f8f}.brown-white .svg-fill{fill:#5b1200}.black-flat{background-color:#000}.black-flat .secondary-bg{background-color:#27224f}.black-flat .accent-bg{background-color:#4736ca}.black-flat .primary-color{color:#fff}.black-flat .secondary-color{color:#e0ddf6}.black-flat .svg-fill{fill:#fff}.black-gradient{background:linear-gradient(#27224f,#000)}.black-gradient .secondary-bg{background-color:#27224f}.black-gradient .accent-bg{background-color:#4736ca}.black-gradient .primary-color{color:#fff}.black-gradient .secondary-color{color:#e0ddf6}.black-gradient .svg-fill{fill:#fff}.black-accent{background-color:#4736ca}.black-accent .secondary-bg{background-color:#27224f}.black-accent .accent-bg{background-color:#000}.black-accent .primary-color{color:#fff}.black-accent .secondary-color{color:#e9fefc}.black-accent .svg-fill{fill:#fff}.black-black{background-color:#27224f}.black-black .secondary-bg{background-color:#000}.black-black .accent-bg{background-color:#4736ca}.black-black .primary-color{color:#fff}.black-black .secondary-color{color:#f0f0f0}.black-black .svg-fill{fill:#fff}.black-white{background-color:#fff}.black-white .secondary-bg{background-color:#e0ddf6}.black-white .accent-bg{background-color:#4736ca}.black-white .primary-color{color:#000}.black-white .secondary-color{color:#8f8f8f}.black-white .svg-fill{fill:#4736ca}.yellow-flat{background-color:#f5ca0a}.yellow-flat .secondary-bg{background-color:#f5870a}.yellow-flat .accent-bg{background-color:#353740}.yellow-flat .primary-color{color:#fff}.yellow-flat .secondary-color{color:#fefae6}.yellow-flat .svg-fill{fill:#fff}.yellow-gradient{background:linear-gradient(#ffa600,#f5ca0a)}.yellow-gradient .secondary-bg{background-color:#f5870a}.yellow-gradient .accent-bg{background-color:#353740}.yellow-gradient .primary-color{color:#fff}.yellow-gradient .secondary-color{color:#fefae6}.yellow-gradient .svg-fill{fill:#fff}.yellow-accent{background-color:#353740}.yellow-accent .secondary-bg{background-color:#f5870a}.yellow-accent .accent-bg{background-color:#f5ca0a}.yellow-accent .primary-color{color:#fff}.yellow-accent .secondary-color{color:#eaebec}.yellow-accent .svg-fill{fill:#fff}.yellow-black{background-color:#f5870a}.yellow-black .secondary-bg{background-color:#f5ca0a}.yellow-black .accent-bg{background-color:#353740}.yellow-black .primary-color{color:#fff}.yellow-black .secondary-color{color:#fef3e6}.yellow-black .svg-fill{fill:#fff}.yellow-white{background-color:#fff}.yellow-white .secondary-bg{background-color:#fefae6}.yellow-white .accent-bg{background-color:#353740}.yellow-white .primary-color{color:#000}.yellow-white .secondary-color{color:#8f8f8f}.yellow-white .svg-fill{fill:#f5ca0a}.orange-flat{background-color:#f24a2a}.orange-flat .secondary-bg{background-color:#d44125}.orange-flat .accent-bg{background-color:#2050d3}.orange-flat .primary-color{color:#fff}.orange-flat .secondary-color{color:#fbdcda}.orange-flat .svg-fill{fill:#fff}.orange-gradient{background:linear-gradient(#d44125,#f24a2a)}.orange-gradient .secondary-bg{background-color:#d44125}.orange-gradient .accent-bg{background-color:#2050d3}.orange-gradient .primary-color{color:#fff}.orange-gradient .secondary-color{color:#fbdcda}.orange-gradient .svg-fill{fill:#fff}.orange-accent{background-color:#2050d3}.orange-accent .secondary-bg{background-color:#d44125}.orange-accent .accent-bg{background-color:#f24a2a}.orange-accent .primary-color{color:#fff}.orange-accent .secondary-color{color:#dfe6f9}.orange-accent .svg-fill{fill:#fff}.orange-black{background-color:#d44125}.orange-black .secondary-bg{background-color:#f24a2a}.orange-black .accent-bg{background-color:#2050d3}.orange-black .primary-color{color:#fff}.orange-black .secondary-color{color:#f6d6d0}.orange-black .svg-fill{fill:#fff}.orange-white{background-color:#fff}.orange-white .secondary-bg{background-color:#fbdcda}.orange-white .accent-bg{background-color:#2050d3}.orange-white .primary-color{color:#000}.orange-white .secondary-color{color:#8f8f8f}.orange-white .svg-fill{fill:#f24a2a}.sunset-flat{background-color:#faa64d}.sunset-flat .secondary-bg{background-color:#e45a6a}.sunset-flat .accent-bg{background-color:#333}.sunset-flat .primary-color{color:#fff}.sunset-flat .secondary-color{color:#fff2e5}.sunset-flat .svg-fill{fill:#fff}.sunset-gradient{background:linear-gradient(#e45a6a,#faa64d)}.sunset-gradient .secondary-bg{background-color:#e45a6a}.sunset-gradient .accent-bg{background-color:#333}.sunset-gradient .primary-color{color:#fff}.sunset-gradient .secondary-color{color:#fff2e5}.sunset-gradient .svg-fill{fill:#fff}.sunset-accent{background-color:#333}.sunset-accent .secondary-bg{background-color:#e45a6a}.sunset-accent .accent-bg{background-color:#faa64d}.sunset-accent .primary-color{color:#fff}.sunset-accent .secondary-color{color:#dcdcdc}.sunset-accent .svg-fill{fill:#fff}.sunset-black{background-color:#e45a6a}.sunset-black .secondary-bg{background-color:#faa64d}.sunset-black .accent-bg{background-color:#333}.sunset-black .primary-color{color:#fff}.sunset-black .secondary-color{color:#fae2e5}.sunset-black .svg-fill{fill:#fff}.sunset-white{background-color:#fff}.sunset-white .secondary-bg{background-color:#fff2e5}.sunset-white .accent-bg{background-color:#333}.sunset-white .primary-color{color:#000}.sunset-white .secondary-color{color:#8f8f8f}.sunset-white .svg-fill{fill:#faa64d}.red-flat{background-color:#e41e35}.red-flat .secondary-bg{background-color:#a5131c}.red-flat .accent-bg{background-color:#193031}.red-flat .primary-color{color:#fff}.red-flat .secondary-color{color:#fbdfe2}.red-flat .svg-fill{fill:#fff}.red-gradient{background:linear-gradient(#a5131c,#e41e35)}.red-gradient .secondary-bg{background-color:#a5131c}.red-gradient .accent-bg{background-color:#193031}.red-gradient .primary-color{color:#fff}.red-gradient .secondary-color{color:#fbdfe2}.red-gradient .svg-fill{fill:#fff}.red-accent{background-color:#193031}.red-accent .secondary-bg{background-color:#a5131c}.red-accent .accent-bg{background-color:#e41e35}.red-accent .primary-color{color:#fff}.red-accent .secondary-color{color:#e0e3e3}.red-accent .svg-fill{fill:#fff}.red-black{background-color:#a5131c}.red-black .secondary-bg{background-color:#e41e35}.red-black .accent-bg{background-color:#193031}.red-black .primary-color{color:#fff}.red-black .secondary-color{color:#f2dedf}.red-black .svg-fill{fill:#fff}.red-white{background-color:#fff}.red-white .secondary-bg{background-color:#fbdfe2}.red-white .accent-bg{background-color:#193031}.red-white .primary-color{color:#000}.red-white .secondary-color{color:#8f8f8f}.red-white .svg-fill{fill:#e41e35}.lightblue-flat{background-color:#01d8fd}.lightblue-flat .secondary-bg{background-color:#2898fb}.lightblue-flat .accent-bg{background-color:#297afb}.lightblue-flat .primary-color{color:#fff}.lightblue-flat .secondary-color{color:#d7f9ff}.lightblue-flat .svg-fill{fill:#fff}.lightblue-gradient{background:linear-gradient(#2898fb,#01d8fd)}.lightblue-gradient .secondary-bg{background-color:#2898fb}.lightblue-gradient .accent-bg{background-color:#297afb}.lightblue-gradient .primary-color{color:#fff}.lightblue-gradient .secondary-color{color:#d7f9ff}.lightblue-gradient .svg-fill{fill:#fff}.lightblue-accent{background-color:#297afb}.lightblue-accent .secondary-bg{background-color:#2898fb}.lightblue-accent .accent-bg{background-color:#01d8fd}.lightblue-accent .primary-color{color:#fff}.lightblue-accent .secondary-color{color:#d3e4fe}.lightblue-accent .svg-fill{fill:#fff}.lightblue-black{background-color:#2898fb}.lightblue-black .secondary-bg{background-color:#01d8fd}.lightblue-black .accent-bg{background-color:#297afb}.lightblue-black .primary-color{color:#fff}.lightblue-black .secondary-color{color:#dbeefe}.lightblue-black .svg-fill{fill:#fff}.lightblue-white{background-color:#fff}.lightblue-white .secondary-bg{background-color:#d7f9ff}.lightblue-white .accent-bg{background-color:#297afb}.lightblue-white .primary-color{color:#000}.lightblue-white .secondary-color{color:#8f8f8f}.lightblue-white .svg-fill{fill:#01d8fd}.olive-flat{background-color:#8fa887}.olive-flat .secondary-bg{background-color:#365135}.olive-flat .accent-bg{background-color:#423310}.olive-flat .primary-color{color:#fff}.olive-flat .secondary-color{color:#e8ede7}.olive-flat .svg-fill{fill:#fff}.olive-gradient{background:linear-gradient(#365135,#8fa887)}.olive-gradient .secondary-bg{background-color:#365135}.olive-gradient .accent-bg{background-color:#423310}.olive-gradient .primary-color{color:#fff}.olive-gradient .secondary-color{color:#e8ede7}.olive-gradient .svg-fill{fill:#fff}.olive-accent{background-color:#423310}.olive-accent .secondary-bg{background-color:#365135}.olive-accent .accent-bg{background-color:#8fa887}.olive-accent .primary-color{color:#fff}.olive-accent .secondary-color{color:#dedcd6}.olive-accent .svg-fill{fill:#fff}.olive-black{background-color:#365135}.olive-black .secondary-bg{background-color:#8fa887}.olive-black .accent-bg{background-color:#423310}.olive-black .primary-color{color:#fff}.olive-black .secondary-color{color:#dce1dc}.olive-black .svg-fill{fill:#fff}.olive-white{background-color:#fff}.olive-white .secondary-bg{background-color:#e8ede7}.olive-white .accent-bg{background-color:#423310}.olive-white .primary-color{color:#000}.olive-white .secondary-color{color:#8f8f8f}.olive-white .svg-fill{fill:#8fa887}.green-flat{background-color:#8bd746}.green-flat .secondary-bg{background-color:#33a07f}.green-flat .accent-bg{background-color:#267b70}.green-flat .primary-color{color:#fff}.green-flat .secondary-color{color:#ebf8de}.green-flat .svg-fill{fill:#fff}.green-gradient{background:linear-gradient(#33a07f,#8bd746)}.green-gradient .secondary-bg{background-color:#33a07f}.green-gradient .accent-bg{background-color:#267b70}.green-gradient .primary-color{color:#fff}.green-gradient .secondary-color{color:#ebf8de}.green-gradient .svg-fill{fill:#fff}.green-accent{background-color:#267b70}.green-accent .secondary-bg{background-color:#33a07f}.green-accent .accent-bg{background-color:#8bd746}.green-accent .primary-color{color:#fff}.green-accent .secondary-color{color:#dbe9e8}.green-accent .svg-fill{fill:#fff}.green-black{background-color:#33a07f}.green-black .secondary-bg{background-color:#8bd746}.green-black .accent-bg{background-color:#267b70}.green-black .primary-color{color:#fff}.green-black .secondary-color{color:#ddefe9}.green-black .svg-fill{fill:#fff}.green-white{background-color:#fff}.green-white .secondary-bg{background-color:#ebf8de}.green-white .accent-bg{background-color:#267b70}.green-white .primary-color{color:#000}.green-white .secondary-color{color:#8f8f8f}.green-white .svg-fill{fill:#8bd746}.turquoise-flat{background-color:#3ad2ad}.turquoise-flat .secondary-bg{background-color:#35bac1}.turquoise-flat .accent-bg{background-color:#353740}.turquoise-flat .primary-color{color:#fff}.turquoise-flat .secondary-color{color:#e4f8f3}.turquoise-flat .svg-fill{fill:#fff}.turquoise-gradient{background:linear-gradient(#35bac1,#3ad2ad)}.turquoise-gradient .secondary-bg{background-color:#35bac1}.turquoise-gradient .accent-bg{background-color:#353740}.turquoise-gradient .primary-color{color:#fff}.turquoise-gradient .secondary-color{color:#e4f8f3}.turquoise-gradient .svg-fill{fill:#fff}.turquoise-accent{background-color:#353740}.turquoise-accent .secondary-bg{background-color:#35bac1}.turquoise-accent .accent-bg{background-color:#3ad2ad}.turquoise-accent .primary-color{color:#fff}.turquoise-accent .secondary-color{color:#e8e8e9}.turquoise-accent .svg-fill{fill:#fff}.turquoise-black{background-color:#35bac1}.turquoise-black .secondary-bg{background-color:#3ad2ad}.turquoise-black .accent-bg{background-color:#353740}.turquoise-black .primary-color{color:#fff}.turquoise-black .secondary-color{color:#e6f6f7}.turquoise-black .svg-fill{fill:#fff}.turquoise-white{background-color:#fff}.turquoise-white .secondary-bg{background-color:#e4f8f3}.turquoise-white .accent-bg{background-color:#353740}.turquoise-white .primary-color{color:#000}.turquoise-white .secondary-color{color:#8f8f8f}.turquoise-white .svg-fill{fill:#3ad2ad}.avocado-flat{background-color:#3aafa2}.avocado-flat .secondary-bg{background-color:#1b7068}.avocado-flat .accent-bg{background-color:#353740}.avocado-flat .primary-color{color:#fff}.avocado-flat .secondary-color{color:#e4f4f2}.avocado-flat .svg-fill{fill:#fff}.avocado-gradient{background:linear-gradient(#1b7068,#3aafa2)}.avocado-gradient .secondary-bg{background-color:#1b7068}.avocado-gradient .accent-bg{background-color:#353740}.avocado-gradient .primary-color{color:#fff}.avocado-gradient .secondary-color{color:#e4f4f2}.avocado-gradient .svg-fill{fill:#fff}.avocado-accent{background-color:#353740}.avocado-accent .secondary-bg{background-color:#1b7068}.avocado-accent .accent-bg{background-color:#3aafa2}.avocado-accent .primary-color{color:#fff}.avocado-accent .secondary-color{color:#e1e1e2}.avocado-accent .svg-fill{fill:#fff}.avocado-black{background-color:#1b7068}.avocado-black .secondary-bg{background-color:#3aafa2}.avocado-black .accent-bg{background-color:#353740}.avocado-black .primary-color{color:#fff}.avocado-black .secondary-color{color:#ddeae9}.avocado-black .svg-fill{fill:#fff}.avocado-white{background-color:#fff}.avocado-white .secondary-bg{background-color:#e4f4f2}.avocado-white .accent-bg{background-color:#353740}.avocado-white .primary-color{color:#000}.avocado-white .secondary-color{color:#8f8f8f}.avocado-white .svg-fill{fill:#3aafa2}.beige-flat{background-color:#f2dec5}.beige-flat .secondary-bg{background-color:#e0c295}.beige-flat .accent-bg{background-color:#666663}.beige-flat .primary-color{color:#2d271e}.beige-flat .secondary-color{color:#756858}.beige-flat .svg-fill{fill:#fff}.beige-gradient{background:linear-gradient(#e0c295,#f2dec5)}.beige-gradient .secondary-bg{background-color:#e0c295}.beige-gradient .accent-bg{background-color:#666663}.beige-gradient .primary-color{color:#2d271e}.beige-gradient .secondary-color{color:#756858}.beige-gradient .svg-fill{fill:#fff}.beige-accent{background-color:#666663}.beige-accent .secondary-bg{background-color:#e0c295}.beige-accent .accent-bg{background-color:#f2dec5}.beige-accent .primary-color{color:#fff}.beige-accent .secondary-color{color:#efefef}.beige-accent .svg-fill{fill:#fff}.beige-black{background-color:#e0c295}.beige-black .secondary-bg{background-color:#f2dec5}.beige-black .accent-bg{background-color:#666663}.beige-black .primary-color{color:#2d271e}.beige-black .secondary-color{color:#6f604a}.beige-black .svg-fill{fill:#fff}.beige-white{background-color:#fff}.beige-white .secondary-bg{background-color:#fbf6ef}.beige-white .accent-bg{background-color:#666663}.beige-white .primary-color{color:#2d271e}.beige-white .secondary-color{color:#8f8f8f}.beige-white .svg-fill{fill:#e0c295}.peach-flat{background-color:#f7806e}.peach-flat .secondary-bg{background-color:#ffa067}.peach-flat .accent-bg{background-color:#143f6e}.peach-flat .primary-color{color:#fff}.peach-flat .secondary-color{color:#feeae7}.peach-flat .svg-fill{fill:#fff}.peach-gradient{background:linear-gradient(#ff465e,#f7806e)}.peach-gradient .secondary-bg{background-color:#ffa067}.peach-gradient .accent-bg{background-color:#143f6e}.peach-gradient .primary-color{color:#fff}.peach-gradient .secondary-color{color:#feeae7}.peach-gradient .svg-fill{fill:#fff}.peach-accent{background-color:#143f6e}.peach-accent .secondary-bg{background-color:#ffa067}.peach-accent .accent-bg{background-color:#f7806e}.peach-accent .primary-color{color:#fff}.peach-accent .secondary-color{color:#d1d9e2}.peach-accent .svg-fill{fill:#fff}.peach-black{background-color:#ffa067}.peach-black .secondary-bg{background-color:#f7806e}.peach-black .accent-bg{background-color:#143f6e}.peach-black .primary-color{color:#fff}.peach-black .secondary-color{color:#fff1e8}.peach-black .svg-fill{fill:#fff}.peach-white{background-color:#fff}.peach-white .secondary-bg{background-color:#feeae7}.peach-white .accent-bg{background-color:#143f6e}.peach-white .primary-color{color:#000}.peach-white .secondary-color{color:#8f8f8f}.peach-white .svg-fill{fill:#f7806e}.chateruse-flat{background-color:#bdd155}.chateruse-flat .secondary-bg{background-color:#6e7725}.chateruse-flat .accent-bg{background-color:#778629}.chateruse-flat .primary-color{color:#fff}.chateruse-flat .secondary-color{color:#f3f7df}.chateruse-flat .svg-fill{fill:#fff}.chateruse-gradient{background:linear-gradient(#99ab3b,#bdd155)}.chateruse-gradient .secondary-bg{background-color:#6e7725}.chateruse-gradient .accent-bg{background-color:#778629}.chateruse-gradient .primary-color{color:#fff}.chateruse-gradient .secondary-color{color:#f3f7df}.chateruse-gradient .svg-fill{fill:#fff}.chateruse-accent{background-color:#778629}.chateruse-accent .secondary-bg{background-color:#6e7725}.chateruse-accent .accent-bg{background-color:#bdd155}.chateruse-accent .primary-color{color:#fff}.chateruse-accent .secondary-color{color:#eaecdd}.chateruse-accent .svg-fill{fill:#fff}.chateruse-black{background-color:#6e7725}.chateruse-black .secondary-bg{background-color:#bdd155}.chateruse-black .accent-bg{background-color:#778629}.chateruse-black .primary-color{color:#fff}.chateruse-black .secondary-color{color:#eaebdf}.chateruse-black .svg-fill{fill:#fff}.chateruse-white{background-color:#fff}.chateruse-white .secondary-bg{background-color:#f3f7df}.chateruse-white .accent-bg{background-color:#778629}.chateruse-white .primary-color{color:#000}.chateruse-white .secondary-color{color:#8f8f8f}.chateruse-white .svg-fill{fill:#bdd155}.android{position:absolute;display:inline-block;background:linear-gradient(#111112,#0e0e0e);padding:52px 30px 51px 11px;border-radius:24px 42px 23px 45px;transform:rotateX(-45deg) rotateZ(45deg) translateX(0);box-shadow:8px 8px 21px 0 rgba(0,0,0,.26),inset -5px -5px 6px 0 rgba(0,0,0,.12156862745098039),inset -15px -15px 13px 0 #111112,inset -11px -11px 6px 3px rgba(255,255,255,.75);right:120px;bottom:-240px}.android:before{content:"";position:absolute;left:39px;top:26px;background:radial-gradient(farthest-corner at 5px 8px,#302f50 13%,#0f0d14 33%);box-shadow:0 0 0 3px #1b1b1c;width:12px;border-radius:50%;height:12px}.android:after{content:"";height:6px;width:100px;left:calc(50% - 50px);top:15px;border-radius:8px;position:absolute;background:#2e2a2a;box-shadow:inset 0 2px 1px 1px #1b1b1b}.android .mask{width:260px;min-height:420px;max-height:500px;border-radius:10px}@media screen and (max-width:820px){.android{position:relative;bottom:initial;right:initial;transform:none;border-radius:30px;margin:50px auto 0;box-shadow:inset 0 2px 2px 0 rgba(255,255,255,.19),inset 0 0 2px 1px #4f4f4f,inset 0 0 1px 3px #1e1e1e,inset 0 0 1px 5px #333,inset 0 3px 0 6px #000,inset 0 -3px 3px 4px #000,inset 0 5px 3px 5px rgba(255,255,255,.2),inset 0 -14px 7px 5px #171717;padding:52px 10px 50px 10px}}.android2 .mask{border-radius:10px;border:1px solid #000;width:250px;min-height:420px;max-height:540px}.android2{position:relative;display:inline-block;background:linear-gradient(#111112,#0e0e0e);z-index:2;padding:55px 13px 45px 13px;border-radius:30px;transform:rotate(6deg);box-shadow:inset 0 2px 2px 0 rgba(255,255,255,.19),inset 0 0 2px 1px #4f4f4f,inset 0 0 1px 3px #1e1e1e,inset 0 0 1px 5px #333,inset 0 3px 0 6px #000,inset 0 -3px 3px 4px #000,inset -4px 0 4px 6px rgba(255,255,255,.3),inset 4px 0 4px 6px rgba(255,255,255,.3),inset 0 -14px 7px 5px #171717,2px 2px 4px 0 rgba(0,0,0,.1),12px 12px 24px 0 rgba(0,0,0,.1)}.android2:before{content:"";position:absolute;left:39px;top:22px;background:radial-gradient(farthest-corner at 4px 7px,#292843 13%,#0f0d14 27%);box-shadow:0 0 0 3px #1b1b1c;width:10px;border-radius:50%;height:10px}.android2:after{content:"";height:5px;width:100px;left:calc(50% - 50px);top:15px;border-radius:8px;position:absolute;background:#2e2a2a;box-shadow:inset 0 2px 1px 1px #1b1b1b}@media screen and (max-width:820px){.android2{position:relative;bottom:initial;right:initial;transform:none;border-radius:30px;margin:50px auto 0;box-shadow:inset 0 2px 2px 0 rgba(255,255,255,.19),inset 0 0 2px 1px #4f4f4f,inset 0 0 1px 3px #1e1e1e,inset 0 0 1px 5px #333,inset 0 3px 0 6px #000,inset 0 -3px 3px 4px #000,inset 0 5px 3px 5px rgba(255,255,255,.2),inset 0 -14px 7px 5px #171717;padding:52px 10px 50px 10px}}.bg{position:absolute;left:0;top:0;width:100%;height:100%}.bg-image{width:100%;height:100%;background-size:cover;background-position:center;mix-blend-mode:multiply;filter:grayscale(100%) contrast(1)}.bg-author{position:absolute;bottom:10px;left:10px;cursor:pointer;z-index:2}.background,.background-white{position:relative}.background-white .bg-image{filter:initial}.background-white .primary-color,.background-white .secondary-color{color:#fff;text-shadow:0 2px 0 rgba(0,0,0,.1)}.browser{border-radius:6px;background-size:60px;overflow:hidden;width:100%;z-index:2;background-size:auto 30px;box-shadow:0 20px 30px 0 rgba(0,0,0,.1);position:relative}.browser .mask{max-height:640px}.browser:before{content:"";height:30px;line-height:30px;display:block;width:100%;position:relative;background:linear-gradient(-180deg,#fafbfc 0,#f1f4f7 100%)}.browser:after{content:"";width:12px;height:12px;background:#e2e5e5;position:absolute;border-radius:50%;top:10px;left:8px;box-shadow:18px 0 0 #e2e5e5,36px 0 0 #e2e5e5}.browserphone{position:relative;margin-top:80px}.browserphone-iphone .mask{width:210px;min-height:310px;max-height:400px}.browserphone-iphone{z-index:2;position:absolute;display:inline-block;bottom:0;right:0;background:linear-gradient(#f4f4f4,#f2f2f2);box-shadow:inset 0 0 1px 1px #dbdcdd,inset 0 0 1px 4px #efefef,inset 0 0 0 5px #fff,inset 0 0 0 6.5px #edf1f2,inset 5px 0 7px 5px #fff,inset -5px 0 7px 5px #fff;border-radius:35px;padding:50px 15px 60px}.browserphone-iphone:before{content:"";width:52px;height:7px;border-radius:10px;position:absolute;box-shadow:inset 0 4px 3px 0 #e6e6e6,inset 0 0 0 2px #ececec;top:25px;left:calc(50% - 26px)}.browserphone-iphone:after{content:"";width:38px;height:38px;border-radius:50%;position:absolute;box-shadow:inset 0 -2px .2px 0 #d5d5d5,inset 0 0 0 2.5px #e8e8e8;bottom:14px;left:calc(50% - 19px)}.browserphone-browser{border-radius:6px;background-size:60px;box-shadow:0 10px 20px rgba(40,39,66,.06);overflow:hidden;width:900px;z-index:2;background-size:auto 30px;position:relative;box-shadow:0 15px 19px 6px rgba(0,0,0,.05)}.browserphone-browser .mask{min-height:490px;max-height:580px}.browserphone-browser:before{content:"";height:30px;line-height:30px;display:block;width:100%;position:relative;background:linear-gradient(-180deg,#fafbfc 0,#f1f4f7 100%)}.browserphone-browser:after{content:"";width:12px;height:12px;background:#e2e5e5;position:absolute;border-radius:50%;top:10px;left:8px;box-shadow:18px 0 0 #e2e5e5,36px 0 0 #e2e5e5}@media screen and (max-width:820px){.browserphone-browser{margin-left:100px}.browserphone-iphone{right:initial}}.codeblock{box-shadow:0 8px 24px 0 rgba(0,0,0,.1);border-radius:6px;position:relative;max-width:calc(100vw - 40px)}.codeblock:before{content:"";height:30px;display:block;width:100%;position:relative;background:#fff;border-radius:6px 6px 0 0}.codeblock::after{content:"";width:12px;height:12px;position:absolute;border-radius:50%;top:10px;left:8px;background:rgba(0,0,0,.07);box-shadow:18px 0 0 rgba(0,0,0,.07),36px 0 0 rgba(0,0,0,.07)}.hljs *{font-family:SFMono-Regular,Consolas,"Liberation Mono",Menlo,Courier,monospace;font-size:14px!important;line-height:1.4;font-weight:400}td.hljs-ln-numbers{user-select:none;text-align:right;color:#333;opacity:.33;vertical-align:top;padding-right:15px}.hljs-ln{border-collapse:collapse}.hljs-ln-line{text-align:left;white-space:pre}.hljs-ln-n:before{content:attr(data-line-number);font-family:SFMono-Regular,Consolas,"Liberation Mono",Menlo,Courier,monospace}td.hljs-ln-code{padding-left:10px}.hljs{padding:30px;display:block;overflow-x:auto;background:#fff;color:#333;overflow:auto;width:100%;border-radius:0 0 6px 6px;min-height:400px;-webkit-overflow-scrolling:touch}.hljs-comment,.hljs-quote{color:#6a737d}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#2fb651}.hljs-literal,.hljs-number,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:#0e9fda}.hljs-doctag,.hljs-string{color:#ff8d29}.hljs-section,.hljs-selector-id,.hljs-title{color:#dd2e03}.hljs-class .hljs-title,.hljs-type{color:#458}.hljs-attribute,.hljs-name,.hljs-tag{color:navy}.hljs-link,.hljs-regexp{color:#009926}.hljs-bullet,.hljs-symbol{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}@media screen and (max-width:820px){.hljs{padding:20px;min-height:inherit;max-height:600px}td.hljs-ln-numbers{display:none}.hljs *{font-size:11px!important}}.computer .mask{box-shadow:0 0 3px 1px rgba(0,0,0,.1);max-width:770px;max-height:520px}.computer{display:inline-block;background:linear-gradient(#000 0,#000 95.5%,#2c2b2d 95.5%);border-radius:30px;position:relative;margin:75px auto 20px;border:3px solid #b1b2b5;box-shadow:-1px 1px 4px 0 rgba(0,0,0,.1),-8px 8px 24px 0 rgba(0,0,0,.1);padding:30px 20px}.computer:after{content:"";position:absolute;height:20px;left:-10%;bottom:-14px;width:120%;transform-origin:0 0 0;border-radius:0 0 100% 100%;box-shadow:inset 0 18px 8px 0 #686a6e}.computer-bottom{content:"";height:15px;background:linear-gradient(to right,#27282b 0,#a5a6aa 1.5%,#3f4044 3%,#8c8d91 10%,#8c8d91 90%,#3f4044 97%,#a5a6aa 98.5%,#27282b 100%);position:absolute;width:120%;left:-10%;bottom:-3px;z-index:2}.computer-bottom:before{content:"";position:absolute;width:15%;left:calc(50% - 7.5%);box-shadow:inset 0 -9px 10px #0000003d,inset 10px 0 10px #0000003d,inset -10px 0 10px #0000003d;border-radius:0 0 10px 10px;height:10px;top:0}.computer .mask{border-radius:2px}.computer .mask:after{background-size:11%}@media screen and (max-width:820px){.computer{padding:15px 10px 15px 10px;border-radius:10px;border:2px solid #cacccf;margin:50px auto 20px}.computer:before{bottom:2px;height:5px}.computer:after{height:7px;bottom:-5px}}.computerphone{margin-top:75px;position:relative}.computerphone-computer .mask{box-shadow:0 0 3px 1px rgba(0,0,0,.1)}.computerphone-computer{display:inline-block;background:linear-gradient(#000 0,#000 95.5%,#2c2b2d 95.5%);border-radius:30px;position:relative;border:3px solid #b1b2b5;box-shadow:-1px 1px 4px 0 rgba(0,0,0,.1),-8px 8px 24px 0 rgba(0,0,0,.1);padding:30px 20px;width:850px}.computerphone-computer:after{content:"";position:absolute;height:21px;left:-10%;bottom:-14px;width:120%;transform-origin:0 0 0;background:#6a686d;box-shadow:inset 0 -3px 8px 1px #373638;border-radius:0 0 100% 100%}.computerphone-computer-bottom{content:"";height:7px;background:linear-gradient(to right,#49474a 0,#dbdbdd 1.5%,#d1d1d5 3%,#c7c7cb 10%,#c7c7cb 90%,#d1d1d5 97%,#dbdbdd 98.5%,#27282b 100%);position:absolute;width:120%;left:-10%;bottom:0;z-index:2}.computerphone-computer-bottom:before{content:"";position:absolute;width:20%;left:calc(50% - 10%);box-shadow:inset 5px 0 6px 0 #0000003d,inset -5px 0 6px 0 #0000003d;height:7px;top:0}.computerphone-computer .mask{min-height:450px;max-height:580px}.computerphone-iphone .mask{width:210px;min-height:310px;max-height:400px}.computerphone-iphone{z-index:2;position:absolute;display:inline-block;bottom:-20px;right:0;background:#010101;box-shadow:inset 0 0 .5px 1px #6e6c72,inset 0 0 1px 2px #4d4d50,inset 0 0 0 3.5px #747479,inset -2px 0 0 3.5px #18181a,inset 2px 0 0 3.5px #18181a,inset 0 2px 10px 10px rgba(255,255,255,.28);border-radius:35px;padding:50px 15px 60px}.computerphone-iphone:before{content:"";width:50px;height:7px;border-radius:10px;position:absolute;box-shadow:inset 0 4px 3px 0 #18181a,inset 0 0 0 2px #373638;top:25px;left:calc(50% - 25px)}.computerphone-iphone:after{content:"";width:38px;height:38px;border-radius:50%;position:absolute;box-shadow:inset 0 -2px .2px 0 #4b4a4d,inset 0 0 0 2.5px #2d2d2d;bottom:14px;left:calc(50% - 19px);opacity:.6}@media screen and (max-width:820px){.computerphone{margin-top:50px}.computerphone-computer{margin-left:100px}.computerphone-iphone{right:initial}}.ipadiphone{position:relative}.ipadiphone-ipad .mask{min-width:720px;max-width:990px;height:520px}.ipadiphone-ipad{border-radius:35px;overflow:hidden;padding:15px 70px;background:linear-gradient(#f4f4f4,#f2f2f2);box-shadow:inset -7px 0 .4px -7px #e5e5e5,inset 7px 0 .4px -7px #eaeaea,inset 0 4px 1px -3px #ddd,inset 0 -5px 1px -4px #8a8a8a,inset 0 0 0 2px #fff,inset 0 0 0 3px rgba(0,0,0,.08),inset 1px 0 0 4px #fff,inset -1px 0 0 4px #fff,inset 2px 0 .2px 5px rgba(0,0,0,.05),inset -2px 0 .2px 5px rgba(0,0,0,.05),inset 12px 0 .2px -1px #fff,inset -12px 0 .2px -1px #fff;display:inline-block;position:relative;left:70px}.ipadiphone-iphone .mask{width:210px;min-height:310px;max-height:400px}.ipadiphone-iphone{z-index:99;position:absolute;display:inline-block;bottom:-20px;background:linear-gradient(#f4f4f4,#f2f2f2);box-shadow:inset 0 0 1px 1px #dbdcdd,inset 0 0 1px 4px #efefef,inset 0 0 0 5px #fff,inset 0 0 0 6.5px #edf1f2,inset 5px 0 7px 5px #fff,inset -5px 0 7px 5px #fff;border-radius:35px;padding:50px 15px 60px}.ipadiphone-iphone:before{content:"";width:53px;height:6px;border-radius:10px;position:absolute;box-shadow:inset 0 4px 3px 0 #e6e6e6,inset 0 0 0 2px #ececec;top:26px;left:calc(50% - 26px)}.ipadiphone-iphone:after{content:"";width:38px;height:38px;border-radius:50%;position:absolute;box-shadow:inset 0 -2px .2px 0 #d5d5d5,inset 0 0 0 2.5px #e8e8e8;bottom:14px;left:calc(50% - 19px)}@media screen and (max-width:820px){.ipadiphone{margin-top:50px}}.iphone .mask{width:250px;min-height:420px;max-height:520px}.iphone{position:absolute;display:inline-block;background:linear-gradient(#f4f4f4,#f2f2f2);padding:62px 30px 78px 12px;border-radius:40px 52px 33px 55px;transform:rotateX(-45deg) rotateZ(45deg) translateX(0);box-shadow:8px 8px 21px 0 rgba(0,0,0,.26),inset -5px -5px 6px 0 rgba(0,0,0,.12156862745098039),inset -15px -15px 13px 0 #c5cbd0,inset -11px -11px 6px 3px rgba(255,255,255,.75);right:130px;bottom:-240px}.iphone:before{content:"";width:58px;height:7px;border-radius:10px;position:absolute;box-shadow:inset 0 4px 3px 0 #d5d5d5,inset 0 0 0 2px #e8e8e8;top:31px;left:calc(50% - 29px)}.iphone:after{content:"";width:44px;height:44px;border-radius:50%;position:absolute;box-shadow:inset 0 -2px .2px 0 #d5d5d5,inset 0 0 0 2.5px #e8e8e8;bottom:27px;left:calc(50% - 22px)}@media screen and (max-width:820px){.iphone{position:relative;bottom:initial;right:initial;transform:none;border-radius:34px;margin:50px auto 0;box-shadow:inset 0 0 1px 1px #dbdcdd,inset 0 0 1px 4px #efefef,inset 0 0 0 5px #fff,inset 0 0 0 6.5px #edf1f2,inset 5px 0 7px 5px #fff,inset -5px 0 7px 5px #fff;padding:55px 14px 60px}.iphone:before{box-shadow:inset 0 4px 3px 0 #e6e6e6,inset 0 0 0 2px #ececec}.iphone:after{width:40px;height:40px;bottom:14px;left:calc(50% - 20px)}}.iphone2 .mask{width:250px;min-height:420px;max-height:540px}.iphone2{background:linear-gradient(#f4f4f4,#f2f2f2);display:inline-block;position:relative;padding:60px 16px 70px 16px;border-radius:37px;transform:rotate(6deg);box-shadow:inset 0 0 2px 2px #dbdcdd,inset 0 0 1px 6px #efefef,inset 0 0 0 7px #fff,inset 0 0 0 8.5px #edf1f2,inset 7px 0 8px 5px #fff,inset -7px 0 8px 5px #fff,2px 2px 4px 0 rgba(0,0,0,.1),12px 12px 24px 0 rgba(0,0,0,.1)}.iphone2:before{content:"";width:58px;height:7px;border-radius:10px;position:absolute;box-shadow:inset 0 4px 3px 0 #d5d5d5,inset 0 0 0 2px #e8e8e8;top:31px;left:calc(50% - 29px)}.iphone2:after{content:"";width:44px;height:44px;border-radius:50%;position:absolute;box-shadow:inset 0 -2px .2px 0 #d5d5d5,inset 0 0 0 2.5px #e8e8e8;bottom:16px;left:calc(50% - 22px)}@media screen and (max-width:820px){.iphone2{position:relative;bottom:initial;right:initial;transform:none;border-radius:34px;margin:50px auto 0;box-shadow:inset 0 0 1px 1px #dbdcdd,inset 0 0 1px 4px #efefef,inset 0 0 0 5px #fff,inset 0 0 0 6.5px #edf1f2,inset 5px 0 7px 5px #fff,inset -5px 0 7px 5px #fff;padding:55px 14px 60px}.iphone2:before{box-shadow:inset 0 4px 3px 0 #e6e6e6,inset 0 0 0 2px #ececec}.iphone2:after{width:40px;height:40px;bottom:14px;left:calc(50% - 20px)}}.iphoneandroid-iphone .mask{box-shadow:0 0 4px rgba(107,124,147,.3);width:210px;min-height:350px;max-height:560px}.iphoneandroid-iphone{background:linear-gradient(#f4f4f4,#f2f2f2);box-shadow:inset 0 0 1px 1px #dbdcdd,inset 0 0 1px 4px #efefef,inset 0 0 0 5px #fff,inset 0 0 0 6.5px #edf1f2,inset 5px 0 7px 5px #fff,inset -5px 0 7px 5px #fff;border-radius:36px;padding:50px 16px 60px;position:absolute;bottom:0;right:0;display:inline-block;z-index:2}.iphoneandroid-iphone:before{content:"";width:48px;height:5px;border-radius:27px;position:absolute;box-shadow:inset 0 4px 3px 0 #e6e6e6,inset 0 0 0 2px #ececec;top:26px;left:calc(50% - 24px)}.iphoneandroid-iphone:after{content:"";width:38px;height:38px;border-radius:50%;position:absolute;box-shadow:inset 0 -2px .2px 0 #d5d5d5,inset 0 0 0 2.5px #e8e8e8;bottom:14px;left:calc(50% - 19px)}.iphoneandroid-android .mask{border:2px solid #000;height:440px;min-width:250px;max-width:270px;border-radius:10px}.iphoneandroid{position:relative;max-width:470px;width:100%}.iphoneandroid-android{position:relative;display:inline-block;border-radius:27px;padding:45px 12px;background:linear-gradient(#0e0e0e,#111);box-shadow:inset 0 2px 2px 0 rgba(255,255,255,.19),inset 0 0 2px 1px #4f4f4f,inset 0 0 1px 3px #1e1e1e,inset 0 0 1px 5px #333,inset 0 3px 0 6px #000,inset 0 -3px 3px 4px #000,inset -4px 0 4px 6px rgba(255,255,255,.3),inset 4px 0 4px 6px rgba(255,255,255,.3),inset 0 -14px 7px 5px #171717,2px 2px 4px 0 rgba(0,0,0,.1),12px 12px 24px 0 rgba(0,0,0,.1)}.iphoneandroid-android:before{content:"";position:absolute;left:39px;top:22px;background:radial-gradient(farthest-corner at 4px 7px,#292843 13%,#0f0d14 27%);box-shadow:0 0 0 3px #1b1b1c;width:10px;border-radius:50%;height:10px}.iphoneandroid-android:after{content:"";height:5px;width:100px;left:calc(50% - 50px);top:15px;border-radius:8px;position:absolute;background:#2e2a2a;box-shadow:inset 0 2px 1px 1px #1b1b1b}@media screen and (max-width:820px){.iphoneandroid{margin-top:50px}}.iphoneandroid2-iphone .mask{box-shadow:0 0 4px rgba(107,124,147,.3);width:220px;min-height:350px;max-height:420px}.iphoneandroid2-iphone{background:linear-gradient(#f4f4f4,#f2f2f2);box-shadow:inset -7px 0 .4px -7px #e5e5e5,inset 7px 0 .4px -7px #eaeaea,inset 0 4px 1px -3px #ddd,inset 0 -5px 1px -4px #8a8a8a,inset 0 0 0 2px #fff,inset 0 0 0 3px rgba(0,0,0,.08),inset 1px 0 0 4px #fff,inset -1px 0 0 4px #fff,inset 2px 0 .2px 5px rgba(0,0,0,.05),inset -2px 0 .2px 5px rgba(0,0,0,.05),inset 12px 0 .2px -1px #fff,inset -12px 0 .2px -1px #fff;border-radius:35px;padding:60px 12px 70px;display:inline-block;position:absolute;right:-30px;bottom:-100px;z-index:2}.iphoneandroid2-iphone:before{content:"";width:48px;height:5px;border-radius:27px;position:absolute;box-shadow:inset 0 4px 3px 0 #d5d5d5,inset 0 0 0 2px #e8e8e8;top:31px;left:calc(50% - 24px)}.iphoneandroid2-iphone:after{content:"";width:44px;height:44px;border-radius:50%;position:absolute;box-shadow:inset 0 -2px .2px 0 #d5d5d5,inset 0 0 0 2.5px #e8e8e8;bottom:15px;left:calc(50% - 22px)}.iphoneandroid2{position:relative;transform:rotate(13deg);max-width:470px;width:100%}.iphoneandroid2-android .mask{border-radius:10px;border:1px solid #000;width:220px;min-height:350px;max-height:420px}.iphoneandroid2-android{position:relative;display:inline-block;z-index:1;border-radius:27px;padding:45px 10px;background:linear-gradient(#0e0e0e,#111);box-shadow:inset 0 2px 2px 0 rgba(255,255,255,.19),inset 0 0 2px 1px #4f4f4f,inset 0 0 1px 3px #1e1e1e,inset 0 0 1px 5px #333,inset 0 3px 0 6px #000,inset 0 -3px 3px 4px #000,inset 0 5px 3px 5px rgba(255,255,255,.2),inset 0 -14px 7px 5px #171717}.iphoneandroid2-android:before{content:"";position:absolute;left:39px;top:22px;background:radial-gradient(farthest-corner at 4px 7px,#292843 13%,#0f0d14 27%);box-shadow:0 0 0 3px #1b1b1c;width:10px;border-radius:50%;height:10px}.iphoneandroid2-android:after{content:"";height:5px;width:100px;left:calc(50% - 50px);top:15px;border-radius:8px;position:absolute;background:#2e2a2a;box-shadow:inset 0 2px 1px 1px #1b1b1b}@media screen and (max-width:820px){.iphoneandroid2{margin-bottom:-200px;transform:rotate(0)}.iphoneandroid2-iphone{top:initial;position:absolute;bottom:0;right:0;padding:50px 16px 60px}.iphoneandroid2-iphone:before{top:26px}.iphoneandroid2-iphone:after{width:38px;height:38px;bottom:16px}.iphoneandroid2-android .mask{max-height:initial;height:440px;min-width:250px;max-width:270px}}.video{position:relative}.youtube-responsive{width:100%;padding-top:56.25%}.youtube-video{position:absolute;top:0;left:0;width:100%;height:100%}.mask{width:100%;overflow:hidden;border-radius:0 0 4px 4px;background:#fff;position:relative}.mask__noimage{height:100%}.mask-img{width:100%;display:block;z-index:2;position:relative}.custom-img{max-width:100%;display:block;position:relative;margin:auto}.icon{width:64px;height:64px;flex-shrink:0;display:flex;align-items:center;justify-content:center;border-radius:50%;background:rgba(34,126,247,.07);margin-bottom:20px}.icon svg{width:100%;max-width:30px;max-height:30px}.slideshow{width:100%;position:relative;height:initial}.slideshow-item{position:absolute;visbility:hidden;width:100%;height:100%;top:0}.slideshow-bullet:checked+.slideshow-item .slideshow-item-content{transition:all .2s ease-in-out .1s;opacity:1}.slideshow-item-content{transition:all .2s ease-in-out;opacity:0}.slideshow-bullet:checked+.slideshow-item{position:relative;visbility:visible;z-index:2;height:initial}.slideshow-nav{position:absolute;top:calc(50% - 20px);right:10px;width:40px;height:40px;border-radius:50%;cursor:pointer;background-image:url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gICAgPHBhdGggZD0iTTguNTkgMTYuMzRsNC41OC00LjU5LTQuNTgtNC41OUwxMCA1Ljc1bDYgNi02IDZ6Ii8+ICAgIDxwYXRoIGQ9Ik0wLS4yNWgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==);background-repeat:no-repeat;background-position:center}.slideshow-nav-previous{right:auto;left:10px;background-image:url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gICAgPHBhdGggZD0iTTE1LjQxIDE2LjA5bC00LjU4LTQuNTkgNC41OC00LjU5TDE0IDUuNWwtNiA2IDYgNnoiLz4gICAgPHBhdGggZD0iTTAtLjVoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz48L3N2Zz4=)}.slideshow input{display:none}@media screen and (max-width:820px){.slideshow-nav,.slideshow-nav-previous{top:initial;bottom:7px}}.card{border:1px solid rgba(0,0,0,.1);border-radius:3px}.card>div:not(:first-of-type){border-left:1px solid rgba(0,0,0,.1)}@media screen and (max-width:820px){.card>div:not(:first-of-type){border-top:1px solid rgba(0,0,0,.1);border-left:none}}.faq:not(:last-of-type){border-bottom:1px solid rgba(0,0,0,.1)}.faq-input{position:absolute;opacity:0;pointer-events:none}.faq-label{cursor:pointer}.faq-content{display:none}.faq-input:checked~label svg{transform:rotate(180deg)}.faq-input:checked~.faq-content{display:block;border-bottom:0}.checklist{list-style:none}.checklist-item:not(:last-of-type){border-bottom:1px solid rgba(0,0,0,.1)}.press-logo{max-width:100%;max-height:60px;display:block;margin:auto}.linebreak{word-spacing:300px}.team-card{padding:30px;min-height:460px;border:1px solid rgba(0,0,0,.1);border-radius:3px}.blog{border-radius:3px;min-height:300px;width:100%;color:inherit;text-decoration:none;background:rgba(0,0,0,.07)}.blog-profilepic{width:50px;height:50px;border-radius:50%;margin-right:10px;background-size:cover;background-position:center}.blog-img{width:100%;height:160px;background-color:#fff;background-size:cover;background-position:center}.blog-img__small{min-height:420px;background-color:#fff;background-size:cover;background-position:center}@media screen and (max-width:820px){.header-email{width:30px;height:30px;border-radius:50%;overflow:hidden;position:relative;text-indent:100px;margin:6px 0}}@media screen and (min-width:820px){.header-email{position:relative;overflow:hidden;background:0 0!important}.header-email:after{background:0 0!important}}@media (max-width:820px){.main-menu{position:fixed;z-index:200;pointer-events:none;opacity:0;transition:all .2s ease-in-out;left:0;top:0;width:100vw;height:100vh;display:flex;justify-content:center;flex-direction:column;background:#fff}.main-menu .menu-close{position:absolute;right:20px;top:20px}.main-menu:target{opacity:1;pointer-events:all}.main-menu:target .nav-link{display:block;width:100%;text-align:center;margin:20px 0;font-size:30px;color:#000!important}}@media (min-width:820px){.header .nav-link:last-of-type{margin-right:0}.menu-toggle{line-height:44px}.main-menu .menu-close,.menu-toggle{display:none}}
--------------------------------------------------------------------------------