├── .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 | Librarian

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 | [![NPM Version](http://img.shields.io/npm/v/librarian-server.svg?style=flat)](https://www.npmjs.org/package/librarian-server) 10 | 11 | Librarian on iOS

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 |
173 |
174 |
175 | 176 | 179 |
180 |
181 |
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 |
197 |
198 |
199 |
Screenshot of Android App
200 |
201 |
202 |
Screenshot of iPhone App
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |

Quick Setup

213 |

Open Terminal and run:

214 |
    215 |
  • 216 |

    npm i -g librarian-server

    217 | 218 | 219 | 220 | 221 | 222 |
  • 223 |
  • 224 |

    librarian setup

    225 | 226 | 227 | 228 | 229 | 230 |
  • 231 |
  • 232 |

    librarian start

    233 | 234 | 235 | 236 | 237 | 238 |
  • 239 |
240 |
241 |
242 |
243 |
Screenshot of iPhone App
244 |
245 |
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}} --------------------------------------------------------------------------------