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 ![Build Status](https://travis-ci.org/tscanlin/css-razor.svg?branch=master) 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 ================================================
Photo of a kitten looking menacing.

Cat

$1,000

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'