Repository: tscanlin/css-razor
Branch: master
Commit: 4bdf4286e09d
Files: 15
Total size: 25.7 KB
Directory structure:
gitextract_1dfxmu6b/
├── .github/
│ └── FUNDING.yml
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── cli.js
├── defaultOptions.js
├── index.js
├── index.test.js
├── package.json
└── test/
├── input/
│ ├── index.css
│ ├── index.html
│ └── tachyons.html
└── results.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: tscanlin
================================================
FILE: .gitignore
================================================
node_modules
npm-debug*
.DS_Store
test/output
dist/*
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- "6.9"
sudo: false
branches:
only:
- master
install:
- npm install
script:
- npm run test
================================================
FILE: CHANGELOG.md
================================================
### Unreleased
...
### 2.3.0
#### Added
- reportDetails option for all selectors to be listed
- get travis-ci working
- add webpages option
#### Updated
- rename config.js to defaultOptions.js
- refactor tests
- update readme
- ignore option appends by default
### 2.2.0
#### Added
- new overwriteCss option
### 2.1.1
#### Fixed
- fix cli when no output file specified and report improvements
### 2.1.0
#### Added
- More docs & examples
- CHANGELOG.md
- Support for passing glob patterns
### 2.0.0
#### Added
- Added `report` option to display stats about used vs unused selectors
#### Changed
- Rewrite tests with lab and code
- Parse args with yargs
### 1.1.0
#### Added
- Got CLI working
- Promise API
- Tests
- Core functionality
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2017 Tim Scanlin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# css-razor

css-razor is a fast way to remove unused selectors from css. Essentially, it accomplishes the same goal as [uncss](https://github.com/giakki/uncss). However, it accomplishes this goal differently. Rather than loading a webpage in phantomjs and using `document.querySelector` to determine if a selector is being used, css-razor uses [cheeriojs](https://github.com/cheeriojs/cheerio) to parse static html and css files to removed unused selectors.
- Helps trim down CSS so you only keep the necessary parts
- Built for speed using the amazing [cheeriojs](https://github.com/cheeriojs/cheerio)
- has an ignore list that can be added to
- ignores common pseudo elements & pseudo selectors by default
- Supports multiple files / globs
- Supports raw html & css input
- Supports html paths from URLs
- Reporting stats detailing how many selectors are removed.
## Getting Started
Install with npm
```bash
npm install --save-dev css-razor
```
You can then use the cli
```bash
css-razor build/css/index.css build/index.html --stdout > build/css/index.min.css
```
And you can even pass globs
```bash
css-razor build/css/*.css build/*.html --stdout > build/css/index.min.css
```
Or you can use the js api
```js
const cssRazor = require('css-razor').default
cssRazor({
html: ['build/index.html'],
css: ['build/css/index.css'],
}, function(err, data) {
console.log(data.css)
})
```
## Options
```js
module.exports = {
// Array of HTML file globs.
html: [],
// Array of CSS file globs.
css: [],
// Raw HTML string.
htmlRaw: '',
// Raw CSS string.
cssRaw: '',
// Array of webpages to add to HTML.
webpages: [],
// Strings in CSS classes to ignore. Pass `false`
// (or `--no-ignore` via cli) to not ignore these.
ignore: [
'html', // global element
'body', // global element
'button', // global element
'active', // state class
'inactive', // state class
'collapsed', // state class
'expanded', // state class
'show', // state class
'hide', // state class
'hidden', // state class
'is-', // state class
],
// Where to output
outputFile: 'dist/index.css',
// Disable output via stdout w/ `--no-stdout`.
stdout: false,
// Report Stats about used vs unused selectors.
report: false,
// Detailed Report Stats including every selector used vs unused.
// Note: this also depends on the `report` option being true.
reportDetails: false,
// Overwrite the input css file if there is only one.
overwriteCss: false,
}
```
## Usage with Postcss
```js
const postcssRazor = require('css-razor').postcss
postcss([
postcssRazor({
html: "your html string",
})
])
.process(css, {
from: 'index.css',
to: 'output.css'
})
```
## React to HTML Example
Below is an example of building an html file from a react app created with `create-react-app`. The resulting HTML file can then be used for server rendering and detecting selectors with css-razor.
index.js:
```js
import App from './components/App'
import './index.css'
if (typeof window !== 'undefined') { // Web
ReactDOM.render(
,
window.document.getElementById('root')
)
} else { // Node / server render
global.appToRender = App
}
```
buildStatic.js:
```js
const app = global.appToRender
const markup = ReactDOM.renderToString(ReactDOM.createElement(app));
const html = fs.readFileSync(HTML_FILE)
const newHtml = html.toString().split('
').join(
'' + markup + '
'
)
fs.writeFileSync(HTML_FILE, newHtml, 'utf8')
```
## Todo
- html input via stdin?
- more tests for raw and globs
- test for postcss plugin usage
================================================
FILE: cli.js
================================================
#!/usr/bin/env node
const cssRazor = require('./index.js').default
const defaultOptions = require('./defaultOptions.js')
const argv = require('yargs')
.usage('Usage: $0 [options]')
.argv
if (process.argv && process.argv.length > 2) {
defaultOptions.outputFile = '' // Default to no output file over cli because of stdout.
const options = Object.assign({}, defaultOptions, argv)
options._.forEach((arg, i) => {
if (arg.indexOf('.html') === arg.length - 5) {
options.html.push(arg)
} else if (arg.indexOf('.css') === arg.length - 4) {
options.css.push(arg)
}
// TODO: Set more CLI options here.
})
cssRazor(options, (err, data) => {
if (err) {
process.stderr.write(err)
process.exit(1)
}
if (options.stdout) {
process.stdout.write(data.css)
process.exit(0)
}
})
} else {
throw new Error('You need to pass arguments to css-razor')
}
================================================
FILE: defaultOptions.js
================================================
module.exports = {
// Array of HTML file globs.
html: [],
// Array of CSS file globs.
css: [],
// Raw HTML string.
htmlRaw: '',
// Raw CSS string.
cssRaw: '',
// Array of webpages to add to HTML.
webpages: [],
// Strings in CSS classes to ignore. Pass `false`
// (or `--no-ignore` via cli) to not ignore these.
ignore: [
'html', // global element
'body', // global element
'button', // global element
'active', // state class
'inactive', // state class
'collapsed', // state class
'expanded', // state class
'show', // state class
'hide', // state class
'hidden', // state class
'is-' // state class
],
// Where to output
outputFile: 'dist/index.css',
// Disable output via stdout w/ `--no-stdout`.
stdout: false,
// Report Stats about used vs unused selectors.
report: false,
// Detailed Report Stats including every selector used vs unused.
// Note: this also depends on the `report` option being true.
reportDetails: false,
// Overwrite the input css file if there is only one.
overwriteCss: false
}
================================================
FILE: index.js
================================================
'use strict'
const cheerio = require('cheerio')
const postcss = require('postcss')
const fs = require('fs')
const path = require('path')
const globby = require('globby')
const mkdirp = require('mkdirp')
require('es6-promise').polyfill()
require('isomorphic-fetch')
const defaultOptions = require('./defaultOptions')
const DELIMITER = ' || '
function cssRazor (options, callback) {
let ignoreList = []
if (typeof options.ignore === 'undefined') {
ignoreList = defaultOptions.ignore.concat(options.ignore)
}
options = Object.assign({}, defaultOptions, options)
options.ignore = ignoreList
if (!((options.htmlRaw || options.html.length || options.webpages.length) && (options.cssRaw || options.css.length))) {
throw new Error('You must include HTML and CSS for input.')
}
const p = new Promise(function (resolve, reject) {
let htmlRaw = options.htmlRaw
let cssRaw = options.cssRaw
Promise.all([
globby(options.html),
globby(options.css)
]).then((pathsArray) => {
const htmlFiles = pathsArray[0]
const cssFiles = pathsArray[1]
getTextFromUrls(options.webpages, (webHtml) =>
getTextFromFiles(htmlFiles, (html) =>
getTextFromFiles(cssFiles, (css) => {
// TODO: Is there a better way to do this. I'd rather not nest it
// but I don't want to pass more args either.
function processInput (html, css) {
const outputFile = options.overwriteCss
? cssFiles[0]
: options.outputFile
postcss([
postcssRazor({
html: html,
ignore: options.ignore,
report: options.report
})
])
.process(css, {
from: options.inputCss,
to: outputFile
})
.then((result) => {
if (outputFile) {
// Make sure the directory exists first.
mkdirp(path.dirname(outputFile), (err, d1) => {
if (err) {
return reject(err)
}
fs.writeFile(outputFile, result.css, (err, d2) => {
if (err) {
return reject(err)
}
resolve(result)
})
})
} else {
resolve(result)
}
})
.catch((e) => {
reject(e)
})
}
return processInput(html + htmlRaw + webHtml, css + cssRaw)
})
)
)
})
})
// Enable callback support too.
if (callback) {
p.then((result) => {
callback(null, result)
}).catch(err => callback(err))
}
return p
}
const postcssRazor = postcss.plugin('postcss-razor', (opt) => {
const html = opt.html
let keepCount = 0
let keepSelectors = ''
let removeCount = 0
let removeSelectors = ''
return (root) => {
const $ = cheerio.load(html)
root.walk((node) => {
if (node.type === 'rule') {
const exists = checkExists(node, $)
const ignore = opt.ignore.some((ignore) => {
return node.selector.indexOf(ignore) !== -1
})
if (!exists && !ignore) {
node.remove()
removeSelectors += node.selector + DELIMITER
removeCount++
} else {
keepSelectors += node.selector + DELIMITER
keepCount++
}
}
})
// Remove empty media queries.
root.walkAtRules((rule) => {
if (typeof rule.nodes === 'undefined' || rule.nodes.length === 0) {
rule.remove()
}
})
if (opt.report) {
const percent = ((removeCount / (keepCount + removeCount)) * 100).toFixed()
console.log(' Selectors kept: ' + keepCount)
console.log('Selectors removed: ' + removeCount)
console.log(' Percent removed: ' + percent + '%')
console.log(' ')
if (opt.reportDetails) {
console.log('Removed selectors: ' + removeSelectors)
console.log(' ')
console.log(' Kept selectors: ' + keepSelectors)
}
}
}
})
function getTextFromFiles (files, cb) {
let text = ''
if (files.length) {
files.forEach((file, i) => {
fs.readFile(file, (err, data) => {
if (err) {
console.error(err)
}
text += data.toString()
if (i === files.length - 1) {
cb(text)
}
})
})
} else {
cb(text)
}
}
function getTextFromUrls (urls, cb) {
let text = ''
if (urls.length) {
urls.forEach((file, i) => {
fetch(file).then(function (response) {
if (response.status >= 400) {
throw new Error('Bad response from server')
}
return response.text()
}).then(function (responseText) {
text += responseText
if (i === urls.length - 1) {
cb(text)
}
})
})
} else {
cb(text)
}
}
function checkExists (node, $) {
// Right now this try is needed because cheerio doesn't handle `pseudo-element` well.
// See: https://github.com/cheeriojs/cheerio/issues/979
try {
return $(removePseudoClasses(node.selector)).length > 0
} catch (e) {
return true
}
}
function removePseudoClasses (selector) {
return [
':active',
':focus',
':hover',
':visited',
'::before',
':before',
'::after',
':after'
].reduce((p, c) => {
return p.split(c).join('')
}, selector)
}
module.exports = {
default: cssRazor,
postcss: postcssRazor
}
================================================
FILE: index.test.js
================================================
const Code = require('code') // assertion library
const Lab = require('lab')
const lab = exports.lab = Lab.script()
const cssRazor = require('./index').default
const spawn = require('child_process').spawn
// Test output
const testResults = require('./test/results.js')
lab.experiment('css-razor', () => {
lab.test('returns promise with used CSS based on input HTML & CSS', (done) => {
cssRazor({
html: ['test/input/index.html'],
css: ['test/input/index.css'],
outputFile: 'test/output/index.css'
}).then((data) => {
Code.expect(data.css.split('\r').join('')).to.equal(testResults.simpleCss)
done()
})
})
lab.test('calls callback with used CSS based on input HTML & CSS', (done) => {
cssRazor({
html: ['test/input/index.html'],
css: ['test/input/index.css'],
outputFile: 'test/output/index.css'
}, function (err, data) {
if (err) {
console.error(err)
}
Code.expect(data.css.split('\r').join('')).to.equal(testResults.simpleCss)
done()
})
})
lab.test('returns promise with used CSS based on more complex input HTML & CSS', (done) => {
cssRazor({
html: ['test/input/tachyons.html'],
css: ['test/input/tachyons.min.css'],
outputFile: 'test/output/tachyons.css'
}).then((data) => {
Code.expect(data.css.split('\r').join('')).to.equal(testResults.complexCss)
done()
})
})
lab.test('calls callback with used CSS based on more complex input webpage & CSS', (done) => {
cssRazor({
webpages: ['http://blog.timscanlin.net/'],
css: ['test/input/tachyons.min.css'],
outputFile: 'test/output/tachyons.css'
}, function (err, data) {
if (err) {
console.error(err)
}
Code.expect(data.css.split('\r').join('')).to.equal(testResults.complexHttpCss)
done()
})
})
lab.test('CLI returns used CSS based on input HTML & CSS', (done) => {
const cli = spawn('node', [
'./cli.js',
'test/input/index.html',
'test/input/index.css'
])
cli.stdout.on('data', (data) => {
Code.expect(data.toString().split('\r').join('')).to.equal(testResults.simpleCss)
})
cli.on('close', (code) => {
Code.expect(code).to.equal(0)
done()
})
})
// empty input
// no files
// multiple files
// raw
// postcss
// set output file
})
================================================
FILE: package.json
================================================
{
"name": "css-razor",
"version": "2.4.4",
"description": "Remove unused selectors from CSS efficiently",
"main": "index.js",
"bin": {
"css-razor": "./cli.js"
},
"scripts": {
"lint": "standard --globals=fetch",
"test:unit": "lab ./index.test.js --ignore fetch,Response,Headers,Request,Base64",
"test": "npm run lint && npm run test:unit",
"v-patch": "npm version patch && git push --tags && npm publish && git push",
"v-minor": "npm version minor && git push --tags && npm publish && git push",
"v-major": "npm version major && git push --tags && npm publish && git push"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tscanlin/css-razor.git"
},
"keywords": [
"css",
"razor",
"strip",
"cut",
"slim",
"cheerio",
"postcss",
"postcss-runner"
],
"author": "Tim Scanlin",
"license": "MIT",
"bugs": {
"url": "https://github.com/tscanlin/css-razor/issues"
},
"homepage": "https://github.com/tscanlin/css-razor#readme",
"dependencies": {
"cheerio": "^0.22.0",
"es6-promise": "^4.1.0",
"globby": "^6.1.0",
"isomorphic-fetch": "^2.2.1",
"mkdirp": "^0.5.1",
"postcss": "^5.2.13",
"yargs": "^6.6.0"
},
"devDependencies": {
"code": "^4.0.0",
"lab": "^12.1.0",
"standard": "^10.0.2"
}
}
================================================
FILE: test/input/index.css
================================================
body {
font-size: 20px;
}
.some-element {
margin: 20px;
}
.some-element .inner-element {
text-align: center;
}
.some-element > .inner-element {
color: blue;
}
.non-existent {
padding: 10px;
}
@media screen and (min-width > 200px) {
.nothing {
font-weight: bold;
}
}
================================================
FILE: test/input/index.html
================================================
Foo Bar
================================================
FILE: test/input/tachyons.html
================================================
If it fits, i sits burrow under covers. Destroy couch leave hair everywhere,
and touch water with paw then recoil in horror.
================================================
FILE: test/results.js
================================================
exports.simpleCss = 'body {\n font-size: 20px;\n}\n\n.some-element {\n margin: 20px;\n}\n\n.some-element .inner-element {\n text-align: center;\n}\n\n.some-element > .inner-element {\n color: blue;\n}\n'
exports.complexCss = '/*! TACHYONS v4.6.1 | http://tachyons.io */\n/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}a:active,a:hover{outline-width:0}img{border-style:none}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}/* 1 */ [type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}/* 1 */[hidden],template{display:none}.border-box,a,article,body,code,dd,div,dl,dt,fieldset,footer,form,h1,h2,h3,h4,h5,h6,header,html,input[type=email],input[type=number],input[type=password],input[type=tel],input[type=text],input[type=url],legend,li,main,ol,p,pre,section,table,td,textarea,th,tr,ul{box-sizing:border-box}img{max-width:100%}.ba{border-style:solid;border-width:1px}.b--black-10{border-color:rgba(0,0,0,.1)}.br2{border-radius:.25rem}.br--top{border-bottom-right-radius:0}.br--right,.br--top{border-bottom-left-radius:0}.db{display:block}.dt{display:table}.dtc{display:table-cell}.button-reset::-moz-focus-inner,.input-reset::-moz-focus-inner{border:0;padding:0}.lh-copy{line-height:1.5}.link,.link:active,.link:focus,.link:hover,.link:link,.link:visited{-webkit-transition:color .15s ease-in;transition:color .15s ease-in}.mw5{max-width:16rem}.w-100{width:100%}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-hidden{overflow-y:hidden}.dark-gray{color:#333}.mid-gray{color:#555}.pa2{padding:.5rem}.mt1{margin-top:.25rem}.mt2{margin-top:.5rem}.mv0{margin-top:0;margin-bottom:0}.mv4{margin-top:2rem;margin-bottom:2rem}.tr{text-align:right}.f5{font-size:1rem}.f6{font-size:.875rem}.measure{max-width:30em}.center{margin-right:auto;margin-left:auto}.dim:active{opacity:.8;-webkit-transition:opacity .15s ease-out;transition:opacity .15s ease-out}.hide-child .child{opacity:0;-webkit-transition:opacity .15s ease-in;transition:opacity .15s ease-in}.hide-child:active .child,.hide-child:focus .child,.hide-child:hover .child{opacity:1;-webkit-transition:opacity .15s ease-in;transition:opacity .15s ease-in}.grow:active{-webkit-transform:scale(.9);transform:scale(.9)}.grow-large:active{-webkit-transform:scale(.95);transform:scale(.95)}@media screen and (min-width:30em){.overflow-hidden-ns{overflow:hidden}.overflow-x-hidden-ns{overflow-x:hidden}.overflow-y-hidden-ns{overflow-y:hidden}.pb3-ns{padding-bottom:1rem}.ph3-ns{padding-left:1rem;padding-right:1rem}.f4-ns{font-size:1.25rem}}@media screen and (min-width:30em) and (max-width:60em){.w-50-m{width:50%}.overflow-hidden-m{overflow:hidden}.overflow-x-hidden-m{overflow-x:hidden}.overflow-y-hidden-m{overflow-y:hidden}}@media screen and (min-width:60em){.w-25-l{width:25%}.overflow-hidden-l{overflow:hidden}.overflow-x-hidden-l{overflow-x:hidden}.overflow-y-hidden-l{overflow-y:hidden}}\n'
exports.complexHttpCss = '/*! TACHYONS v4.6.1 | http://tachyons.io */\n/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}small{font-size:80%}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}/* 1 */ [type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}/* 1 */[hidden],template{display:none}.border-box,a,article,body,code,dd,div,dl,dt,fieldset,footer,form,h1,h2,h3,h4,h5,h6,header,html,input[type=email],input[type=number],input[type=password],input[type=tel],input[type=text],input[type=url],legend,li,main,ol,p,pre,section,table,td,textarea,th,tr,ul{box-sizing:border-box}.bb{border-bottom-style:solid;border-bottom-width:1px}.b--light-gray{border-color:#eee}.top-0{top:0}.right-0{right:0}.db{display:block}.dib{display:inline-block}.normal{font-weight:400}.b{font-weight:700}.button-reset::-moz-focus-inner,.input-reset::-moz-focus-inner{border:0;padding:0}.lh-solid{line-height:1}.lh-title{line-height:1.25}.lh-copy{line-height:1.5}.link,.link:active,.link:focus,.link:hover,.link:link,.link:visited{-webkit-transition:color .15s ease-in;transition:color .15s ease-in}.mw6{max-width:32rem}.mw7{max-width:48rem}.w5{width:16rem}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-hidden{overflow-y:hidden}.relative{position:relative}.absolute{position:absolute}.o-80{opacity:.8}.o-70{opacity:.7}.o-60{opacity:.6}.o-40{opacity:.4}.black{color:#000}.white{color:#fff}.bg-mid-gray{background-color:#555}.pa0{padding:0}.pa1{padding:.25rem}.pa3{padding:1rem}.pb2{padding-bottom:.5rem}.pb3{padding-bottom:1rem}.pt2{padding-top:.5rem}.pv4{padding-top:2rem;padding-bottom:2rem}.ph1{padding-left:.25rem;padding-right:.25rem}.ma0{margin:0}.ma1{margin:.25rem}.mb4{margin-bottom:2rem}.mt4{margin-top:2rem}.mv1{margin-top:.25rem;margin-bottom:.25rem}.no-underline{text-decoration:none}.tc{text-align:center}.ttu{text-transform:uppercase}.f1{font-size:3rem}.f3{font-size:1.5rem}.f6{font-size:.875rem}.center{margin-right:auto;margin-left:auto}.dim:active{opacity:.8;-webkit-transition:opacity .15s ease-out;transition:opacity .15s ease-out}.glow,.glow:focus,.glow:hover{-webkit-transition:opacity .15s ease-in;transition:opacity .15s ease-in}.glow:focus,.glow:hover{opacity:1}.hide-child .child{opacity:0;-webkit-transition:opacity .15s ease-in;transition:opacity .15s ease-in}.hide-child:active .child,.hide-child:focus .child,.hide-child:hover .child{opacity:1;-webkit-transition:opacity .15s ease-in;transition:opacity .15s ease-in}.grow:active{-webkit-transform:scale(.9);transform:scale(.9)}.grow-large:active{-webkit-transform:scale(.95);transform:scale(.95)}@media screen and (min-width:30em){.overflow-hidden-ns{overflow:hidden}.overflow-x-hidden-ns{overflow-x:hidden}.overflow-y-hidden-ns{overflow-y:hidden}.pa4-ns{padding:2rem}}@media screen and (min-width:30em) and (max-width:60em){.overflow-hidden-m{overflow:hidden}.overflow-x-hidden-m{overflow-x:hidden}.overflow-y-hidden-m{overflow-y:hidden}}@media screen and (min-width:60em){.overflow-hidden-l{overflow:hidden}.overflow-x-hidden-l{overflow-x:hidden}.overflow-y-hidden-l{overflow-y:hidden}}\n'