Ver Fonte

Merge branch 'master' of gitlab.com:bebeco/AutoMate

Tomi Cvetic há 8 anos atrás
pai
commit
c28fe8debe

+ 4 - 0
.babelrc

@@ -0,0 +1,4 @@
+{
+  "plugins": [["transform-object-rest-spread", {"useBuiltIns": true}]],
+  "presets": ["latest"]
+}

+ 8 - 0
package.json

@@ -20,9 +20,17 @@
   },
   "devDependencies": {
     "autoprefixer-stylus": "0.13.0",
+    "babel-cli": "^6.24.0",
+    "babel-plugin-syntax-object-rest-spread": "^6.13.0",
+    "babel-plugin-transform-es2015-spread": "^6.22.0",
+    "babel-plugin-transform-object-rest-spread": "^6.23.0",
+    "babel-preset-es2015": "^6.24.0",
+    "babel-preset-es2016": "^6.22.0",
+    "babel-preset-latest": "^6.24.0",
     "body-parser": "^1.17.1",
     "concurrently": "3.4.0",
     "jsdoc": "^3.4.3",
+    "node-babel": "^0.1.2",
     "react-scripts": "0.9.5",
     "stylus": "0.54.5"
   },

+ 1 - 13
public/index.html

@@ -15,17 +15,5 @@
     -->
     <title>React App</title>
   </head>
-  <body>
-    <div id="root"></div>
-    <!--
-      This HTML file is a template.
-      If you open it directly in the browser, you will see an empty page.
-
-      You can add webfonts, meta tags, or analytics to this file.
-      The build step will place the bundled scripts into the <body> tag.
-
-      To begin the development, run `npm start`.
-      To create a production bundle, use `npm run build`.
-    -->
-  </body>
+  <body id="root"></body>
 </html>

+ 37 - 0
server/basicSchema.js

@@ -0,0 +1,37 @@
+/** History requirement
+- Document can have embedded sub-documents
+- Document can have referenced documents
+- Document can be referenced by documents
+
+- On save:
+  * copy current version
+  *
+*/
+import { Schema } from 'mongoose'
+
+const historySchema = new Schema({
+  author: [Schema.Types.ObjectId],
+  created: Date,
+  version: Number,
+  tag: String,
+  reference: [Schema.Types.ObjectId]
+})
+
+const basicSchema = {
+  name: {
+    type: String,
+    maxlength: 20,
+    required: true },
+  tag: {
+    type: String,
+    maxlength: 10,
+    required: true },
+  description: {
+    type: String,
+    maxlength: 200 }
+}
+
+export const collection = {
+  ...basicSchema,
+  __history: [historySchema]
+}

+ 66 - 0
server/index.html

@@ -0,0 +1,66 @@
+<!doctype html>
+<html><head>
+    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
+    <script src="http://cdn.jsdelivr.net/sockjs/1.0.1/sockjs.min.js"></script>
+    <style>
+      .box {
+          width: 300px;
+          float: left;
+          margin: 0 20px 0 20px;
+      }
+      .box div, .box input {
+          border: 1px solid;
+          -moz-border-radius: 4px;
+          border-radius: 4px;
+          width: 100%;
+          padding: 0px;
+          margin: 5px;
+      }
+      .box div {
+          border-color: grey;
+          height: 300px;
+          overflow: auto;
+      }
+      .box input {
+          height: 30px;
+      }
+      h1 {
+          margin-left: 30px;
+      }
+      body {
+          background-color: #F0F0F0;
+          font-family: "Arial";
+      }
+    </style>
+</head><body lang="en">
+    <h1>SockJS Express example</h1>
+
+    <div id="first" class="box">
+      <div></div>
+      <form><input autocomplete="off" value="Type here..."></input></form>
+    </div>
+
+    <script>
+        var sockjs_url = '/echo';
+        var sockjs = new SockJS(sockjs_url);
+        $('#first input').focus();
+        var div  = $('#first div');
+        var inp  = $('#first input');
+        var form = $('#first form');
+        var print = function(m, p) {
+            p = (p === undefined) ? '' : JSON.stringify(p);
+            div.append($("<code>").text(m + ' ' + p));
+            div.append($("<br>"));
+            div.scrollTop(div.scrollTop()+10000);
+        };
+        sockjs.onopen    = function()  {print('[*] open', sockjs.protocol);};
+        sockjs.onmessage = function(e) {print('[.] message', e.data);};
+        sockjs.onclose   = function()  {print('[*] close');};
+        form.submit(function() {
+            print('[ ] sending', inp.val());
+            sockjs.send(inp.val());
+            inp.val('');
+            return false;
+        });
+    </script>
+</body></html>

+ 39 - 7
server/index.js

@@ -1,21 +1,53 @@
 const express = require('express')
 const bodyParser = require('body-parser')
+const sockjs = require('sockjs')
+const http = require('http')
 const mongoose = require('mongoose')
-const movies = require('./routes/movies')
 
+/** Load the submodules */
+import project from './project/route'
+
+/** Create the express app */
 const app = express()
 
+/** MongoDB middleware */
 const dbName = 'movieDB'
-var connectionString = `mongodb://localhost:27017/${dbName}`
+const connectionString = `mongodb://localhost:27017/${dbName}`
+
+/** Sockjs middleware */
+const sockjsOpts = {sockjs_url: 'http://cdn.jsdelivr.net/sockjs/1.0.1/sockjs.min.js'}
+const sockjsServer = sockjs.createServer(sockjsOpts)
+sockjsServer.on('connection', function (conn) {
+  conn.on('data', function (message) {
+    conn.write(message)
+  })
+})
 
+/** Bind the http server to express */
+const server = http.createServer(app)
+
+sockjsServer.installHandlers(server, {prefix: '/echo'})
 mongoose.connect(connectionString)
 
+function welcomeRouter (req, res) {
+  res.json({ message: 'Welcome to the AutoMate db API!' })
+}
+function errorRouter (req, res) {
+  res.status(404)
+  res.send({ message: 'Unknown route.' })
+}
+
 app.use(bodyParser.json())
 app.use(bodyParser.urlencoded({ extended: true }))
-app.use('/api', movies)
+app.get(/^\/db\/?$/, welcomeRouter)
+app.get(/^\/db(\/\w+)?(\/\w+)?\/?$/, [project.get])
+app.post(/^\/db(\/\w+)?(\/\w+)?\/?$/, [project.post])
+app.put(/^\/db(\/\w+)?(\/\w+)?\/?$/, [project.put])
+app.delete(/^\/db(\/\w+)?(\/\w+)?\/?$/, [project.delete])
+app.use(/.*/, errorRouter)
 
-app.set('port', process.env.PORT || 4000)
+/* app.get('/', function (req, res) {
+  res.sendFile(`${__dirname}/index.html`)
+}) */
 
-const server = app.listen(app.get('port'), function () {
-  console.log(`Server listening on port ${server.address().port}`)
-})
+server.listen(process.env.PORT || 4000)

+ 14 - 5
server/project/model.js

@@ -1,10 +1,19 @@
-const mongoose = require('mongoose')
+import mongoose from 'mongoose'
+import { collection } from '../basicSchema'
 
