├── routes ├── index.js └── api.js ├── public ├── stylesheets │ ├── style.css │ ├── normalize.css │ └── skeleton.css ├── javascripts │ ├── components │ │ ├── CodeDisplay.js │ │ └── FieldsetElement.js │ ├── utils.js │ └── script.js └── index.html ├── package.json ├── app.js ├── LICENSE ├── README.md ├── utils.js ├── .gitignore └── bin └── www /routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | /* GET home page. */ 5 | router.get('/', function(req, res, next) { 6 | res.render('index', { title: 'Express' }); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | @import 'normalize.css'; 2 | @import 'skeleton.css'; 3 | * { 4 | box-sizing: border-box; 5 | } 6 | .content-group { 7 | margin-bottom: 10px; 8 | padding: 10px 0; 9 | border-bottom: 1px solid rgb(221, 221, 221); 10 | } 11 | .text-center { 12 | text-align: center; 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "provably-fair-example", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "cookie-parser": "~1.4.4", 10 | "debug": "~2.6.9", 11 | "express": "~4.16.1", 12 | "morgan": "~1.9.1", 13 | "vary": "^1.1.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const cookieParser = require('cookie-parser'); 4 | const logger = require('morgan'); 5 | 6 | const indexRouter = require('./routes/index'); 7 | const apiRouter = require('./routes/api'); 8 | 9 | const app = express(); 10 | 11 | app.use(logger('dev')); 12 | app.use(express.json()); 13 | app.use(express.urlencoded({ extended: false })); 14 | app.use(cookieParser()); 15 | app.use(express.static(path.join(__dirname, 'public'))); 16 | 17 | app.use('/', indexRouter); 18 | app.use('/api', apiRouter); 19 | 20 | module.exports = app; 21 | -------------------------------------------------------------------------------- /public/javascripts/components/CodeDisplay.js: -------------------------------------------------------------------------------- 1 | customElements.define( 2 | 'code-display', 3 | class extends HTMLElement { 4 | constructor() { 5 | super(); 6 | 7 | const template = document.createElement('template'); 8 | template.innerHTML = ` 9 | 18 | 19 | `; 20 | 21 | this.attachShadow({ mode: 'open' }).appendChild( 22 | document.importNode(template.content, true) 23 | ); 24 | } 25 | } 26 | ); 27 | -------------------------------------------------------------------------------- /public/javascripts/components/FieldsetElement.js: -------------------------------------------------------------------------------- 1 | customElements.define( 2 | 'fieldset-element', 3 | class Fieldset extends HTMLElement { 4 | constructor() { 5 | super(); 6 | const template = document.createElement('template'); 7 | template.innerHTML = ` 8 | 20 |
21 | 22 | 23 |
24 | `; 25 | const root = this.attachShadow({ mode: 'open' }); 26 | root.appendChild(template.content.cloneNode(true)); 27 | } 28 | } 29 | ); 30 | -------------------------------------------------------------------------------- /public/javascripts/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a random string with a specified length. 3 | * @param length - String length 4 | * @returns {string} 5 | */ 6 | const randomString = length => { 7 | const availableChars = 8 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 9 | let randomString = ''; 10 | for (let i = 0; i < length; i++) { 11 | randomString += 12 | availableChars[Math.floor(Math.random() * availableChars.length)]; 13 | } 14 | return randomString; 15 | }; 16 | 17 | const getClientSeed = () => { 18 | // try to get the client seed from local storage 19 | let clientSeed = localStorage.getItem('client-seed'); 20 | // if there isn't a client seed set on the client 21 | if (!clientSeed) { 22 | // generate a new client seed 23 | clientSeed = randomString(20); 24 | // set the generated client seed to local storage so it will be stored opn reloading 25 | localStorage.setItem('client-seed', clientSeed); 26 | } 27 | return clientSeed; 28 | }; 29 | const getHashedServerSeed = async () => await $.ajax('/api/hashed-server-seed'); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alexandru Cambose 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Provably fair example 2 | 3 | A provably fair example build with express, and plain javascript. 4 | 5 | ### What is **provably fair** 6 | 7 | A provably fair algorithm allows users to check and analyze the legitimacy of every roll and confirm that the server/house did not alter them in their favor. By using a provably fair algorithm to generate random numbers neither players nor the operator of the site can know the result of the game before it starts. 8 | 9 | For more information about the techincal details of the implementation check out this article 10 | 11 | [Provably fair system in javascript - What is and how to build a provably fair system in javascript](https://medium.com/@alexcambose/provably-fair-system-in-javascript-6457e028d2aa) 12 | 13 | ## Getting Started 14 | 15 | ### Prerequisites 16 | 17 | Nodejs 8+ installed 18 | 19 | ### Installation 20 | 21 | [Clone](https://git-scm.com/docs/git-clone) this repo 22 | 23 | ``` 24 | $ npm install 25 | ``` 26 | 27 | ## How to use? 28 | 29 | ``` 30 | $ npm run start 31 | ``` 32 | 33 | Open you favourite browser on [localhost:3000](http://localhost:3000/). 34 | 35 | ## License 36 | 37 | This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details 38 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | /** 3 | * Hashed a string 4 | * @param string 5 | * @returns {string} 6 | */ 7 | const sha512 = string => 8 | crypto 9 | .createHash('sha512') 10 | .update(string) 11 | .digest('hex'); 12 | /** 13 | * Generate a 256 characters string 14 | * @returns {string} 15 | */ 16 | const generateServerSeed = () => crypto.randomBytes(256).toString('hex'); 17 | /** 18 | * Custom function for concatenating server-seed, client-seed and nonce. 19 | * @param serverSeed 20 | * @param clientSeed 21 | * @param nonce 22 | * @returns {*} 23 | */ 24 | const combine = (serverSeed, clientSeed, nonce) => 25 | serverSeed + clientSeed + nonce; 26 | /** 27 | * Converts a hex hash into a number between [0, 99] 28 | * @param hashedValue 29 | * @returns {*} 30 | */ 31 | const getResult = hashedValue => { 32 | // the offset of the interval 33 | let index = 0; 34 | // result variable 35 | let result; 36 | 37 | do { 38 | // get the decimal value from an interval of 5 hex letters 39 | result = parseInt(hashedValue.substring(index * 5, index * 5 + 5), 16); 40 | // increment the offset in case we will need to repeat the operation above 41 | index += 1; 42 | // if all the numbers were over 999999 and we reached the end of the string, we set that to a default value of 9999 (99 as a result) 43 | if (index * 5 + 5 > 129) { 44 | result = 9999; 45 | break; 46 | } 47 | } while (result >= 1e6); 48 | // the result is between 0-999999 and we need to convert if into a 4 digit number 49 | // we a apply a modulus of 1000 and the 4 digit number is further split into a 2 digit number with decimals 50 | return [result % 1e4] * 1e-2; 51 | }; 52 | 53 | module.exports = { generateServerSeed, sha512, combine, getResult }; 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment constiables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # next.js build output 79 | .next 80 | 81 | # nuxt.js build output 82 | .nuxt 83 | 84 | # gatsby files 85 | .cache/ 86 | #public 87 | 88 | # vuepress build output 89 | .vuepress/dist 90 | 91 | # Serverless directories 92 | .serverless/ 93 | 94 | # FuseBox cache 95 | .fusebox/ 96 | 97 | # DynamoDB Local files 98 | .dynamodb/ 99 | .idea 100 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const app = require('../app'); 8 | const debug = require('debug')('provably-fair-example:server'); 9 | const http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | const port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | const server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | const port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; 62 | 63 | // handle specific listen errors with friendly messages 64 | switch (error.code) { 65 | case 'EACCES': 66 | console.error(bind + ' requires elevated privileges'); 67 | process.exit(1); 68 | break; 69 | case 'EADDRINUSE': 70 | console.error(bind + ' is already in use'); 71 | process.exit(1); 72 | break; 73 | default: 74 | throw error; 75 | } 76 | } 77 | 78 | /** 79 | * Event listener for HTTP server "listening" event. 80 | */ 81 | 82 | function onListening() { 83 | const addr = server.address(); 84 | const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; 85 | debug('Listening on ' + bind); 86 | } 87 | -------------------------------------------------------------------------------- /public/javascripts/script.js: -------------------------------------------------------------------------------- 1 | $(document).ready(async () => { 2 | // get hashed server seed when the page is loading for the first time 3 | const hashedServerSeed = await getHashedServerSeed(); 4 | // get client seed for further usages 5 | let clientSeed = getClientSeed(); 6 | 7 | // seleting general page elements 8 | const $hashedServerSeedBox = $('#hashed-server-seed'); 9 | const $clientSeedInput = $('#client-seed-input'); 10 | const $verifyClientSeedInput = $('#verify-client-seed-input'); 11 | const $verifyNonceInput = $('#verify-nonce-input'); 12 | const $verifyServerSeedInput = $('#verify-server-seed-input'); 13 | const $rollResultsContainer = $('#roll-results-container'); 14 | const $rollResult = $('#roll-result'); 15 | const $serverSeed = $('#server-seed'); 16 | const $nonce = $('#nonce'); 17 | const $verifyRollResultsContainer = $('#verify-roll-results-container'); 18 | const $verifyRollResult = $('#verify-roll-result'); 19 | // set hashed server seed so that the client can see this before rolling 20 | $hashedServerSeedBox.text(hashedServerSeed); 21 | // set client seed content 22 | $clientSeedInput.val(clientSeed); 23 | $verifyClientSeedInput.val(clientSeed); 24 | // add on click event to the roll button 25 | $('#roll-buton').on('click', async () => { 26 | const { result, serverSeed, nonce } = await $.ajax({ 27 | url: '/api/result', 28 | data: { 29 | clientSeed, 30 | }, 31 | }); 32 | $rollResultsContainer.show(); 33 | $rollResult.text(result); 34 | $serverSeed.text(serverSeed); 35 | $nonce.text(nonce); 36 | // get and update the server seed box, you can also include this data in the resutl api call so an additional request can be saved 37 | $hashedServerSeedBox.text(await getHashedServerSeed()); 38 | }); 39 | // add on submit event for the verify roll foerm 40 | $('#verify-bet-form').on('submit', async e => { 41 | e.preventDefault(); 42 | // get values from inputs 43 | const clientSeed = $verifyClientSeedInput.val(); 44 | const nonce = $verifyNonceInput.val(); 45 | const serverSeed = $verifyServerSeedInput.val(); 46 | // create an ajax request to the server to compute the values provided into a result 47 | const result = await $.ajax({ 48 | url: '/api/verify', 49 | data: { 50 | clientSeed, 51 | nonce, 52 | serverSeed, 53 | }, 54 | }); 55 | // update html elements to displat the returned result 56 | $verifyRollResultsContainer.show(); 57 | $verifyRollResult.text(result); 58 | }); 59 | // add event handler for the client seed input 60 | $clientSeedInput.on('change', e => { 61 | // update the client seed with the input value when it changes 62 | clientSeed = e.target.value; 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /routes/api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { generateServerSeed, sha512, combine, getResult } = require('../utils'); 4 | // _These are for illustration purposes only_, you shouldn't store values on a node instance as these 5 | // values will be erased when the server is closed and also it doesn't allow for more that one user. 6 | 7 | // Ideally the each user should have their own server-seed stored in their profile and the nonce will 8 | // be the number of the results + 1 9 | 10 | let results = []; 11 | const user = { 12 | serverSeed: '', 13 | }; 14 | /** 15 | * This route is called before the client is creating a new roll, 16 | * it returns a hashed version of the server seed. By using a hashed version 17 | * of it, the client can make sure that this is not going to be changed in 18 | * the future but also cannot get it's initial value (because it's hashed). 19 | * It doesn't return the hashed serverseed in advance, because that would 20 | * allow the client to generate all the rolls in advance. 21 | */ 22 | router.get('/hashed-server-seed', function(req, res, next) { 23 | // save the server seed into the users "table" 24 | user.serverSeed = generateServerSeed(); 25 | // return a hashed version of the server seed so the client can verify that it wasn't altered 26 | res.send(sha512(user.serverSeed)); 27 | }); 28 | 29 | /** 30 | * When called, a random number gets generated 31 | * Query parameters: 32 | * clientSeed - String - A random sequence of characters generated by the client 33 | */ 34 | router.get('/result', function(req, res, next) { 35 | // get current server seed specific to the current user 36 | const { serverSeed } = user; 37 | // get client seed from the request query parameters, the client seed is provided by the client's browser 38 | const { clientSeed } = req.query; 39 | // get nonce by incrementing the number of results 40 | const nonce = results.length + 1; 41 | // RESULT GENERATION 42 | // combine values -> append them to one another, or your custom rule of combining them 43 | const combination = combine(serverSeed, clientSeed, nonce); 44 | // create a result object 45 | const newResult = { 46 | result: getResult(sha512(combination)), 47 | serverSeed, 48 | nonce, 49 | }; 50 | // NEW SERVER SEED 51 | // generate a new server seed 52 | user.serverSeed = generateServerSeed(); 53 | // RESULT RETURNING 54 | // insert into our "database" the new result 55 | results.push(newResult); 56 | // return values, the result and the plain server seed with nonce 57 | res.json(newResult); 58 | }); 59 | /** 60 | * The client can now take this value and check that the result did not 61 | * change after the roll was generated. 62 | * 63 | * Query Parameters: 64 | * clientSeed - String - The client seed that was used to generate the result 65 | * serverSeed - String - The server seed that was used to generate the result 66 | * nonce - Number - The nonce of the result 67 | */ 68 | router.get('/verify', function(req, res, next) { 69 | // get the data provided by the client 70 | const { clientSeed, serverSeed, nonce } = req.query; 71 | // create a combination from the provided data 72 | const combination = combine(serverSeed, clientSeed, nonce); 73 | // generate a result from the data 74 | const result = getResult(sha512(combination)); 75 | // return to client 76 | res.send(String(result)); 77 | }); 78 | module.exports = router; 79 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Express 4 | 5 | 9 | 13 | 14 | 15 | 16 |
17 |
18 |
19 | 20 | Roll 21 |
22 | Hashed server seed is: 23 | 24 |
25 |
26 | 27 | 28 |
29 | 32 | 43 |
44 |
45 |
46 | 47 | Verify 48 |
49 |
50 |
51 | 52 | 58 |
59 |
60 | 61 | 67 |
68 |
69 |
70 | 71 | 77 |
78 | 81 |
82 | 85 |
86 |
87 |
88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /public/stylesheets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address constiable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and constiable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } 428 | -------------------------------------------------------------------------------- /public/stylesheets/skeleton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/29/2014 8 | */ 9 | 10 | 11 | /* Table of contents 12 | –––––––––––––––––––––––––––––––––––––––––––––––––– 13 | - Grid 14 | - Base Styles 15 | - Typography 16 | - Links 17 | - Buttons 18 | - Forms 19 | - Lists 20 | - Code 21 | - Tables 22 | - Spacing 23 | - Utilities 24 | - Clearing 25 | - Media Queries 26 | */ 27 | 28 | 29 | /* Grid 30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 31 | .container { 32 | position: relative; 33 | width: 100%; 34 | max-width: 960px; 35 | margin: 0 auto; 36 | padding: 0 20px; 37 | box-sizing: border-box; } 38 | .column, 39 | .columns { 40 | width: 100%; 41 | float: left; 42 | box-sizing: border-box; } 43 | 44 | /* For devices larger than 400px */ 45 | @media (min-width: 400px) { 46 | .container { 47 | width: 85%; 48 | padding: 0; } 49 | } 50 | 51 | /* For devices larger than 550px */ 52 | @media (min-width: 550px) { 53 | .container { 54 | width: 80%; } 55 | .column, 56 | .columns { 57 | margin-left: 4%; } 58 | .column:first-child, 59 | .columns:first-child { 60 | margin-left: 0; } 61 | 62 | .one.column, 63 | .one.columns { width: 4.66666666667%; } 64 | .two.columns { width: 13.3333333333%; } 65 | .three.columns { width: 22%; } 66 | .four.columns { width: 30.6666666667%; } 67 | .five.columns { width: 39.3333333333%; } 68 | .six.columns { width: 48%; } 69 | .seven.columns { width: 56.6666666667%; } 70 | .eight.columns { width: 65.3333333333%; } 71 | .nine.columns { width: 74.0%; } 72 | .ten.columns { width: 82.6666666667%; } 73 | .eleven.columns { width: 91.3333333333%; } 74 | .twelve.columns { width: 100%; margin-left: 0; } 75 | 76 | .one-third.column { width: 30.6666666667%; } 77 | .two-thirds.column { width: 65.3333333333%; } 78 | 79 | .one-half.column { width: 48%; } 80 | 81 | /* Offsets */ 82 | .offset-by-one.column, 83 | .offset-by-one.columns { margin-left: 8.66666666667%; } 84 | .offset-by-two.column, 85 | .offset-by-two.columns { margin-left: 17.3333333333%; } 86 | .offset-by-three.column, 87 | .offset-by-three.columns { margin-left: 26%; } 88 | .offset-by-four.column, 89 | .offset-by-four.columns { margin-left: 34.6666666667%; } 90 | .offset-by-five.column, 91 | .offset-by-five.columns { margin-left: 43.3333333333%; } 92 | .offset-by-six.column, 93 | .offset-by-six.columns { margin-left: 52%; } 94 | .offset-by-seven.column, 95 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 96 | .offset-by-eight.column, 97 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 98 | .offset-by-nine.column, 99 | .offset-by-nine.columns { margin-left: 78.0%; } 100 | .offset-by-ten.column, 101 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 102 | .offset-by-eleven.column, 103 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 104 | 105 | .offset-by-one-third.column, 106 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 107 | .offset-by-two-thirds.column, 108 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 109 | 110 | .offset-by-one-half.column, 111 | .offset-by-one-half.columns { margin-left: 52%; } 112 | 113 | } 114 | 115 | 116 | /* Base Styles 117 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 118 | /* NOTE 119 | html is set to 62.5% so that all the REM measurements throughout Skeleton 120 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 121 | html { 122 | font-size: 62.5%; } 123 | body { 124 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 125 | line-height: 1.6; 126 | font-weight: 400; 127 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 128 | color: #222; } 129 | 130 | 131 | /* Typography 132 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 133 | h1, h2, h3, h4, h5, h6 { 134 | margin-top: 0; 135 | margin-bottom: 2rem; 136 | font-weight: 300; } 137 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} 138 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } 139 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } 140 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } 141 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } 142 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } 143 | 144 | /* Larger than phablet */ 145 | @media (min-width: 550px) { 146 | h1 { font-size: 5.0rem; } 147 | h2 { font-size: 4.2rem; } 148 | h3 { font-size: 3.6rem; } 149 | h4 { font-size: 3.0rem; } 150 | h5 { font-size: 2.4rem; } 151 | h6 { font-size: 1.5rem; } 152 | } 153 | 154 | p { 155 | margin-top: 0; } 156 | 157 | 158 | /* Links 159 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 160 | a { 161 | color: #1EAEDB; } 162 | a:hover { 163 | color: #0FA0CE; } 164 | 165 | 166 | /* Buttons 167 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 168 | .button, 169 | button, 170 | input[type="submit"], 171 | input[type="reset"], 172 | input[type="button"] { 173 | display: inline-block; 174 | height: 38px; 175 | padding: 0 30px; 176 | color: #555; 177 | text-align: center; 178 | font-size: 11px; 179 | font-weight: 600; 180 | line-height: 38px; 181 | letter-spacing: .1rem; 182 | text-transform: uppercase; 183 | text-decoration: none; 184 | white-space: nowrap; 185 | background-color: transparent; 186 | border-radius: 4px; 187 | border: 1px solid #bbb; 188 | cursor: pointer; 189 | box-sizing: border-box; } 190 | .button:hover, 191 | button:hover, 192 | input[type="submit"]:hover, 193 | input[type="reset"]:hover, 194 | input[type="button"]:hover, 195 | .button:focus, 196 | button:focus, 197 | input[type="submit"]:focus, 198 | input[type="reset"]:focus, 199 | input[type="button"]:focus { 200 | color: #333; 201 | border-color: #888; 202 | outline: 0; } 203 | .button.button-primary, 204 | button.button-primary, 205 | input[type="submit"].button-primary, 206 | input[type="reset"].button-primary, 207 | input[type="button"].button-primary { 208 | color: #FFF; 209 | background-color: #33C3F0; 210 | border-color: #33C3F0; } 211 | .button.button-primary:hover, 212 | button.button-primary:hover, 213 | input[type="submit"].button-primary:hover, 214 | input[type="reset"].button-primary:hover, 215 | input[type="button"].button-primary:hover, 216 | .button.button-primary:focus, 217 | button.button-primary:focus, 218 | input[type="submit"].button-primary:focus, 219 | input[type="reset"].button-primary:focus, 220 | input[type="button"].button-primary:focus { 221 | color: #FFF; 222 | background-color: #1EAEDB; 223 | border-color: #1EAEDB; } 224 | 225 | 226 | /* Forms 227 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 228 | input[type="email"], 229 | input[type="number"], 230 | input[type="search"], 231 | input[type="text"], 232 | input[type="tel"], 233 | input[type="url"], 234 | input[type="password"], 235 | textarea, 236 | select { 237 | height: 38px; 238 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 239 | background-color: #fff; 240 | border: 1px solid #D1D1D1; 241 | border-radius: 4px; 242 | box-shadow: none; 243 | box-sizing: border-box; } 244 | /* Removes awkward default styles on some inputs for iOS */ 245 | input[type="email"], 246 | input[type="number"], 247 | input[type="search"], 248 | input[type="text"], 249 | input[type="tel"], 250 | input[type="url"], 251 | input[type="password"], 252 | textarea { 253 | -webkit-appearance: none; 254 | -moz-appearance: none; 255 | appearance: none; } 256 | textarea { 257 | min-height: 65px; 258 | padding-top: 6px; 259 | padding-bottom: 6px; } 260 | input[type="email"]:focus, 261 | input[type="number"]:focus, 262 | input[type="search"]:focus, 263 | input[type="text"]:focus, 264 | input[type="tel"]:focus, 265 | input[type="url"]:focus, 266 | input[type="password"]:focus, 267 | textarea:focus, 268 | select:focus { 269 | border: 1px solid #33C3F0; 270 | outline: 0; } 271 | label, 272 | legend { 273 | display: block; 274 | margin-bottom: .5rem; 275 | font-weight: 600; } 276 | fieldset { 277 | padding: 0; 278 | border-width: 0; } 279 | input[type="checkbox"], 280 | input[type="radio"] { 281 | display: inline; } 282 | label > .label-body { 283 | display: inline-block; 284 | margin-left: .5rem; 285 | font-weight: normal; } 286 | 287 | 288 | /* Lists 289 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 290 | ul { 291 | list-style: circle inside; } 292 | ol { 293 | list-style: decimal inside; } 294 | ol, ul { 295 | padding-left: 0; 296 | margin-top: 0; } 297 | ul ul, 298 | ul ol, 299 | ol ol, 300 | ol ul { 301 | margin: 1.5rem 0 1.5rem 3rem; 302 | font-size: 90%; } 303 | li { 304 | margin-bottom: 1rem; } 305 | 306 | 307 | /* Code 308 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 309 | code { 310 | padding: .2rem .5rem; 311 | margin: 0 .2rem; 312 | font-size: 90%; 313 | white-space: nowrap; 314 | background: #F1F1F1; 315 | border: 1px solid #E1E1E1; 316 | border-radius: 4px; } 317 | pre > code { 318 | display: block; 319 | padding: 1rem 1.5rem; 320 | white-space: pre; } 321 | 322 | 323 | /* Tables 324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 325 | th, 326 | td { 327 | padding: 12px 15px; 328 | text-align: left; 329 | border-bottom: 1px solid #E1E1E1; } 330 | th:first-child, 331 | td:first-child { 332 | padding-left: 0; } 333 | th:last-child, 334 | td:last-child { 335 | padding-right: 0; } 336 | 337 | 338 | /* Spacing 339 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 340 | button, 341 | .button { 342 | margin-bottom: 1rem; } 343 | input, 344 | textarea, 345 | select, 346 | fieldset { 347 | margin-bottom: 1.5rem; } 348 | pre, 349 | blockquote, 350 | dl, 351 | figure, 352 | table, 353 | p, 354 | ul, 355 | ol, 356 | form { 357 | margin-bottom: 2.5rem; } 358 | 359 | 360 | /* Utilities 361 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 362 | .u-full-width { 363 | width: 100%; 364 | box-sizing: border-box; } 365 | .u-max-full-width { 366 | max-width: 100%; 367 | box-sizing: border-box; } 368 | .u-pull-right { 369 | float: right; } 370 | .u-pull-left { 371 | float: left; } 372 | 373 | 374 | /* Misc 375 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 376 | hr { 377 | margin-top: 3rem; 378 | margin-bottom: 3.5rem; 379 | border-width: 0; 380 | border-top: 1px solid #E1E1E1; } 381 | 382 | 383 | /* Clearing 384 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 385 | 386 | /* Self Clearing Goodness */ 387 | .container:after, 388 | .row:after, 389 | .u-cf { 390 | content: ""; 391 | display: table; 392 | clear: both; } 393 | 394 | 395 | /* Media Queries 396 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 397 | /* 398 | Note: The best way to structure the use of media queries is to create the queries 399 | near the relevant code. For example, if you wanted to change the styles for buttons 400 | on small devices, paste the mobile query code up in the buttons section and style it 401 | there. 402 | */ 403 | 404 | 405 | /* Larger than mobile */ 406 | @media (min-width: 400px) {} 407 | 408 | /* Larger than phablet (also point when grid becomes active) */ 409 | @media (min-width: 550px) {} 410 | 411 | /* Larger than tablet */ 412 | @media (min-width: 750px) {} 413 | 414 | /* Larger than desktop */ 415 | @media (min-width: 1000px) {} 416 | 417 | /* Larger than Desktop HD */ 418 | @media (min-width: 1200px) {} 419 | --------------------------------------------------------------------------------