-const projectSchema = new mongoose.Schema({
+const metaSchema = new mongoose.Schema({
+  type: String,
   name: String,
-  tag: String,
   description: String,
-  meta: [mongoose.Schema.Types.Mixed]
+  value: String
 })
 
-module.exports = mongoose.model('Project', projectSchema)
+/** Metas are embedded. */
+const projectSchema = new mongoose.Schema({
+  ...collection,
+  meta: [metaSchema]
+})
+
+const Project = mongoose.model('Project', projectSchema)
+
+export default Project

+ 116 - 0
server/project/route.js

@@ -0,0 +1,116 @@
+import Project from './model'
+import mongoose from 'mongoose'
+
+const routing = {
+  /** GET handler (request one or more projects) */
+  get: (req, res, next) => {
+    const path = req.params['0']
+    let id = req.params['1']
+    console.log(req.params)
+    /** If the path doesn't match, call the next router */
+    if (path !== '/project') {
+      next()
+    }
+
+    /** check whether an id was specified. */
+    if (typeof id !== 'undefined') {
+      try {
+        id = mongoose.Types.ObjectId(req.params['1'].substring(1))
+      } catch (err) {
+        res.status(422)
+        res.send({error: err.message})
+        return
+      }
+      console.log(id)
+      /** if yes, return the one project */
+      Project.findOne({ _id: id }, function (err, project) {
+        if (err) {
+          res.status(404)
+          res.send(err)
+          return
+        }
+        res.json(project)
+      })
+    } else {
+      /** if not, return all projects */
+      /** @todo: add some pagination here */
+      /** @todo: add some filtering here */
+      Project.find(function (err, projects) {
+        if (err) {
+          res.status(404)
+          res.send(err)
+          return
+        }
+        res.json(projects)
+      })
+    }
+  },
+
+  /** POST handler (insert new projects into database) */
+  post: (req, res, next) => {
+    const path = req.params['0']
+    // const id = req.params['1']
+    /** If the path doesn't match, call the next router */
+    if (path !== '/project') {
+      next()
+    }
+
+    const project = new Project(req.body)
+
+    project.save(function (err) {
+      if (err) {
+        res.status(422)
+        res.send(err)
+      }
+      res.send({ success: 'Project added.' })
+    })
+  },
+
+  /** PUT handler (update existing project) */
+  put: (req, res, next) => {
+    const path = req.params['0']
+    const id = mongoose.Types.ObjectId(req.params['1'].substring(1))
+    /** If the path doesn't match, call the next router */
+    if (path !== '/project') {
+      next()
+    }
+
+    Project.findOne({ _id: id }, function (err, project) {
+      if (err) {
+        res.status(404)
+        res.send(err)
+      }
+
+      for (let prop in req.body) {
+        project[prop] = req.body[prop]
+      }
+
+      project.save(function (err) {
+        if (err) {
+          res.status(422)
+          res.send(err)
+        }
+        res.json({ message: 'Movie updated.' })
+      })
+    })
+  },
+
+  /** DELETE handler (delete project) */
+  delete: (req, res, next) => {
+    const path = req.params['0']
+    const id = mongoose.Types.ObjectId(req.params['1'].substring(1))
+    /** If the path doesn't match, call the next router */
+    if (path !== '/project') {
+      next()
+    }
+
+    Project.remove({ _id: id }, function (err, project) {
+      if (err) {
+        res.send(err)
+      }
+      res.json({ message: 'Movie deleted.' })
+    })
+  }
+}
+
+export default routing

+ 67 - 0
server/router.js

@@ -0,0 +1,67 @@
+const Project = require('./project')
+const express = require('express')
+const router = express.Router()
+
+// GET all movies
+router.route(/^\/(\w+)\/(\w+)$/).get(function (req, res, next) {
+  Project.find(function (err, movies) {
+    if (err) {
+      return res.send(err)
+    }
+    res.json(movies)
+  })
+})
+/*
+// POST new movie
+router.route('/movies').post(function (req, res) {
+  const movie = new Project(req.body)
+
+  movie.save(function (err) {
+    if (err) {
+      return res.send(err)
+    }
+    res.send({ message: 'Project added.' })
+  })
+})
+
+// PUT update movie
+router.route('/movies/:id').put(function (req, res) {
+  Project.findOne({ _id: req.params.id }, function (err, movie) {
+    if (err) {
+      return res.send(err)
+    }
+
+    for (prop in req.body) {
+      movie[prop] = req.body[prop]
+    }
+
+    movie.save(function (req, res) {
+      if (err) {
+        return res.send(err)
+      }
+      res.json({ message: 'Project updated.' })
+    })
+  })
+})
+
+// GET one movie
+router.route('/movies/:id').get(function (req, res) {
+  Project.findOne({ _id: req.params.id }, function (err, movie) {
+    if (err) {
+      return res.send(err)
+    }
+    res.json(movie)
+  })
+})
+
+// DELETE one
+router.route('/movies/:id').delete(function (req, res) {
+  Project.remove({ _id: req.params.id }, function (err, movie) {
+    if (err) {
+      return res.send(err)
+    }
+    res.json({ message: 'Project deleted.' })
+  })
+}) */
+
+module.exports = router

+ 5 - 8
server/routes/movies.js

@@ -2,7 +2,7 @@ const Movie = require('../models/movie.js')
 const express = require('express')
 const router = express.Router()
 
-// GET
+// GET all movies
 router.route('/movies').get(function (req, res) {
   Movie.find(function (err, movies) {
     if (err) {
@@ -12,12 +12,10 @@ router.route('/movies').get(function (req, res) {
   })
 })
 
-// POST
+// POST new movie
 router.route('/movies').post(function (req, res) {
-  console.log('Body:', req.body)
   const movie = new Movie(req.body)
 
-  console.log('Movie:', movie)
   movie.save(function (err) {
     if (err) {
       return res.send(err)
@@ -26,7 +24,7 @@ router.route('/movies').post(function (req, res) {
   })
 })
 
-// PUT
+// PUT update movie
 router.route('/movies/:id').put(function (req, res) {
   Movie.findOne({ _id: req.params.id }, function (err, movie) {
     if (err) {
@@ -37,7 +35,6 @@ router.route('/movies/:id').put(function (req, res) {
       movie[prop] = req.body[prop]
     }
 
-    console.log('Movie:', movie)
     movie.save(function (req, res) {
       if (err) {
         return res.send(err)
@@ -47,7 +44,7 @@ router.route('/movies/:id').put(function (req, res) {
   })
 })
 
-// GET :id
+// GET one movie
 router.route('/movies/:id').get(function (req, res) {
   Movie.findOne({ _id: req.params.id }, function (err, movie) {
     if (err) {
@@ -57,7 +54,7 @@ router.route('/movies/:id').get(function (req, res) {
   })
 })
 
-// GET :id
+// DELETE one
 router.route('/movies/:id').delete(function (req, res) {
   Movie.remove({ _id: req.params.id }, function (err, movie) {
     if (err) {

+ 10 - 9
src/Main.js

@@ -1,7 +1,8 @@
 import React from 'react'
 import { Link } from 'react-router'
 
-import './css/style.css'
+// import './css/style.css'
+import './css/spectre.css'
 import Navigation from './Navigation'
 
 class Main extends React.Component {
@@ -11,15 +12,15 @@ class Main extends React.Component {
     )
     console.log('Found these child elements:', children)
     return (
-      <div className='app-wrapper'>
-        <div className='app-header'>
-          <h2>AutoMate</h2>
-          <Navigation />
-        </div>
+      <div>
+        <Navigation />
         {children}
-        <div className='app-footer'>
-          <h2>AutoMate footer</h2>
-        </div>
+        <footer className='app-footer'>
+          <section>
+            <h2>AutoMate footer</h2>
+            <p>some text</p>
+          </section>
+        </footer>
       </div>
     )
   }

+ 24 - 10
src/MetaData.js

@@ -1,13 +1,14 @@
 import React from 'react'
 
-class MetaInformationForm extends React.Component {
+class MetaDataForm extends React.Component {
   constructor () {
     super()
     this.changeType = this.changeType.bind(this)
   }
 
   changeType (event) {
-    this.type = this.sali
+    console.log(this.sali.value)
+    this.props.data.type = this.sali.value
   }
 
   render () {
@@ -41,22 +42,35 @@ class MetaInformationForm extends React.Component {
         </fieldset>
       )
     }
+    if (typeof data.type === 'undefined') {
+      data.type = 'text'
+    }
 
     return (
       <form>
-        <select value={data.type} ref={(input) => { this.sali = input }} onChange={changeType}>
+        <select value={data.type} ref={(input) => { this.sali = input }} onChange={this.changeType}>
           <option value='text'>Text</option>
           <option value='image'>Image</option>
           <option value='link'>Link</option>
           <option value='file'>File</option>
         </select>
-        {metaElements[type]}
+        {metaElements[data.type]}
       </form>
     )
   }
 }
 
-class MetaInformationDisplay extends React.Component {
+class MetaDataDisplay extends React.Component {
+  constructor () {
+    super()
+    this.changeType = this.changeType.bind(this)
+  }
+
+  changeType (event) {
+    console.log(event, this.sali)
+    this.props.data.type = this.sali.value
+  }
+
   render () {
     const { data } = this.props
     const metaElements = {
@@ -90,7 +104,7 @@ class MetaInformationDisplay extends React.Component {
 
     return (
       <form>
-        <select value={data.type} ref={(input) => { this.sali = input }} onChange={changeType}>
+        <select value={data.type} ref={(input) => { this.sali = input }} onChange={this.changeType}>
           <option value='text'>Text</option>
           <option value='image'>Image</option>
           <option value='link'>Link</option>
@@ -102,18 +116,18 @@ class MetaInformationDisplay extends React.Component {
   }
 }
 
-class MetaInformation extends React.Component {
+class MetaData extends React.Component {
   render () {
     const { method, data } = this.props
     if (method === 'form') {
       return (
-        <MetaInformationForm method={method} data={data} />
+        <MetaDataForm method={method} data={data} />
       )
     }
     return (
-      <MetaInformationDisplay method={method} data={data} />
+      <MetaDataDisplay method={method} data={data} />
     )
   }
 }
 
-export default MetaInformation
+export default MetaData

+ 12 - 8
src/Navigation.js

@@ -3,14 +3,18 @@ import { Link } from 'react-router'
 
 function Navigation () {
   return (
-    <nav className='app-nav'>
-      <ul>
-        <li><Link to='/demo_module'>Demo Module</Link></li>
-        <li><Link to='/project'>Projects</Link></li>
-        <li><Link to='/registermap'>Registermaps</Link></li>
-        <li><Link to='/sample'>Sample</Link></li>
-      </ul>
-    </nav>
+    <header className='navbar bg-gray'>
+      <section className='navbar-section'>
+        <a href='/' className='navbar-brand'><h2>AutoMate</h2></a>
+      </section>
+      <section className='navbar-section'>
+        <Link to='/demo_module' className='btn btn-link'>Demo Module</Link>
+        <Link to='/project' className='btn btn-link'>Projects</Link>
+        <Link to='/registermap' className='btn btn-link'>Registermaps</Link>
+        <Link to='/sample' className='btn btn-link'>Sample</Link>
+        <Link to='/sample' className='btn btn-primary'>Log In</Link>
+      </section>
+    </header>
   )
 }
 

+ 26 - 0
src/auth/AuthService.js

@@ -0,0 +1,26 @@
+/** @todo Need to implement authentication!
+  * https://auth0.com/blog/adding-authentication-to-your-react-flux-app/
+  * https://jwt.io/introduction/
+  */
+
+class AuthService {
+  login (username, password) {
+    return when(req({
+      url: 'http://localhost:3000/sessions/create',
+      method: 'POST',
+      crossOrigin: true,
+      type: 'json',
+      data: {
+        username,
+        password
+      }
+    }))
+    .then((res) => {
+      let jwt = res.id_token
+      LoginActions.loginUser(jwt)
+      return true
+    })
+  }
+}
+
+export default new AuthService()

+ 39 - 0
src/auth/Login.js

@@ -0,0 +1,39 @@
+import React from 'react'
+
+class Login extends React.Component {
+  constructor () {
+    super()
+    this.state = {
+      user: '',
+      password: ''
+    }
+    this.login = this.login.bind(this)
+    this.changeState = this.changeState.bind(this)
+  }
+
+  login (event) {
+    event.preventDefault()
+    Auth.login(this.state.user, this.state.password)
+      .catch((error) => {
+        console.log('Login error:', error)
+      })
+  }
+
+  changeState (event) {
+    this.state[event.target.name] = event.target.value
+  }
+
+  render () {
+    return (
+      <form action='' role='form'>
+        <div className='form-group'>
+          <input type='text' name='user' value={this.state.user} onChange={this.changeState} placeholder='Username' />
+          <input type='password' name='password' value={this.state.password} onChange={this.changeState} placeholder='Password' />
+          <button type='submit' onClick={this.login}>Submit</button>
+        </div>
+      </form>
+    )
+  }
+}
+
+export default Login

+ 10 - 0
src/auth/LoginAction.js

@@ -0,0 +1,10 @@
+export default {
+  loginUser: (jwt) => {
+    RouterContainer.get().transitionTo('/')
+    localStorage.setItem('jwt', jwt)
+    AppDispatcher.dispatch({
+      actionType: 'LOGIN_USER',
+      jwt
+    })
+  }
+}

+ 461 - 0
src/css/normalize.css

@@ -0,0 +1,461 @@
+/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
+
+/**
+ * 1. Change the default font family in all browsers (opinionated).
+ * 2. Correct the line height in all browsers.
+ * 3. Prevent adjustments of font size after orientation changes in
+ *    IE on Windows Phone and in iOS.
+ */
+
+/* Document
+   ========================================================================== */
+
+html {
+  font-family: sans-serif; /* 1 */
+  line-height: 1.15; /* 2 */
+  -ms-text-size-adjust: 100%; /* 3 */
+  -webkit-text-size-adjust: 100%; /* 3 */
+}
+
+/* Sections
+   ========================================================================== */
+
+/**
+ * Remove the margin in all browsers (opinionated).
+ */
+
+body {
+  margin: 0;
+}
+
+/**
+ * Add the correct display in IE 9-.
+ */
+
+article,
+aside,
+footer,
+header,
+nav,
+section {
+  display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+/* Grouping content
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 9-.
+ * 1. Add the correct display in IE.
+ */
+
+figcaption,
+figure,
+main { /* 1 */
+  display: block;
+}
+
+/**
+ * Add the correct margin in IE 8.
+ */
+
+figure {
+  margin: 1em 40px;
+}
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+  box-sizing: content-box; /* 1 */
+  height: 0; /* 1 */
+  overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+   ========================================================================== */
+
+/**
+ * 1. Remove the gray background on active links in IE 10.
+ * 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
+ */
+
+a {
+  background-color: transparent; /* 1 */
+  -webkit-text-decoration-skip: objects; /* 2 */
+}
+
+/**
+ * Remove the outline on focused links when they are also active or hovered
+ * in all browsers (opinionated).
+ */
+
+a:active,
+a:hover {
+  outline-width: 0;
+}
+
+/**
+ * 1. Remove the bottom border in Firefox 39-.
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+  border-bottom: none; /* 1 */
+  text-decoration: underline; /* 2 */
+  text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Prevent the duplicate application of `bolder` by the next rule in Safari 6.
+ */
+
+b,
+strong {
+  font-weight: inherit;
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+  font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font style in Android 4.3-.
+ */
+
+dfn {
+  font-style: italic;
+}
+
+/**
+ * Add the correct background and color in IE 9-.
+ */
+
+mark {
+  background-color: #ff0;
+  color: #000;
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+  font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+sup {
+  top: -0.5em;
+}
+
+/* Embedded content
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 9-.
+ */
+
+audio,
+video {
+  display: inline-block;
+}
+
+/**
+ * Add the correct display in iOS 4-7.
+ */
+
+audio:not([controls]) {
+  display: none;
+  height: 0;
+}
+
+/**
+ * Remove the border on images inside links in IE 10-.
+ */
+
+img {
+  border-style: none;
+}
+
+/**
+ * Hide the overflow in IE.
+ */
+
+svg:not(:root) {
+  overflow: hidden;
+}
+
+/* Forms
+   ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers (opinionated).
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  font-family: sans-serif; /* 1 */
+  font-size: 100%; /* 1 */
+  line-height: 1.15; /* 1 */
+  margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+  overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+  text-transform: none;
+}
+
+/**
+ * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
+ *    controls in Android 4.
+ * 2. Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+html [type="button"], /* 1 */
+[type="reset"],
+[type="submit"] {
+  -webkit-appearance: button; /* 2 */
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+  border-style: none;
+  padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+  outline: 1px dotted ButtonText;
+}
+
+/**
+ * Change the border, margin, and padding in all browsers (opinionated).
+ */
+
+fieldset {
+  border: 1px solid #c0c0c0;
+  margin: 0 2px;
+  padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ *    `fieldset` elements in all browsers.
+ */
+
+legend {
+  box-sizing: border-box; /* 1 */
+  color: inherit; /* 2 */
+  display: table; /* 1 */
+  max-width: 100%; /* 1 */
+  padding: 0; /* 3 */
+  white-space: normal; /* 1 */
+}
+
+/**
+ * 1. Add the correct display in IE 9-.
+ * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+  display: inline-block; /* 1 */
+  vertical-align: baseline; /* 2 */
+}
+
+/**
+ * Remove the default vertical scrollbar in IE.
+ */
+
+textarea {
+  overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10-.
+ * 2. Remove the padding in IE 10-.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+  box-sizing: border-box; /* 1 */
+  padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+  -webkit-appearance: textfield; /* 1 */
+  outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-cancel-button,
+[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+  -webkit-appearance: button; /* 1 */
+  font: inherit; /* 2 */
+}
+
+/* Interactive
+   ========================================================================== */
+
+/*
+ * Add the correct display in IE 9-.
+ * 1. Add the correct display in Edge, IE, and Firefox.
+ */
+
+details, /* 1 */
+menu {
+  display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+  display: list-item;
+}
+
+/* Scripting
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 9-.
+ */
+
+canvas {
+  display: inline-block;
+}
+
+/**
+ * Add the correct display in IE.
+ */
+
+template {
+  display: none;
+}
+
+/* Hidden
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 10-.
+ */
+
+[hidden] {
+  display: none;
+}

+ 2760 - 0
src/css/spectre.css

@@ -0,0 +1,2760 @@
+/*! Spectre.css | MIT License | github.com/picturepan2/spectre */
+/* Manually forked from Normalize.css */
+/* normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
+/**
+ * 1. Change the default font family in all browsers (opinionated).
+ * 2. Correct the line height in all browsers.
+ * 3. Prevent adjustments of font size after orientation changes in
+ *    IE on Windows Phone and in iOS.
+ */
+/* Document
+   ========================================================================== */
+html {
+  font-family: sans-serif;
+  /* 1 */
+  line-height: 1.15;
+  /* 3 */
+  -webkit-text-size-adjust: 100%;
+  /* 2 */
+  -ms-text-size-adjust: 100%;
+  /* 3 */
+}
+/* Sections
+   ========================================================================== */
+/**
+ * Remove the margin in all browsers (opinionated).
+ */
+body {
+  margin: 0;
+}
+/**
+ * Add the correct display in IE 9-.
+ */
+article,
+aside,
+footer,
+header,
+nav,
+section {
+  display: block;
+}
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+h1 {
+  font-size: 2em;
+  margin: .67em 0;
+}
+/* Grouping content
+   ========================================================================== */
+/**
+ * Add the correct display in IE 9-.
+ * 1. Add the correct display in IE.
+ */
+figcaption,
+figure,
+main {
+  /* 1 */
+  display: block;
+}
+/**
+ * Add the correct margin in IE 8 (removed).
+ */
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+hr {
+  box-sizing: content-box;
+  /* 1 */
+  height: 0;
+  /* 1 */
+  overflow: visible;
+  /* 2 */
+}
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers. (removed)
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+/* Text-level semantics
+   ========================================================================== */
+/**
+ * 1. Remove the gray background on active links in IE 10.
+ * 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
+ */
+a {
+  background-color: transparent;
+  /* 1 */
+  -webkit-text-decoration-skip: objects;
+  /* 2 */
+}
+/**
+ * Remove the outline on focused links when they are also active or hovered
+ * in all browsers (opinionated).
+ */
+a:active,
+a:hover {
+  outline-width: 0;
+}
+/**
+ * 1. Remove the bottom border in Firefox 39-.
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. (removed)
+ */
+/**
+ * Prevent the duplicate application of `bolder` by the next rule in Safari 6.
+ */
+b,
+strong {
+  font-weight: inherit;
+}
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+b,
+strong {
+  font-weight: bolder;
+}
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+code,
+kbd,
+pre,
+samp {
+  font-family: monospace, monospace;
+  /* 1 */
+  font-size: 1em;
+  /* 2 */
+}
+/**
+ * Add the correct font style in Android 4.3-.
+ */
+dfn {
+  font-style: italic;
+}
+/**
+ * Add the correct background and color in IE 9-. (Removed)
+ */
+/**
+ * Add the correct font size in all browsers.
+ */
+small {
+  font-size: 80%;
+}
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+sub {
+  bottom: -.25em;
+}
+sup {
+  top: -.5em;
+}
+/* Embedded content
+   ========================================================================== */
+/**
+ * Add the correct display in IE 9-.
+ */
+audio,
+video {
+  display: inline-block;
+}
+/**
+ * Add the correct display in iOS 4-7.
+ */
+audio:not([controls]) {
+  display: none;
+  height: 0;
+}
+/**
+ * Remove the border on images inside links in IE 10-.
+ */
+img {
+  border-style: none;
+}
+/**
+ * Hide the overflow in IE.
+ */
+svg:not(:root) {
+  overflow: hidden;
+}
+/* Forms
+   ========================================================================== */
+/**
+ * 1. Change the font styles in all browsers (opinionated).
+ * 2. Remove the margin in Firefox and Safari.
+ */
+button,
+input,
+optgroup,
+select,
+textarea {
+  font-family: inherit;
+  /* 1 (changed) */
+  font-size: inherit;
+  /* 1 (changed) */
+  line-height: inherit;
+  /* 1 (changed) */
+  margin: 0;
+  /* 2 */
+}
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+button,
+input {
+  /* 1 */
+  overflow: visible;
+}
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+button,
+select {
+  /* 1 */
+  text-transform: none;
+}
+/**
+ * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
+ *    controls in Android 4.
+ * 2. Correct the inability to style clickable types in iOS and Safari.
+ */
+button,
+html [type="button"],
+[type="reset"],
+[type="submit"] {
+  -webkit-appearance: button;
+  /* 2 */
+}
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+  border-style: none;
+  padding: 0;
+}
+/**
+ * Restore the focus styles unset by the previous rule (removed).
+ */
+/**
+ * Change the border, margin, and padding in all browsers (opinionated) (changed).
+ */
+fieldset {
+  border: 0;
+  margin: 0;
+  padding: 0;
+}
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ *    `fieldset` elements in all browsers.
+ */
+legend {
+  box-sizing: border-box;
+  /* 1 */
+  color: inherit;
+  /* 2 */
+  display: table;
+  /* 1 */
+  max-width: 100%;
+  /* 1 */
+  padding: 0;
+  /* 3 */
+  white-space: normal;
+  /* 1 */
+}
+/**
+ * 1. Add the correct display in IE 9-.
+ * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+progress {
+  display: inline-block;
+  /* 1 */
+  vertical-align: baseline;
+  /* 2 */
+}
+/**
+ * Remove the default vertical scrollbar in IE.
+ */
+textarea {
+  overflow: auto;
+}
+/**
+ * 1. Add the correct box sizing in IE 10-.
+ * 2. Remove the padding in IE 10-.
+ */
+[type="checkbox"],
+[type="radio"] {
+  box-sizing: border-box;
+  /* 1 */
+  padding: 0;
+  /* 2 */
+}
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+[type="search"] {
+  -webkit-appearance: textfield;
+  /* 1 */
+  outline-offset: -2px;
+  /* 2 */
+}
+/**
+ * Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
+ */
+[type="search"]::-webkit-search-cancel-button,
+[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+::-webkit-file-upload-button {
+  -webkit-appearance: button;
+  /* 1 */
+  font: inherit;
+  /* 2 */
+}
+/* Interactive
+   ========================================================================== */
+/*
+ * Add the correct display in IE 9-.
+ * 1. Add the correct display in Edge, IE, and Firefox.
+ */
+details,
+menu {
+  display: block;
+}
+/*
+ * Add the correct display in all browsers.
+ */
+summary {
+  display: list-item;
+}
+/* Scripting
+   ========================================================================== */
+/**
+ * Add the correct display in IE 9-.
+ */
+canvas {
+  display: inline-block;
+}
+/**
+ * Add the correct display in IE.
+ */
+template {
+  display: none;
+}
+/* Hidden
+   ========================================================================== */
+/**
+ * Add the correct display in IE 10-.
+ */
+[hidden] {
+  display: none;
+}
+*,
+*::before,
+*::after {
+  box-sizing: inherit;
+}
+html {
+  box-sizing: border-box;
+  font-size: 10px;
+  line-height: 1.42857143;
+  -webkit-tap-highlight-color: transparent;
+}
+body {
+  background: #fff;
+  color: #333;
+  font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
+  font-size: 1.4rem;
+  overflow-x: hidden;
+  text-rendering: optimizeLegibility;
+}
+a {
+  color: #5764c6;
+  text-decoration: none;
+}
+a:focus,
+a:hover,
+a:active,
+a.active {
+  color: #3b49af;
+  text-decoration: underline;
+}
+:focus {
+  box-shadow: 0 0 0 .2rem rgba(87, 100, 198, .15);
+  outline: 0;
+}
+.btn .icon,
+.toast .icon,
+.menu .icon {
+  font-size: 1.3333em;
+  line-height: .8em;
+  vertical-align: -20%;
+}
+.icon-caret {
+  border-left: .4rem solid transparent;
+  border-right: .4rem solid transparent;
+  border-top: .4rem solid currentColor;
+  display: inline-block;
+  height: 0;
+  margin: 0;
+  vertical-align: middle;
+  width: 0;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  color: inherit;
+  font-weight: 300;
+  line-height: 1.2;
+  margin-bottom: 1.5rem;
+  margin-top: 0;
+}
+h1 {
+  font-size: 5rem;
+}
+h2 {
+  font-size: 4rem;
+}
+h3 {
+  font-size: 3rem;
+}
+h4 {
+  font-size: 2.4rem;
+}
+h5 {
+  font-size: 2rem;
+}
+h6 {
+  font-size: 1.6rem;
+}
+p {
+  line-height: 2.4rem;
+  margin: 0 0 1rem;
+}
+a,
+ins,
+u {
+  -webkit-text-decoration-skip: ink edges;
+  text-decoration-skip: ink edges;
+}
+blockquote {
+  border-left: .2rem solid #efefef;
+  margin-left: 0;
+  padding: 1rem 2rem;
+}
+blockquote p:last-child {
+  margin-bottom: 0;
+}
+blockquote cite {
+  color: #999;
+}
+ul,
+ol {
+  margin: 2rem 0 2rem 2rem;
+  padding: 0;
+}
+ul ul,
+ol ul,
+ul ol,
+ol ol {
+  margin: 1.5rem 0 1.5rem 2rem;
+}
+ul li,
+ol li {
+  margin-top: 1rem;
+}
+ul {
+  list-style: disc inside;
+}
+ul ul {
+  list-style-type: circle;
+}
+ol {
+  list-style: decimal inside;
+}
+ol ol {
+  list-style-type: lower-alpha;
+}
+dl dt {
+  font-weight: bold;
+}
+dl dd {
+  margin: .5rem 0 1.5rem 0;
+}
+mark {
+  background: #ffe9b3;
+  border-radius: .2rem;
+  display: inline-block;
+  line-height: 1;
+  padding: .3rem .4rem;
+  vertical-align: baseline;
+}
+kbd {
+  background: #333;
+  border-radius: .2rem;
+  color: #fff;
+  display: inline-block;
+  line-height: 1;
+  padding: .3rem .4rem;
+  vertical-align: baseline;
+}
+abbr[title] {
+  border-bottom: .1rem dotted;
+  cursor: help;
+  text-decoration: none;
+}
+.table {
+  border-collapse: collapse;
+  border-spacing: 0;
+  text-align: left;
+  width: 100%;
+}
+.table.table-striped tbody tr:nth-of-type(odd) {
+  background: #f8f8f8;
+}
+.table.table-hover tbody tr:hover {
+  background: #f0f0f0;
+}
+.table tbody tr.active,
+.table.table-striped tbody tr.active {
+  background: #f0f0f0;
+}
+.table td {
+  border-bottom: .1rem solid #efefef;
+  padding: 1.5rem 1rem;
+}
+.table th {
+  border-bottom: .2rem solid #333;
+  padding: 1.5rem 1rem;
+}
+.btn {
+  -webkit-appearance: none;
+  background: #fff;
+  border: .1rem solid #5764c6;
+  border-radius: .2rem;
+  color: #5764c6;
+  cursor: pointer;
+  display: inline-block;
+  font-size: 1.4rem;
+  height: 3.2rem;
+  line-height: 2rem;
+  padding: .5rem 1.2rem;
+  text-align: center;
+  text-decoration: none;
+  transition: all .2s ease;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+  vertical-align: middle;
+  white-space: nowrap;
+}
+.btn:focus {
+  background: #f7f8fc;
+  text-decoration: none;
+}
+.btn:hover {
+  background: #5764c6;
+  border-color: #4452c0;
+  color: #fff;
+  text-decoration: none;
+}
+.btn:active,
+.btn.active {
+  background: #4452c0;
+  border-color: #3b49af;
+  color: #fff;
+}
+.btn[disabled],
+.btn:disabled,
+.btn.disabled {
+  cursor: default;
+  opacity: .5;
+  pointer-events: none;
+}
+.btn.btn-primary {
+  background: #5764c6;
+  border-color: #4452c0;
+  color: #fff;
+}
+.btn.btn-primary:focus {
+  background: #4452c0;
+  border-color: #3f4eba;
+  color: #fff;
+}
+.btn.btn-primary:hover {
+  background: #3f4eba;
+  border-color: #3946a7;
+  color: #fff;
+}
+.btn.btn-primary:active,
+.btn.btn-primary.active {
+  background: #3d4ab3;
+  border-color: #36429f;
+  color: #fff;
+}
+.btn.btn-primary.loading::after {
+  border-color: #fff;
+  border-right-color: transparent;
+  border-top-color: transparent;
+}
+.btn.btn-link {
+  background: transparent;
+  border-color: transparent;
+  color: #5764c6;
+}
+.btn.btn-link:focus,
+.btn.btn-link:hover,
+.btn.btn-link:active,
+.btn.btn-link.active {
+  color: #3b49af;
+}
+.btn.btn-sm {
+  font-size: 1.2rem;
+  height: 2.4rem;
+  padding: .1rem .8rem;
+}
+.btn.btn-lg {
+  font-size: 1.8rem;
+  height: 4rem;
+  padding: .9rem 1.5rem;
+}
+.btn.btn-block {
+  display: block;
+  width: 100%;
+}
+.btn.btn-action {
+  padding-left: .2rem;
+  padding-right: .2rem;
+  width: 3.2rem;
+}
+.btn.btn-action.btn-sm {
+  width: 2.4rem;
+}
+.btn.btn-action.btn-lg {
+  width: 4rem;
+}
+.btn.btn-clear {
+  background: transparent;
+  border: 0;
+  color: currentColor;
+  height: 2rem;
+  margin-left: .2rem;
+  margin-right: -.4rem;
+  opacity: .45;
+  padding: 0 .4rem;
+  text-decoration: none;
+  width: 2rem;
+}
+.btn.btn-clear:hover {
+  opacity: .85;
+}
+.btn.btn-clear::before {
+  content: "\00d7";
+  display: inline-block;
+  font-family: sans-serif;
+  font-size: 2rem;
+}
+.btn-group {
+  display: inline-flex;
+  display: -ms-inline-flexbox;
+  display: -webkit-inline-flex;
+  -webkit-flex-wrap: wrap;
+  -ms-flex-wrap: wrap;
+  flex-wrap: wrap;
+}
+.btn-group .btn {
+  -webkit-flex: 1 0 auto;
+  -ms-flex: 1 0 auto;
+  flex: 1 0 auto;
+}
+.btn-group .btn:first-child:not(:last-child) {
+  border-bottom-right-radius: 0;
+  border-top-right-radius: 0;
+}
+.btn-group .btn:not(:first-child):not(:last-child) {
+  border-radius: 0;
+  margin-left: -.1rem;
+}
+.btn-group .btn:last-child:not(:first-child) {
+  border-bottom-left-radius: 0;
+  border-top-left-radius: 0;
+  margin-left: -.1rem;
+}
+.btn-group .btn:hover,
+.btn-group .btn:focus,
+.btn-group .btn:active,
+.btn-group .btn.active {
+  z-index: 9;
+}
+.btn-group.btn-group-block {
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+}
+.form-group:not(:last-child) {
+  margin-bottom: 1rem;
+}
+.form-input {
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  appearance: none;
+  background: #fff;
+  background-image: none;
+  border: .1rem solid #ccc;
+  border-radius: .2rem;
+  color: #333;
+  display: block;
+  font-size: 1.4rem;
+  height: 3.2rem;
+  line-height: 2rem;
+  max-width: 100%;
+  outline: 0;
+  padding: .5rem .8rem;
+  position: relative;
+  transition: all .2s ease;
+  width: 100%;
+}
+.form-input:focus {
+  border-color: #5764c6;
+}
+.form-input.input-sm {
+  font-size: 1.2rem;
+  height: 2.4rem;
+  padding: .1rem .6rem;
+}
+.form-input.input-lg {
+  font-size: 1.6rem;
+  height: 4rem;
+  padding: .9rem .8rem;
+}
+.form-input.input-inline {
+  display: inline-block;
+  vertical-align: middle;
+  width: auto;
+}
+textarea.form-input {
+  height: auto;
+  line-height: 2rem;
+}
+.form-input[type=file] {
+  height: auto;
+}
+.form-input-hint {
+  color: #999;
+  margin-top: .4rem;
+}
+.has-success .form-input-hint,
+.is-success + .form-input-hint {
+  color: #32b643;
+}
+.has-danger .form-input-hint,
+.is-danger + .form-input-hint {
+  color: #e85600;
+}
+.form-label {
+  display: block;
+  line-height: 1.6rem;
+  margin-bottom: .5rem;
+}
+.form-select {
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  appearance: none;
+  border: .1rem solid #ccc;
+  border-radius: .2rem;
+  font-size: 1.4rem;
+  line-height: 2rem;
+  min-width: 18rem;
+  outline: 0;
+  padding: .5rem .8rem;
+  vertical-align: middle;
+}
+.form-select[multiple] option {
+  padding: .2rem .4rem;
+}
+.form-select:not([multiple]) {
+  background: #fff url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAUCAMAAACzvE1FAAAADFBMVEUzMzMzMzMzMzMzMzMKAG/3AAAAA3RSTlMAf4C/aSLHAAAAPElEQVR42q3NMQ4AIAgEQTn//2cLdRKppSGzBYwzVXvznNWs8C58CiussPJj8h6NwgorrKRdTvuV9v16Afn0AYFOB7aYAAAAAElFTkSuQmCC) no-repeat right .75rem center / .8rem 1rem;
+  height: 3.2rem;
+  padding-right: 2.4rem;
+}
+.form-select:focus {
+  border-color: #5764c6;
+}
+.form-select::-ms-expand {
+  display: none;
+}
+.form-select.select-sm {
+  font-size: 1.2rem;
+  height: 2.4rem;
+  padding: .1rem 2rem .1rem .6rem;
+}
+.form-select.select-lg {
+  font-size: 1.6rem;
+  height: 4rem;
+  padding: .9rem 2.4rem .9rem .8rem;
+}
+.has-success .form-input,
+.has-success .form-select,
+.form-input.is-success,
+.form-select.is-success {
+  border-color: #32b643;
+}
+.has-success .form-input:focus,
+.has-success .form-select:focus,
+.form-input.is-success:focus,
+.form-select.is-success:focus {
+  box-shadow: 0 0 0 .2rem rgba(50, 182, 67, .15);
+}
+.has-danger .form-input,
+.has-danger .form-select,
+.form-input.is-danger,
+.form-select.is-danger {
+  border-color: #e85600;
+}
+.has-danger .form-input:focus,
+.has-danger .form-select:focus,
+.form-input.is-danger:focus,
+.form-select.is-danger:focus {
+  box-shadow: 0 0 0 .2rem rgba(232, 86, 0, .15);
+}
+.form-checkbox input,
+.form-radio input,
+.form-switch input {
+  clip: rect(0, 0, 0, 0);
+  height: .1rem;
+  margin: -.1rem;
+  overflow: hidden;
+  position: absolute;
+  width: .1rem;
+}
+.form-checkbox input:focus + .form-icon,
+.form-radio input:focus + .form-icon,
+.form-switch input:focus + .form-icon {
+  border-color: #5764c6;
+  box-shadow: 0 0 0 .2rem rgba(87, 100, 198, .15);
+}
+.form-checkbox,
+.form-radio {
+  display: inline-block;
+  line-height: 1.8rem;
+  padding: .3rem 2rem;
+  position: relative;
+}
+.form-checkbox .form-icon,
+.form-radio .form-icon {
+  border: .1rem solid #ccc;
+  cursor: pointer;
+  display: inline-block;
+  font-size: 1.4rem;
+  height: 1.4rem;
+  left: 0;
+  line-height: 2.4rem;
+  outline: none;
+  padding: 0;
+  position: absolute;
+  top: .5rem;
+  transition: all .2s ease;
+  vertical-align: top;
+  width: 1.4rem;
+}
+.form-checkbox input:checked + .form-icon,
+.form-radio input:checked + .form-icon {
+  background: #5764c6;
+  border-color: #5764c6;
+}
+.form-checkbox input:active + .form-icon,
+.form-radio input:active + .form-icon {
+  background: #efefef;
+}
+.form-checkbox .form-icon {
+  border-radius: .2rem;
+}
+.form-checkbox input:checked + .form-icon::after {
+  background-clip: padding-box;
+  border: .2rem solid #fff;
+  border-left-width: 0;
+  border-top-width: 0;
+  content: "";
+  height: 1rem;
+  left: 50%;
+  margin-left: -.3rem;
+  margin-top: -.6rem;
+  position: absolute;
+  top: 50%;
+  -webkit-transform: rotate(45deg);
+  -ms-transform: rotate(45deg);
+  transform: rotate(45deg);
+  width: .6rem;
+}
+.form-checkbox input:indeterminate + .form-icon {
+  background: #5764c6;
+  border-color: #5764c6;
+}
+.form-checkbox input:indeterminate + .form-icon::after {
+  background: #fff;
+  content: "";
+  height: .2rem;
+  left: 50%;
+  margin-left: -.4rem;
+  margin-top: -.1rem;
+  position: absolute;
+  top: 50%;
+  width: .8rem;
+}
+.form-radio .form-icon {
+  border-radius: .7rem;
+}
+.form-radio input:checked + .form-icon::after {
+  background: #fff;
+  border-radius: .2rem;
+  content: "";
+  height: .4rem;
+  left: 50%;
+  margin-left: -.2rem;
+  margin-top: -.2rem;
+  position: absolute;
+  top: 50%;
+  width: .4rem;
+}
+.form-switch {
+  display: inline-block;
+  line-height: 2rem;
+  padding: .2rem 2rem .2rem 3.6rem;
+  position: relative;
+}
+.form-switch .form-icon {
+  background: #ccc;
+  background-clip: padding-box;
+  border: .1rem solid #ccc;
+  border-radius: .9rem;
+  cursor: pointer;
+  display: inline-block;
+  height: 1.8rem;
+  left: 0;
+  line-height: 2.6rem;
+  outline: none;
+  padding: 0;
+  position: absolute;
+  top: .3rem;
+  vertical-align: top;
+  width: 3rem;
+}
+.form-switch .form-icon::after {
+  background: #fff;
+  border-radius: .8rem;
+  content: "";
+  display: block;
+  height: 1.6rem;
+  left: 0;
+  position: absolute;
+  top: 0;
+  transition: left .2s ease;
+  width: 1.6rem;
+}
+.form-switch input:checked + .form-icon {
+  background: #5764c6;
+  border-color: #5764c6;
+}
+.form-switch input:checked + .form-icon::after {
+  left: 1.2rem;
+}
+.form-switch input:active + .form-icon::after {
+  background: #efefef;
+}
+.input-group {
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+}
+.input-group .input-group-addon {
+  background: #f8f8f8;
+  border: .1rem solid #ccc;
+  border-radius: .2rem;
+  line-height: 2rem;
+  padding: .5rem .8rem;
+}
+.input-group .input-group-addon.addon-sm {
+  font-size: 1.2rem;
+  padding: .1rem .6rem;
+}
+.input-group .input-group-addon.addon-lg {
+  font-size: 1.6rem;
+  line-height: 2rem;
+  padding: .9rem .8rem;
+}
+.input-group .input-group-addon,
+.input-group .input-group-btn {
+  -webkit-flex: 1 0 auto;
+  -ms-flex: 1 0 auto;
+  flex: 1 0 auto;
+}
+.input-group .form-input:first-child:not(:last-child),
+.input-group .input-group-addon:first-child:not(:last-child),
+.input-group .input-group-btn:first-child:not(:last-child) {
+  border-bottom-right-radius: 0;
+  border-top-right-radius: 0;
+}
+.input-group .form-input:not(:first-child):not(:last-child),
+.input-group .input-group-addon:not(:first-child):not(:last-child),
+.input-group .input-group-btn:not(:first-child):not(:last-child) {
+  border-radius: 0;
+  margin-left: -.1rem;
+}
+.input-group .form-input:last-child:not(:first-child),
+.input-group .input-group-addon:last-child:not(:first-child),
+.input-group .input-group-btn:last-child:not(:first-child) {
+  border-bottom-left-radius: 0;
+  border-top-left-radius: 0;
+  margin-left: -.1rem;
+}
+.input-group .form-input:focus,
+.input-group .input-group-addon:focus,
+.input-group .input-group-btn:focus {
+  z-index: 99;
+}
+.input-group.input-inline {
+  display: inline-flex;
+  display: -ms-inline-flexbox;
+  display: -webkit-inline-flex;
+}
+.form-input:disabled,
+.form-select:disabled,
+.form-input.disabled,
+.form-select.disabled {
+  background-color: #f0f0f0;
+  cursor: not-allowed;
+  opacity: .5;
+}
+input:disabled + .form-icon,
+input.disabled + .form-icon {
+  background: #f0f0f0;
+  border-color: #ccc;
+  cursor: not-allowed;
+  opacity: .5;
+}
+.form-switch input:disabled + .form-icon::after,
+.form-switch input.disabled + .form-icon::after {
+  background: #fff;
+}
+.form-horizontal {
+  padding: 1rem;
+}
+.form-horizontal .form-group {
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+}
+.form-horizontal .form-label {
+  margin-bottom: 0;
+  padding: .8rem .4rem;
+}
+.form-horizontal .form-checkbox,
+.form-horizontal .form-radio,
+.form-horizontal .form-switch {
+  margin: .4rem 0;
+}
+.label {
+  background: #f8f8f8;
+  border-radius: .2rem;
+  color: #666;
+  display: inline-block;
+  line-height: 1;
+  padding: .3rem .4rem;
+  vertical-align: baseline;
+}
+.label.label-primary {
+  background: #5764c6;
+  color: #fff;
+}
+.label.label-success {
+  background: #32b643;
+  color: #fff;
+}
+.label.label-warning {
+  background: #ffb700;
+  color: #fff;
+}
+.label.label-danger {
+  background: #e85600;
+  color: #fff;
+}
+code {
+  background: #f8f8f8;
+  border-radius: .2rem;
+  color: #e06870;
+  display: inline-block;
+  line-height: 1;
+  padding: .3rem .4rem;
+  vertical-align: baseline;
+}
+.code {
+  border-radius: .2rem;
+  color: #666;
+  line-height: 2rem;
+  position: relative;
+}
+.code::before {
+  color: #ccc;
+  content: attr(data-lang);
+  font-size: 1.2rem;
+  position: absolute;
+  right: 1rem;
+  top: .2rem;
+}
+.code code {
+  color: inherit;
+  display: block;
+  line-height: inherit;
+  overflow-x: auto;
+  padding: 2rem;
+  width: 100%;
+}
+.img-responsive {
+  display: block;
+  height: auto;
+  max-width: 100%;
+}
+.video-responsive {
+  display: block;
+  overflow: hidden;
+  padding: 0;
+  position: relative;
+  width: 100%;
+}
+.video-responsive::before {
+  content: "";
+  display: block;
+  padding-bottom: 56.25%;
+  /* Default 16:9, you can calculate this value by dividing 9 by 16 */
+}
+.video-responsive iframe,
+.video-responsive object,
+.video-responsive embed {
+  bottom: 0;
+  height: 100%;
+  left: 0;
+  position: absolute;
+  right: 0;
+  top: 0;
+  width: 100%;
+}
+.video-responsive video {
+  height: auto;
+  max-width: 100%;
+  width: 100%;
+}
+.video-responsive-4-3::before {
+  padding-bottom: 75%;
+  /* 4:3 */
+}
+.video-responsive-1-1::before {
+  padding-bottom: 100%;
+  /* 1:1 */
+}
+.figure {
+  margin: 0 0 1rem 0;
+}
+.figure .figure-caption {
+  color: #666;
+  margin-top: 1rem;
+}
+.container {
+  margin-left: auto;
+  margin-right: auto;
+  padding-left: 1rem;
+  padding-right: 1rem;
+  width: 100%;
+}
+.container.grid-960 {
+  max-width: 98rem;
+}
+.container.grid-480 {
+  max-width: 50rem;
+}
+.columns {
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+  -webkit-flex-wrap: wrap;
+  -ms-flex-wrap: wrap;
+  flex-wrap: wrap;
+  margin-left: -1rem;
+  margin-right: -1rem;
+}
+.columns.col-gapless {
+  margin-left: 0;
+  margin-right: 0;
+}
+.columns.col-gapless .column {
+  padding-left: 0;
+  padding-right: 0;
+}
+.columns.col-oneline {
+  -webkit-flex-wrap: nowrap;
+  -ms-flex-wrap: nowrap;
+  flex-wrap: nowrap;
+  overflow-x: auto;
+}
+.column {
+  -webkit-flex: 1;
+  -ms-flex: 1;
+  flex: 1;
+  padding: 1rem;
+}
+.column.col-12,
+.column.col-11,
+.column.col-10,
+.column.col-9,
+.column.col-8,
+.column.col-7,
+.column.col-6,
+.column.col-5,
+.column.col-4,
+.column.col-3,
+.column.col-2,
+.column.col-1 {
+  -webkit-flex: none;
+  -ms-flex: none;
+  flex: none;
+}
+.col-12 {
+  width: 100%;
+}
+.col-11 {
+  width: 91.66666667%;
+}
+.col-10 {
+  width: 83.33333333%;
+}
+.col-9 {
+  width: 75%;
+}
+.col-8 {
+  width: 66.66666667%;
+}
+.col-7 {
+  width: 58.33333333%;
+}
+.col-6 {
+  width: 50%;
+}
+.col-5 {
+  width: 41.66666667%;
+}
+.col-4 {
+  width: 33.33333333%;
+}
+.col-3 {
+  width: 25%;
+}
+.col-2 {
+  width: 16.66666667%;
+}
+.col-1 {
+  width: 8.33333333%;
+}
+@media screen and (max-width: 1280px) {
+  .col-xl-12,
+  .col-xl-11,
+  .col-xl-10,
+  .col-xl-9,
+  .col-xl-8,
+  .col-xl-7,
+  .col-xl-6,
+  .col-xl-5,
+  .col-xl-4,
+  .col-xl-3,
+  .col-xl-2,
+  .col-xl-1 {
+    -webkit-flex: none;
+    -ms-flex: none;
+    flex: none;
+  }
+  .col-xl-12 {
+    width: 100%;
+  }
+  .col-xl-11 {
+    width: 91.66666667%;
+  }
+  .col-xl-10 {
+    width: 83.33333333%;
+  }
+  .col-xl-9 {
+    width: 75%;
+  }
+  .col-xl-8 {
+    width: 66.66666667%;
+  }
+  .col-xl-7 {
+    width: 58.33333333%;
+  }
+  .col-xl-6 {
+    width: 50%;
+  }
+  .col-xl-5 {
+    width: 41.66666667%;
+  }
+  .col-xl-4 {
+    width: 33.33333333%;
+  }
+  .col-xl-3 {
+    width: 25%;
+  }
+  .col-xl-2 {
+    width: 16.66666667%;
+  }
+  .col-xl-1 {
+    width: 8.33333333%;
+  }
+}
+@media screen and (max-width: 960px) {
+  .col-lg-12,
+  .col-lg-11,
+  .col-lg-10,
+  .col-lg-9,
+  .col-lg-8,
+  .col-lg-7,
+  .col-lg-6,
+  .col-lg-5,
+  .col-lg-4,
+  .col-lg-3,
+  .col-lg-2,
+  .col-lg-1 {
+    -webkit-flex: none;
+    -ms-flex: none;
+    flex: none;
+  }
+  .col-lg-12 {
+    width: 100%;
+  }
+  .col-lg-11 {
+    width: 91.66666667%;
+  }
+  .col-lg-10 {
+    width: 83.33333333%;
+  }
+  .col-lg-9 {
+    width: 75%;
+  }
+  .col-lg-8 {
+    width: 66.66666667%;
+  }
+  .col-lg-7 {
+    width: 58.33333333%;
+  }
+  .col-lg-6 {
+    width: 50%;
+  }
+  .col-lg-5 {
+    width: 41.66666667%;
+  }
+  .col-lg-4 {
+    width: 33.33333333%;
+  }
+  .col-lg-3 {
+    width: 25%;
+  }
+  .col-lg-2 {
+    width: 16.66666667%;
+  }
+  .col-lg-1 {
+    width: 8.33333333%;
+  }
+}
+@media screen and (max-width: 840px) {
+  .col-md-12,
+  .col-md-11,
+  .col-md-10,
+  .col-md-9,
+  .col-md-8,
+  .col-md-7,
+  .col-md-6,
+  .col-md-5,
+  .col-md-4,
+  .col-md-3,
+  .col-md-2,
+  .col-md-1 {
+    -webkit-flex: none;
+    -ms-flex: none;
+    flex: none;
+  }
+  .col-md-12 {
+    width: 100%;
+  }
+  .col-md-11 {
+    width: 91.66666667%;
+  }
+  .col-md-10 {
+    width: 83.33333333%;
+  }
+  .col-md-9 {
+    width: 75%;
+  }
+  .col-md-8 {
+    width: 66.66666667%;
+  }
+  .col-md-7 {
+    width: 58.33333333%;
+  }
+  .col-md-6 {
+    width: 50%;
+  }
+  .col-md-5 {
+    width: 41.66666667%;
+  }
+  .col-md-4 {
+    width: 33.33333333%;
+  }
+  .col-md-3 {
+    width: 25%;
+  }
+  .col-md-2 {
+    width: 16.66666667%;
+  }
+  .col-md-1 {
+    width: 8.33333333%;
+  }
+}
+@media screen and (max-width: 600px) {
+  .col-sm-12,
+  .col-sm-11,
+  .col-sm-10,
+  .col-sm-9,
+  .col-sm-8,
+  .col-sm-7,
+  .col-sm-6,
+  .col-sm-5,
+  .col-sm-4,
+  .col-sm-3,
+  .col-sm-2,
+  .col-sm-1 {
+    -webkit-flex: none;
+    -ms-flex: none;
+    flex: none;
+  }
+  .col-sm-12 {
+    width: 100%;
+  }
+  .col-sm-11 {
+    width: 91.66666667%;
+  }
+  .col-sm-10 {
+    width: 83.33333333%;
+  }
+  .col-sm-9 {
+    width: 75%;
+  }
+  .col-sm-8 {
+    width: 66.66666667%;
+  }
+  .col-sm-7 {
+    width: 58.33333333%;
+  }
+  .col-sm-6 {
+    width: 50%;
+  }
+  .col-sm-5 {
+    width: 41.66666667%;
+  }
+  .col-sm-4 {
+    width: 33.33333333%;
+  }
+  .col-sm-3 {
+    width: 25%;
+  }
+  .col-sm-2 {
+    width: 16.66666667%;
+  }
+  .col-sm-1 {
+    width: 8.33333333%;
+  }
+}
+@media screen and (max-width: 480px) {
+  .col-xs-12,
+  .col-xs-11,
+  .col-xs-10,
+  .col-xs-9,
+  .col-xs-8,
+  .col-xs-7,
+  .col-xs-6,
+  .col-xs-5,
+  .col-xs-4,
+  .col-xs-3,
+  .col-xs-2,
+  .col-xs-1 {
+    -webkit-flex: none;
+    -ms-flex: none;
+    flex: none;
+  }
+  .col-xs-12 {
+    width: 100%;
+  }
+  .col-xs-11 {
+    width: 91.66666667%;
+  }
+  .col-xs-10 {
+    width: 83.33333333%;
+  }
+  .col-xs-9 {
+    width: 75%;
+  }
+  .col-xs-8 {
+    width: 66.66666667%;
+  }
+  .col-xs-7 {
+    width: 58.33333333%;
+  }
+  .col-xs-6 {
+    width: 50%;
+  }
+  .col-xs-5 {
+    width: 41.66666667%;
+  }
+  .col-xs-4 {
+    width: 33.33333333%;
+  }
+  .col-xs-3 {
+    width: 25%;
+  }
+  .col-xs-2 {
+    width: 16.66666667%;
+  }
+  .col-xs-1 {
+    width: 8.33333333%;
+  }
+}
+.show-xs,
+.show-sm,
+.show-md,
+.show-lg,
+.show-xl {
+  display: none !important;
+}
+@media screen and (max-width: 480px) {
+  .hide-xs {
+    display: none !important;
+  }
+  .show-xs {
+    display: block !important;
+  }
+}
+@media screen and (max-width: 600px) {
+  .hide-sm {
+    display: none !important;
+  }
+  .show-sm {
+    display: block !important;
+  }
+}
+@media screen and (max-width: 840px) {
+  .hide-md {
+    display: none !important;
+  }
+  .show-md {
+    display: block !important;
+  }
+}
+@media screen and (max-width: 960px) {
+  .hide-lg {
+    display: none !important;
+  }
+  .show-lg {
+    display: block !important;
+  }
+}
+@media screen and (max-width: 1280px) {
+  .hide-xl {
+    display: none !important;
+  }
+  .show-xl {
+    display: block !important;
+  }
+}
+.navbar {
+  -webkit-align-items: stretch;
+  align-items: stretch;
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+  -ms-flex-align: stretch;
+  -webkit-flex-wrap: wrap;
+  -ms-flex-wrap: wrap;
+  flex-wrap: wrap;
+}
+.navbar .navbar-section {
+  -webkit-align-items: center;
+  align-items: center;
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+  -webkit-flex: 1 0 0;
+  -ms-flex: 1 0 0;
+  flex: 1 0 0;
+  -ms-flex-align: center;
+}
+.navbar .navbar-section:last-of-type {
+  -ms-flex-pack: end;
+  -webkit-justify-content: flex-end;
+  justify-content: flex-end;
+}
+.navbar .navbar-primary {
+  -webkit-align-items: center;
+  align-items: center;
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+  -webkit-flex: 0 0 auto;
+  -ms-flex: 0 0 auto;
+  flex: 0 0 auto;
+  -ms-flex-align: center;
+}
+.navbar .navbar-brand {
+  font-size: 1.6rem;
+  font-weight: 500;
+  text-decoration: none;
+}
+.panel {
+  border: .1rem solid #efefef;
+  border-radius: .2rem;
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+  -webkit-flex-direction: column;
+  -ms-flex-direction: column;
+  flex-direction: column;
+}
+.panel .panel-header,
+.panel .panel-footer {
+  -webkit-flex: 0 0 auto;
+  -ms-flex: 0 0 auto;
+  flex: 0 0 auto;
+  padding: 1.5rem;
+}
+.panel .panel-nav {
+  -webkit-flex: 0 0 auto;
+  -ms-flex: 0 0 auto;
+  flex: 0 0 auto;
+}
+.panel .panel-body {
+  -webkit-flex: 1 1 auto;
+  -ms-flex: 1 1 auto;
+  flex: 1 1 auto;
+  overflow-y: auto;
+}
+.panel .panel-body {
+  padding: 0 1.5rem;
+}
+.panel .panel-title {
+  font-size: 2rem;
+  line-height: 1.4;
+}
+.empty {
+  background: #f8f8f8;
+  border-radius: .2rem;
+  color: #666;
+  padding: 4rem;
+  text-align: center;
+}
+.empty .empty-title,
+.empty .empty-meta {
+  margin: 1rem auto;
+}
+.empty .empty-meta {
+  color: #999;
+}
+.empty .empty-action {
+  margin-top: 1.5rem;
+}
+.form-autocomplete {
+  position: relative;
+}
+.form-autocomplete .form-autocomplete-input {
+  -webkit-align-content: flex-start;
+  align-content: flex-start;
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+  -ms-flex-line-pack: start;
+  -webkit-flex-wrap: wrap;
+  -ms-flex-wrap: wrap;
+  flex-wrap: wrap;
+  height: auto;
+  min-height: 3.6rem;
+  padding: .1rem 0 .1rem .2rem;
+}
+.form-autocomplete .form-autocomplete-input.is-focused {
+  border-color: #5764c6;
+  box-shadow: 0 0 0 .2rem rgba(87, 100, 198, .15);
+}
+.form-autocomplete .form-autocomplete-input .form-input {
+  border-color: transparent;
+  box-shadow: none;
+  display: inline-block;
+  -webkit-flex: 1 0 auto;
+  -ms-flex: 1 0 auto;
+  flex: 1 0 auto;
+  padding: .3rem;
+  width: auto;
+}
+.form-autocomplete mark {
+  font-size: 1;
+  padding: .1em 0;
+}
+.form-autocomplete .menu {
+  left: 0;
+  position: absolute;
+  top: 100%;
+  width: 100%;
+}
+.avatar {
+  background: #5764c6;
+  border-radius: 50%;
+  color: rgba(255, 255, 255, .75);
+  display: inline-block;
+  font-size: 1.4rem;
+  font-weight: 300;
+  height: 3.2rem;
+  line-height: 1;
+  margin: 0;
+  position: relative;
+  vertical-align: middle;
+  width: 3.2rem;
+}
+.avatar.avatar-xs {
+  font-size: 1.4rem;
+  height: 1.6rem;
+  width: 1.6rem;
+}
+.avatar.avatar-sm {
+  font-size: 1rem;
+  height: 2.4rem;
+  width: 2.4rem;
+}
+.avatar.avatar-lg {
+  font-size: 2rem;
+  height: 4.8rem;
+  width: 4.8rem;
+}
+.avatar.avatar-xl {
+  font-size: 2.6rem;
+  height: 6.4rem;
+  width: 6.4rem;
+}
+.avatar img {
+  border-radius: 50%;
+  height: 100%;
+  position: relative;
+  width: 100%;
+  z-index: 99;
+}
+.avatar .avatar-icon {
+  background: #fff;
+  bottom: -.2em;
+  height: 50%;
+  padding: 5%;
+  position: absolute;
+  right: -.2em;
+  width: 50%;
+}
+.avatar[data-initial]::before {
+  color: currentColor;
+  content: attr(data-initial);
+  left: 50%;
+  position: absolute;
+  top: 50%;
+  -webkit-transform: translate(-50%, -50%);
+  -ms-transform: translate(-50%, -50%);
+  transform: translate(-50%, -50%);
+  vertical-align: middle;
+  z-index: 1;
+}
+.avatar.avatar-xs[data-initial]::before {
+  -webkit-transform: translate(-50%, -50%) scale(.5);
+  -ms-transform: translate(-50%, -50%) scale(.5);
+  transform: translate(-50%, -50%) scale(.5);
+}
+.badge {
+  display: inline-block;
+  position: relative;
+}
+.badge[data-badge]::after,
+.badge:not([data-badge])::after {
+  background: #5764c6;
+  background-clip: padding-box;
+  border: .1rem solid #fff;
+  border-radius: 1rem;
+  color: #fff;
+  content: attr(data-badge);
+  display: inline-block;
+  -webkit-transform: translate(-.4rem, -1rem);
+  -ms-transform: translate(-.4rem, -1rem);
+  transform: translate(-.4rem, -1rem);
+}
+.badge[data-badge]::after {
+  font-size: 1.2rem;
+  height: 2rem;
+  line-height: 1.4rem;
+  min-width: 2rem;
+  padding: .2rem .5rem;
+  text-align: center;
+  white-space: nowrap;
+}
+.badge:not([data-badge])::after,
+.badge[data-badge=""]::after {
+  height: .8rem;
+  min-width: .8rem;
+  padding: 0;
+  width: .8rem;
+}
+.badge.btn::after {
+  position: absolute;
+  right: 0;
+  top: 0;
+  -webkit-transform: translate(50%, -50%);
+  -ms-transform: translate(50%, -50%);
+  transform: translate(50%, -50%);
+}
+.bar {
+  background: #efefef;
+  border-radius: .2rem;
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+  -webkit-flex-wrap: nowrap;
+  -ms-flex-wrap: nowrap;
+  flex-wrap: nowrap;
+  height: 1.6rem;
+  width: 100%;
+}
+.bar.bar-sm {
+  height: .4rem;
+}
+.bar .bar-item {
+  background: #5764c6;
+  color: #fff;
+  display: block;
+  -ms-flex-negative: 0;
+  -webkit-flex-shrink: 0;
+  flex-shrink: 0;
+  font-size: 1.2rem;
+  height: 100%;
+  line-height: 1;
+  padding: .2rem 0;
+  text-align: center;
+  width: 0;
+}
+.bar .bar-item:first-of-type {
+  border-bottom-left-radius: .2rem;
+  border-top-left-radius: .2rem;
+}
+.bar .bar-item:last-of-type {
+  border-bottom-right-radius: .2rem;
+  border-top-right-radius: .2rem;
+  -ms-flex-negative: 1;
+  -webkit-flex-shrink: 1;
+  flex-shrink: 1;
+}
+.card {
+  background: #fff;
+  border: .1rem solid #efefef;
+  border-radius: .2rem;
+  display: block;
+  margin: 0;
+  padding: 0;
+  text-align: left;
+}
+.card .card-header,
+.card .card-body,
+.card .card-footer {
+  padding: 1.5rem 1.5rem 0 1.5rem;
+}
+.card .card-header:last-child,
+.card .card-body:last-child,
+.card .card-footer:last-child {
+  padding-bottom: 1.5rem;
+}
+.card .card-image {
+  padding-top: 1.5rem;
+}
+.card .card-image:first-child {
+  padding-top: 0;
+}
+.card .card-image:first-child img {
+  border-top-left-radius: .2rem;
+  border-top-right-radius: .2rem;
+}
+.card .card-image:last-child img {
+  border-bottom-left-radius: .2rem;
+  border-bottom-right-radius: .2rem;
+}
+.card .card-title {
+  font-size: 2rem;
+  line-height: 1.4;
+}
+.card .card-meta {
+  color: #999;
+  font-size: 1.3rem;
+}
+.chip {
+  -webkit-align-items: center;
+  align-items: center;
+  background: #efefef;
+  border-radius: .2rem;
+  color: #666;
+  display: -ms-inline-flexbox;
+  display: inline-flex;
+  display: -webkit-inline-flex;
+  -ms-flex-align: center;
+  height: 3rem;
+  margin: .1rem .2rem .1rem 0;
+  max-width: 100%;
+  padding: .3rem .8rem;
+  text-decoration: none;
+  vertical-align: middle;
+}
+.chip.active {
+  background: #5764c6;
+  color: #fff;
+}
+.chip .avatar {
+  margin-left: -.4rem;
+  margin-right: .4rem;
+}
+.dropdown {
+  display: inline-block;
+  position: relative;
+}
+.dropdown .menu {
+  -webkit-animation: slide-down .2s 1;
+  animation: slide-down .2s 1;
+  display: none;
+  left: 0;
+  position: absolute;
+  top: 100%;
+}
+.dropdown.dropdown-right .menu {
+  left: auto;
+  right: 0;
+}
+.dropdown.active .menu,
+.dropdown .dropdown-toggle:focus + .menu,
+.dropdown .menu:hover {
+  display: block;
+}
+.menu {
+  background: #fff;
+  border-radius: .2rem;
+  box-shadow: 0 .1rem .4rem rgba(0, 0, 0, .3);
+  list-style: none;
+  margin: 0;
+  min-width: 18rem;
+  padding: .8rem;
+  text-align: left;
+  -webkit-transform: translateY(.5rem);
+  -ms-transform: translateY(.5rem);
+  transform: translateY(.5rem);
+  z-index: 999;
+}
+.menu .menu-item {
+  border-radius: .2rem;
+  margin-top: 0;
+  padding: 0 .8rem;
+  text-decoration: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+.menu .menu-item > a {
+  border-radius: .2rem;
+  color: inherit;
+  display: block;
+  line-height: 2.4rem;
+  margin: 0 -.8rem;
+  padding: .4rem .8rem;
+  text-decoration: none;
+}
+.menu .menu-item > a:focus,
+.menu .menu-item > a:hover {
+  color: #5764c6;
+}
+.menu .menu-item > a:active,
+.menu .menu-item > a.active {
+  background: #f7f8fc;
+  color: #5764c6;
+}
+.menu .menu-header {
+  color: #ccc;
+  font-size: 1.2rem;
+  line-height: 2rem;
+  margin-top: 0;
+  padding: .2rem 0;
+  position: relative;
+}
+.menu .menu-header::after {
+  border-bottom: .1rem solid #efefef;
+  content: "";
+  display: block;
+  height: .1rem;
+  position: absolute;
+  top: 50%;
+  width: 100%;
+}
+.menu .menu-header .menu-header-text {
+  background: #fff;
+  display: inline-block;
+  padding: 0 .8rem;
+  position: relative;
+  z-index: 99;
+}
+.menu .menu-badge {
+  float: right;
+  padding: .4rem 0;
+}
+.menu .menu-badge .label {
+  margin: .2rem 0;
+}
+.modal {
+  -webkit-align-items: center;
+  align-items: center;
+  bottom: 0;
+  display: none;
+  -ms-flex-align: center;
+  -ms-flex-pack: center;
+  -webkit-justify-content: center;
+  justify-content: center;
+  left: 0;
+  opacity: 0;
+  overflow: hidden;
+  padding: 1rem;
+  position: fixed;
+  right: 0;
+  top: 0;
+}
+.modal:target,
+.modal.active {
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+  opacity: 1;
+  z-index: 1988;
+}
+.modal:target .modal-overlay,
+.modal.active .modal-overlay {
+  background: rgba(51, 51, 51, .5);
+  bottom: 0;
+  display: block;
+  left: 0;
+  position: absolute;
+  right: 0;
+  top: 0;
+}
+.modal:target .modal-container,
+.modal.active .modal-container {
+  -webkit-animation: slide-down .2s;
+  animation: slide-down .2s;
+  max-width: 64rem;
+}
+.modal.modal-sm .modal-container {
+  max-width: 32rem;
+}
+.modal-container {
+  background: #fff;
+  border-radius: .2rem;
+  box-shadow: 0 .1rem .4rem rgba(0, 0, 0, .3);
+  display: block;
+  margin: 0 auto;
+  padding: 0;
+  text-align: left;
+  z-index: 1988;
+}
+.modal-container .modal-header {
+  padding: 1.5rem;
+}
+.modal-container .modal-header .modal-title {
+  font-size: 1.6rem;
+  margin: 0;
+}
+.modal-container .modal-body {
+  max-height: 50vh;
+  overflow-y: auto;
+  padding: 1.5rem;
+  position: relative;
+}
+.modal-container .modal-footer {
+  padding: 1.5rem;
+  text-align: right;
+}
+.breadcrumb,
+.tab,
+.pagination,
+.nav {
+  list-style: none;
+  margin: .5rem 0;
+}
+.breadcrumb {
+  padding: 1.2rem;
+}
+.breadcrumb .breadcrumb-item {
+  display: inline-block;
+  margin: 0;
+}
+.breadcrumb .breadcrumb-item:last-child {
+  color: #999;
+}
+.breadcrumb .breadcrumb-item:not(:first-child)::before {
+  color: #ccc;
+  content: "/";
+  padding: 0 .4rem;
+}
+.tab {
+  -webkit-align-items: center;
+  align-items: center;
+  border-bottom: .1rem solid #efefef;
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+  -ms-flex-align: center;
+  -webkit-flex-wrap: wrap;
+  -ms-flex-wrap: wrap;
+  flex-wrap: wrap;
+}
+.tab .tab-item {
+  margin-top: 0;
+}
+.tab .tab-item.tab-action {
+  -webkit-flex: 1 0 auto;
+  -ms-flex: 1 0 auto;
+  flex: 1 0 auto;
+  text-align: right;
+}
+.tab .tab-item a {
+  border-bottom: .2rem solid transparent;
+  color: inherit;
+  display: block;
+  margin-bottom: -.1rem;
+  margin-top: 0;
+  padding: .6rem 1.2rem;
+  text-decoration: none;
+}
+.tab .tab-item a:focus,
+.tab .tab-item a:hover {
+  color: #5764c6;
+}
+.tab .tab-item.active a,
+.tab .tab-item a.active {
+  border-bottom-color: #5764c6;
+  color: #5764c6;
+}
+.tab.tab-block .tab-item {
+  -webkit-flex: 1 0 auto;
+  -ms-flex: 1 0 auto;
+  flex: 1 0 auto;
+  text-align: center;
+}
+.tab.tab-block .tab-item .badge[data-badge]::after {
+  position: absolute;
+  right: -.4rem;
+  top: -.4rem;
+  -webkit-transform: translate(0, 0);
+  -ms-transform: translate(0, 0);
+  transform: translate(0, 0);
+}
+.tab:not(.tab-block) .badge {
+  padding-right: 0;
+}
+.pagination {
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+}
+.pagination .page-item {
+  margin: 1rem .1rem;
+}
+.pagination .page-item span {
+  display: inline-block;
+  padding: .6rem .4rem;
+}
+.pagination .page-item a {
+  border-radius: .2rem;
+  color: #666;
+  display: inline-block;
+  padding: .6rem .8rem;
+  text-decoration: none;
+}
+.pagination .page-item a:focus,
+.pagination .page-item a:hover {
+  color: #5764c6;
+}
+.pagination .page-item a[disabled],
+.pagination .page-item a.disabled {
+  cursor: default;
+  opacity: .5;
+  pointer-events: none;
+}
+.pagination .page-item.active a {
+  background: #5764c6;
+  color: #fff;
+}
+.pagination .page-item.page-prev,
+.pagination .page-item.page-next {
+  -webkit-flex: 1 0 50%;
+  -ms-flex: 1 0 50%;
+  flex: 1 0 50%;
+}
+.pagination .page-item.page-next {
+  text-align: right;
+}
+.pagination .page-item .page-item-title {
+  margin: 0;
+}
+.pagination .page-item .page-item-meta {
+  margin: 0;
+  opacity: .5;
+}
+.nav {
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+  -webkit-flex-direction: column;
+  -ms-flex-direction: column;
+  flex-direction: column;
+}
+.nav .nav-item a {
+  color: #666;
+  padding: .6rem .8rem;
+  text-decoration: none;
+}
+.nav .nav-item a:focus,
+.nav .nav-item a:hover {
+  color: #5764c6;
+}
+.nav .nav-item.active > a {
+  color: #666;
+  font-weight: bold;
+}
+.nav .nav-item.active > a:focus,
+.nav .nav-item.active > a:hover {
+  color: #5764c6;
+}
+.nav .nav {
+  margin-bottom: 1rem;
+  margin-left: 2rem;
+}
+.nav .nav a {
+  color: #999;
+}
+.step {
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+  -webkit-flex-wrap: nowrap;
+  -ms-flex-wrap: nowrap;
+  flex-wrap: nowrap;
+  list-style: none;
+  margin: .5rem 0;
+  width: 100%;
+}
+.step .step-item {
+  -webkit-flex: 1 1 0;
+  -ms-flex: 1 1 0;
+  flex: 1 1 0;
+  margin-top: 0;
+  min-height: 2rem;
+  position: relative;
+  text-align: center;
+}
+.step .step-item:not(:first-child)::before {
+  background: #5764c6;
+  content: "";
+  height: .2rem;
+  left: -50%;
+  position: absolute;
+  top: .9rem;
+  width: 100%;
+}
+.step .step-item a {
+  color: #999;
+  display: inline-block;
+  padding: 2rem 1rem 0;
+  text-decoration: none;
+}
+.step .step-item a::before {
+  background: #5764c6;
+  border: .2rem solid #fff;
+  border-radius: 50%;
+  content: "";
+  display: block;
+  height: 1.2rem;
+  left: 50%;
+  position: absolute;
+  top: .4rem;
+  -webkit-transform: translateX(-50%);
+  -ms-transform: translateX(-50%);
+  transform: translateX(-50%);
+  width: 1.2rem;
+  z-index: 99;
+}
+.step .step-item.active a::before {
+  background: #fff;
+  border: .2rem solid #5764c6;
+}
+.step .step-item.active ~ .step-item::before {
+  background: #efefef;
+}
+.step .step-item.active ~ .step-item a::before {
+  background: #ccc;
+}
+.tile {
+  -webkit-align-content: space-between;
+  align-content: space-between;
+  -webkit-align-items: flex-start;
+  align-items: flex-start;
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+  -ms-flex-align: start;
+  -ms-flex-line-pack: justify;
+  margin: .5rem 0;
+  padding: .5rem 0;
+}
+.tile .tile-icon,
+.tile .tile-action {
+  -webkit-flex: 0 0 auto;
+  -ms-flex: 0 0 auto;
+  flex: 0 0 auto;
+}
+.tile .tile-content {
+  -webkit-flex: 1 1 auto;
+  -ms-flex: 1 1 auto;
+  flex: 1 1 auto;
+}
+.tile .tile-content:not(:first-child) {
+  padding-left: 1rem;
+}
+.tile .tile-content:not(:last-child) {
+  padding-right: 1rem;
+}
+.tile .tile-title {
+  font-weight: 500;
+}
+.tile .tile-meta {
+  color: #999;
+  line-height: 2rem;
+}
+.tile.tile-centered {
+  -webkit-align-items: center;
+  align-items: center;
+  -ms-flex-align: center;
+}
+.tile.tile-centered .tile-content {
+  overflow: hidden;
+}
+.tile.tile-centered .tile-title,
+.tile.tile-centered .tile-meta {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.toast {
+  background: rgba(51, 51, 51, .9);
+  border: .1rem solid #333;
+  border-color: #333;
+  border-radius: .2rem;
+  color: #fff;
+  display: block;
+  padding: 1.4rem;
+  width: 100%;
+}
+.toast.toast-primary {
+  background: rgba(87, 100, 198, .9);
+  border-color: #5764c6;
+}
+.toast.toast-success {
+  background: rgba(50, 182, 67, .9);
+  border-color: #32b643;
+}
+.toast.toast-danger {
+  background: rgba(232, 86, 0, .9);
+  border-color: #e85600;
+}
+.toast a {
+  color: #fff;
+  text-decoration: underline;
+}
+.toast a:hover,
+.toast a:focus,
+.toast a:active {
+  opacity: .75;
+}
+.tooltip {
+  position: relative;
+}
+.tooltip::after {
+  background: rgba(51, 51, 51, .9);
+  border-radius: .2rem;
+  bottom: 100%;
+  color: #fff;
+  content: attr(data-tooltip);
+  display: block;
+  font-size: 1.2rem;
+  left: 50%;
+  line-height: 1.6rem;
+  max-width: 32rem;
+  opacity: 0;
+  overflow: hidden;
+  padding: .6rem 1rem;
+  pointer-events: none;
+  position: absolute;
+  text-overflow: ellipsis;
+  -webkit-transform: translate(-50%, 0);
+  -ms-transform: translate(-50%, 0);
+  transform: translate(-50%, 0);
+  transition: transform .2s ease, -webkit-transform .2s ease;
+  transition: transform .2s ease;
+  transition: -webkit-transform .2s ease;
+  white-space: nowrap;
+  z-index: 999;
+}
+.tooltip:focus::after,
+.tooltip:hover::after {
+  opacity: 1;
+  -webkit-transform: translate(-50%, -.5rem);
+  -ms-transform: translate(-50%, -.5rem);
+  transform: translate(-50%, -.5rem);
+}
+.tooltip[disabled],
+.tooltip.disabled {
+  pointer-events: auto;
+}
+.tooltip.tooltip-right::after {
+  bottom: 50%;
+  left: 100%;
+  -webkit-transform: translate(0, 50%);
+  -ms-transform: translate(0, 50%);
+  transform: translate(0, 50%);
+}
+.tooltip.tooltip-right:focus::after,
+.tooltip.tooltip-right:hover::after {
+  -webkit-transform: translate(.5rem, 50%);
+  -ms-transform: translate(.5rem, 50%);
+  transform: translate(.5rem, 50%);
+}
+.tooltip.tooltip-bottom::after {
+  bottom: auto;
+  top: 100%;
+  -webkit-transform: translate(-50%, 0);
+  -ms-transform: translate(-50%, 0);
+  transform: translate(-50%, 0);
+}
+.tooltip.tooltip-bottom:focus::after,
+.tooltip.tooltip-bottom:hover::after {
+  -webkit-transform: translate(-50%, .5rem);
+  -ms-transform: translate(-50%, .5rem);
+  transform: translate(-50%, .5rem);
+}
+.tooltip.tooltip-left::after {
+  bottom: 50%;
+  left: auto;
+  right: 100%;
+  -webkit-transform: translate(0, 50%);
+  -ms-transform: translate(0, 50%);
+  transform: translate(0, 50%);
+}
+.tooltip.tooltip-left:focus::after,
+.tooltip.tooltip-left:hover::after {
+  -webkit-transform: translate(-.5rem, 50%);
+  -ms-transform: translate(-.5rem, 50%);
+  transform: translate(-.5rem, 50%);
+}
+@-webkit-keyframes loading {
+  0% {
+    -webkit-transform: rotate(0deg);
+    transform: rotate(0deg);
+  }
+  100% {
+    -webkit-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+@keyframes loading {
+  0% {
+    -webkit-transform: rotate(0deg);
+    transform: rotate(0deg);
+  }
+  100% {
+    -webkit-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+@-webkit-keyframes slide-down {
+  0% {
+    opacity: 0;
+    -webkit-transform: translateY(-3rem);
+    transform: translateY(-3rem);
+  }
+  100% {
+    opacity: 1;
+    -webkit-transform: translateY(0);
+    transform: translateY(0);
+  }
+}
+@keyframes slide-down {
+  0% {
+    opacity: 0;
+    -webkit-transform: translateY(-3rem);
+    transform: translateY(-3rem);
+  }
+  100% {
+    opacity: 1;
+    -webkit-transform: translateY(0);
+    transform: translateY(0);
+  }
+}
+.divider {
+  border-bottom: .1rem solid #efefef;
+  display: block;
+  margin: .8rem 0;
+}
+.loading {
+  color: transparent !important;
+  min-height: 1.6rem;
+  pointer-events: none;
+  position: relative;
+}
+.loading::after {
+  -webkit-animation: loading 500ms infinite linear;
+  animation: loading 500ms infinite linear;
+  border: .2rem solid #5764c6;
+  border-radius: .8rem;
+  border-right-color: transparent;
+  border-top-color: transparent;
+  content: "";
+  display: block;
+  height: 1.6rem;
+  left: 50%;
+  margin-left: -.8rem;
+  margin-top: -.8rem;
+  position: absolute;
+  top: 50%;
+  width: 1.6rem;
+}
+.clearfix::after,
+.container::after {
+  clear: both;
+  content: "";
+  display: table;
+}
+.float-left {
+  float: left !important;
+}
+.float-right {
+  float: right !important;
+}
+.rel {
+  position: relative;
+}
+.abs {
+  position: absolute;
+}
+.fixed {
+  position: fixed;
+}
+.centered {
+  display: block;
+  float: none;
+  margin-left: auto;
+  margin-right: auto;
+}
+.mt-10 {
+  margin-top: 1rem;
+}
+.mr-10 {
+  margin-right: 1rem;
+}
+.mb-10 {
+  margin-bottom: 1rem;
+}
+.ml-10 {
+  margin-left: 1rem;
+}
+.mt-5 {
+  margin-top: .5rem;
+}
+.mr-5 {
+  margin-right: .5rem;
+}
+.mb-5 {
+  margin-bottom: .5rem;
+}
+.ml-5 {
+  margin-left: .5rem;
+}
+.pt-10 {
+  padding-top: 1rem;
+}
+.pr-10 {
+  padding-right: 1rem;
+}
+.pb-10 {
+  padding-bottom: 1rem;
+}
+.pl-10 {
+  padding-left: 1rem;
+}
+.pt-5 {
+  padding-top: .5rem;
+}
+.pr-5 {
+  padding-right: .5rem;
+}
+.pb-5 {
+  padding-bottom: .5rem;
+}
+.pl-5 {
+  padding-left: .5rem;
+}
+.block {
+  display: block;
+}
+.inline {
+  display: inline;
+}
+.inline-block {
+  display: inline-block;
+}
+.flex {
+  display: flex;
+  display: -ms-flexbox;
+  display: -webkit-flex;
+}
+.inline-flex {
+  display: inline-flex;
+  display: -ms-inline-flexbox;
+  display: -webkit-inline-flex;
+}
+.hide {
+  display: none !important;
+}
+.visible {
+  visibility: visible;
+}
+.invisible {
+  visibility: hidden;
+}
+.text-hide {
+  background: transparent;
+  border: 0;
+  color: transparent;
+  font-size: 0;
+  line-height: 0;
+  text-shadow: none;
+}
+.text-assistive {
+  border: 0;
+  clip: rect(0, 0, 0, 0);
+  height: .1rem;
+  margin: -.1rem;
+  overflow: hidden;
+  padding: 0;
+  position: absolute;
+  width: .1rem;
+}
+.text-left {
+  text-align: left;
+}
+.text-right {
+  text-align: right;
+}
+.text-center {
+  text-align: center;
+}
+.text-justify {
+  text-align: justify;
+}
+.text-lowercase {
+  text-transform: lowercase;
+}
+.text-uppercase {
+  text-transform: uppercase;
+}
+.text-capitalize {
+  text-transform: capitalize;
+}
+.text-normal {
+  font-weight: normal;
+}
+.text-bold {
+  font-weight: bold;
+}
+.text-italic {
+  font-style: italic;
+}
+.text-large {
+  font-size: 1.2em;
+}
+.text-ellipsis {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.text-clip {
+  overflow: hidden;
+  text-overflow: clip;
+  white-space: nowrap;
+}
+.text-break {
+  -webkit-hyphens: auto;
+  -ms-hyphens: auto;
+  hyphens: auto;
+  word-break: break-word;
+  word-wrap: break-word;
+}
+.hand {
+  cursor: pointer;
+}
+.shadow {
+  box-shadow: 0 .1rem .4rem rgba(0, 0, 0, .3);
+}
+.light-shadow {
+  box-shadow: 0 .1rem .2rem rgba(0, 0, 0, .15);
+}
+.rounded {
+  border-radius: .2rem;
+}
+.circle {
+  border-radius: 50%;
+}

+ 7 - 8
src/css/style.css

@@ -76,22 +76,21 @@ button {
   cursor: pointer;
 }
 /** Appearance */
-@import url("https://fonts.googleapis.com/css?family=Roboto");
+@import url("https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700");
 .clearfix {
   overflow: auto;
   zoom: 1;
 }
 * {
   font-family: 'Roboto', sans-serif;
-  font-size: 18;
+  font-size: 14px;
   box-sizing: border-box;
 }
-h1,
-h2,
-h3,
-h4,
-th {
-  font-family: 'Roboto', sans-serif;
+h1 {
+  font-size: 48px;
+  font-weight: 100;
+  font-style: normal;
+  margin: 0.5em 0 0.3em 0;
 }
 code {
   font-family: 'Source Code Pro', monospace;

+ 13 - 4
src/css/style.styl

@@ -5,7 +5,9 @@
 //@import url('https://fonts.googleapis.com/css?family=Proza+Libre')
 //@import url('https://fonts.googleapis.com/css?family=Source+Code+Pro')
 //@import url('https://fonts.googleapis.com/css?family=Alegreya+Sans')
-@import url('https://fonts.googleapis.com/css?family=Roboto')
+@import url('https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700');
+//@import url('https://fonts.googleapis.com/css?family=News+Cycle')
+//@import url('https://fonts.googleapis.com/css?family=Wire+One');
 
 .clearfix
   overflow auto
@@ -14,15 +16,22 @@
   //font-family 'Mukta Vaani', sans-serif
   //font-family 'Alegreya Sans', sans-serif; 
   font-family 'Roboto', sans-serif
-  font-size 18
+  font-size 14px
   box-sizing border-box
 h1, h2, h3, h4, th
-  font-family 'Roboto', sans-serif
+  //font-family 'Alegreya Sans', sans-serif; 
+  //font-family 'Roboto', sans-serif
+  //font-family 'News Cycle', sans-serif
   //font-family 'Proza Libre', sans-serif
+  //font-family: 'Wire One', sans-serif;
+h1
+  font-size 48px
+  font-weight 100
+  font-style normal
+  margin 0.5em 0 0.3em 0 
 code
   font-family 'Source Code Pro', monospace
 p, input
-  //font-family: 'Alegreya Sans', sans-serif;
   font-family 'Roboto', sans-serif
 
 /** Main layout (Flexbox-Design) */

+ 95 - 12
src/helpers.js

@@ -1,15 +1,4 @@
-export function lockRange (value, nrOfBits) {
-  const lowerValue = 0
-  const upperValue = Math.pow(2, nrOfBits) - 1
-  if (value < lowerValue) {
-    return lowerValue
-  } else if (value > upperValue) {
-    return lowerValue
-  } else {
-    return value
-  }
-}
-
+/** Parse and print engineering notation */
 const prefixMap = {
   'y': 1e-24,
   'z': 1e-21,
@@ -54,3 +43,97 @@ export function parseEng (string) {
   }
   return sign * num * Math.pow(10, exp)
 }
+
+/** Redux helpers */
+import { call, put, takeEvery } from 'redux-sagas/effects'
+
+export function genStuff (name, actionsList = ['create', 'update', 'remove'], sync = true, api = null) {
+  const actionTypes = {}
+  const actions = {}
+
+  let actionList
+  if (!sync) {
+    let tmpActionList = []
+    actionList.forEach(action => {
+      tmpActionList.push(`${action}_request`)
+      tmpActionList.push(`${action}_success`)
+      tmpActionList.push(`${action}_failure`)
+    })
+    actionList = tmpActionList
+  }
+
+  /** Populate the actionTypes and actions */
+  actionList.forEach(action => {
+    const actionType = `${action.toUpperCase()}_${name.toUpperCase()}`
+    const actionName = `${action}${name[0].toUpperCase()}${name.substring(1)}`
+    actionTypes[actionType] = `${name}/${actionType}`
+    actions[actionName] = (id, data) => { return { type: `${name}/${actionType}`, id, data } }
+  })
+
+  const state = []
+
+  function * worker (action) {
+    try {
+      const data = yield call(api, payload)
+      yield put({ type: '??_SUCCESS', data })
+    } catch (error) {
+      yield put({ type: '??_FAILURE', error })
+    }
+  }
+
+  function * watcher (action) {
+    actionList.forEach(action => {
+      yield takeEvery(`${action.toUpperCase()}_REQUEST`, worker)
+    })
+  }
+
+  const reducer = (sync) ? (state = [], action) => {
+    let nextState
+    switch (action.type) {
+      case actionTypes[`CREATE_${name.toUpperCase()}`]:
+        nextState = [ ...state, action.data ]
+        return nextState
+      case actionTypes[`UPDATE_${name.toUpperCase()}`]:
+        nextState = [ ...state.slice(0, action.id), { ...state[action.id], ...action.data }, ...state.slice(action.id + 1) ]
+        return nextState
+      case actionTypes[`REMOVE_${name.toUpperCase()}`]:
+        nextState = [ ...state.slice(0, action.id), ...state.slice(action.id + 1) ]
+        return nextState
+      default:
+        return state
+    }
+  } : (state = [], action) => {
+    let nextState
+    switch (action.type) {
+      case actionTypes[`CREATE_REQUEST_${name.toUpperCase()}`]:
+        worker(action)
+        return nextState
+      case actionTypes[`CREATE_SUCCESS_${name.toUpperCase()}`]:
+        nextState = [ ...state, action.data ]
+        return nextState
+      case actionTypes[`UPDATE_REQUEST_${name.toUpperCase()}`]:
+        nextState = [ ...state.slice(0, action.id), { ...state[action.id], ...action.data }, ...state.slice(action.id + 1) ]
+        return nextState
+      case actionTypes[`UPDATE_SUCCESS_${name.toUpperCase()}`]:
+        nextState = [ ...state.slice(0, action.id), { ...state[action.id], ...action.data }, ...state.slice(action.id + 1) ]
+        return nextState
+      case actionTypes[`REMOVE_REQUEST_${name.toUpperCase()}`]:
+        nextState = [ ...state.slice(0, action.id), ...state.slice(action.id + 1) ]
+        return nextState
+      case actionTypes[`REMOVE_SUCCESS_${name.toUpperCase()}`]:
+        nextState = [ ...state.slice(0, action.id), ...state.slice(action.id + 1) ]
+        return nextState
+      default:
+        return state
+    }
+  }
+
+  return {
+    actionTypes,
+    actions,
+    state,
+    reducer,
+    worker: (sync) ? null : worker,
+    watcher: (sync) ? null : watcher
+  }
+}

+ 12 - 7
src/project/components/EditProject.js

@@ -1,15 +1,20 @@
 import React from 'react'
+import MetaData from 'MetaData'
 
 class EditProjectForm extends React.Component {
   render () {
+    let a = {}
     return (
-      <form className='app-form'>
-        <input type='text' ref={input => { this.projectName = input }} placeholder='Project ID' />
-        <input type='text' ref={input => { this.projectName = input }} placeholder='Project Name' />
-        <input type='text' ref={input => { this.projectName = input }} placeholder='Project Description' />
-        <input type='text' ref={input => { this.projectName = input }} placeholder='Project Manager' />
-        <button type='submit'>Save</button>
-      </form>
+      <div>
+        <form className='app-form'>
+          <input type='text' ref={input => { this.projectName = input }} placeholder='Project ID' />
+          <input type='text' ref={input => { this.projectName = input }} placeholder='Project Name' />
+          <input type='text' ref={input => { this.projectName = input }} placeholder='Project Description' />
+          <input type='text' ref={input => { this.projectName = input }} placeholder='Project Manager' />
+          <button type='submit'>Save</button>
+        </form>
+        <MetaData method='form' data={a} />
+      </div>
     )
   }
 }

+ 1 - 0
src/project/constants.js

@@ -6,3 +6,4 @@
  **/
 
 export const NAME = 'project'
+export const DATA = ['project']

+ 12 - 24
src/project/initialData.js

@@ -2,34 +2,22 @@ export const projectList = [{
   id: 'JU_CSD',
   name: 'Jungfrau',
   description: '9th generation GNSS receiver',
-  manager: 'Thomas Brauner',
-  versions: [
-    { name: 'MPW033C', date: '11.12.2015' },
-    { name: 'Production TO', date: '10.03.2017' }
-  ],
-  documents: [
-    { name: 'Specification', file: 'ju_spec.docx' },
-    { name: 'Pin List', file: 'ju_pin_list.xlsx' },
-    { name: 'Characterization PCB', file: 'ju_char_bord.brd' }
-  ],
-  pictures: [
-    { name: 'Jungfrau Logo', file: 'jungfrau.png' }
+  meta: [
+    { type: 'text', key: 'Project manager', value: 'Thomas Brauner' },
+    { type: 'link', url: 'ju_spec.docx', description: 'Specification' },
+    { type: 'link', url: 'ju_pin_list.xlsx', description: 'Pin List' },
+    { type: 'link', url: 'ju_char_bord.brd', description: 'Characterization PCB' },
+    { type: 'image', url: 'jungfrau.png', description: 'Jungfrau Logo' }
   ]
 }, {
   id: 'TI_CSD',
   name: 'Titlis',
   description: '7th generation GNSS receiver',
-  manager: 'Luca Plutino',
-  versions: [
-    { name: 'MPWA', date: '11.12.2013' },
-    { name: 'Production TO', date: '10.03.2015' }
-  ],
-  documents: [
-    { name: 'Specification', file: 'ti_spec.docx' },
-    { name: 'Pin List', file: 'ti_pin_list.xlsx' },
-    { name: 'Characterization PCB', file: 'ti_char_bord.brd' }
-  ],
-  pictures: [
-    { name: 'Titlis Logo', file: 'titlis.png' }
+  meta: [
+    { type: 'text', key: 'Project manager', value: 'Luca Plutino' },
+    { type: 'link', url: 'ti_spec.docx', description: 'Specification' },
+    { type: 'link', url: 'ti_pin_list.xlsx', description: 'Pin List' },
+    { type: 'link', url: 'ti_char_bord.brd', description: 'Characterization PCB' },
+    { type: 'image', url: 'Titlis.png', description: 'Titlis Logo' }
   ]
 }]

+ 30 - 30
src/project/state.js

@@ -8,46 +8,46 @@
  * - actions
  **/
 
-import { NAME } from './constants'
+import { NAME, DATA } from './constants'
 // import { call, put, takeEvery } from 'redux-saga/effects'
 
 /** actionTypes define what actions are handeled by the reducer. */
-export const actionTypes = {
-  CREATE_REQ: `${NAME}/CREATE_REQ`,
-  UPDATE_REQ: `${NAME}/UPDATE_REQ`,
-  DELETE_REQ: `${NAME}/DELETE_REQ`
-}
-
-/** actions is an object with references to all action creators */
-function createProject (project) {
-  return {
-    type: actionTypes.CREATE_REQ,
-    project
-  }
-}
+const actionTypes = {}
+export const actionCreators = {}
 
-function updateProject (projectId, project) {
-  return {
-    type: actionTypes.UPDATE_REQ,
-    projectId,
-    project
-  }
-}
+// Generate default actionsTypes CREATE, UPDATE, REMOVE
+DATA.forEach((dataItem, idx) => {
+  ['create_request', 'create_success', 'create_fail',
+    'update_request', 'update_success', 'update_fail',
+    'remove_request', 'remove_success', 'remove_fail'].forEach(action => {
+    // The Redux convention is to name action types e.g. demo_module/UPDATE
+    // For action creators, we define here the name e.g. removePrimary(id, data)
+    // where id is the element id and data is the element itself.
+      const actionType = `${action.toUpperCase()}_${dataItem.toUpperCase()}`
+      const actionName = `${action}${dataItem[0].toUpperCase()}${dataItem.substring(1)}`
+      if (idx === 0) {
+        actionCreators[actionName] = (id, data) => { return { type: `${NAME}/${actionType}`, id, data } }
+      } else {
+        actionCreators[actionName] = (primaryId, id, data) => { return { type: `${NAME}/${actionType}`, primaryId, id, data } }
+      }
+      actionTypes[actionType] = `${NAME}/${actionType}`
+    })
+})
 
-function removeProject (projectId) {
-  return {
-    type: actionTypes.REMOVE_REQ,
-    projectId
-  }
-}
-export const actions = { createProject, updateProject, removeProject }
+// Add specific action creators here:
+// actionCreators['loadSamples'] = () => { return { type: `${NAME}/LOAD_SAMPLES` } }
+// actionTypes['LOAD_SAMPLES'] = `${NAME}/LOAD_SAMPLES`
 
 /** state definition */
+/** It is generally easier to not have another object here. */
 export const state = []
+console.log('State state', state)
 
 /** reducer is called by the redux dispatcher and handles all component actions */
 export function reducer (state = {}, action) {
   switch (action.type) {
+    case actionTypes.CREATE_REQUEST:
+
     case actionTypes.CREATE:
       return {
         ...state,
@@ -68,7 +68,7 @@ export function reducer (state = {}, action) {
   }
 }
 
-/** sagas are asynchronous workers (JS generators) to handle the state.
+/** sagas are asynchronous workers (JS generators) to handle the state. */
 // Worker
 export function * incrementAsync () {
   try {
@@ -88,5 +88,5 @@ export function * watchIncrementAsync () {
 function * sagas () {
   yield takeEvery('INCREMENT_REQUEST')
   yield takeEvery('DECREMENT_REQUEST')
-} */
+}
 export const sagas = null

+ 0 - 1
src/registermap/components/ShowRegistermap.js

@@ -2,7 +2,6 @@ import React from 'react'
 import EditRegistermap from './EditRegistermap'
 import EditSetting from './EditSetting'
 import RegisterTable from './RegisterTable'
-import './registermap.css'
 
 class ShowRegistermap extends React.Component {
   constructor () {

+ 0 - 17
src/registermap/components/registermap.css

@@ -1,17 +0,0 @@
-table,
-tbody,
-thead {
-  border-collapse: collapse;
-}
-table tr,
-tbody tr,
-thead tr,
-table th,
-tbody th,
-thead th,
-table td,
-tbody td,
-thead td {
-  border-collapse: collapse;
-  border: 1px solid #dcdef2;
-}

+ 0 - 5
src/registermap/components/registermap.styl

@@ -1,5 +0,0 @@
-table, tbody, thead
-	border-collapse collapse
-	tr, th, td
-		border-collapse collapse
-		border 1px solid #DCDEF2